17611538698
webmaster@21cto.com

爬虫搞崩网站后,程序员自制“炸弹”反击

安全 2 45 12小时前
图片

自从我开通这个博客几个月后,流量就空前高涨。我写了一篇文章,分别投在 Hacker News 和 Reddit 都 “爆红”了。

它一登上内容头条,我的小服务器就要彻底崩溃了。请求像海啸一样涌入,Apache 苦苦挣扎,我只能无助地坐着,一遍又一遍地重启机器——就像消防员用水枪灭火一样。

这要拿互联网术语来说,这叫做“死亡拥抱”,挺吓人的

当时,我很难形容当时收到的请求有多么密集,以及给我带来的压力有多大。

图片

就在今年二月,我又写了一篇帖子,几分钟内就登上了 Hacker News 的榜单第一名。这一回,我可做好了充分的准备。我先保存了一份服务器日志,然后制作了一个可视化图表,准确展现我每月 6 美元的小服务器究竟经历了什么。

Web请求可视化


图片

服务器的每个 Web 请求都用一个向服务器移动的圆圈表示。查看右下角的图例:
-机器人 vs. 真实用户:基于用户代理检测。合法机器人的名称中通常包含“bot”,而其他机器人则通过启发式识别。

-响应类型如下

- ✅ 200 OK:请求成功
- 🔄重定向:表示为摆动的点
- ❌ 404 Not Found:屏幕上掉落的红点
- 💥 Zip 炸弹:稍后向各位详细介绍

我的服务器规格


尽管一片混乱,我那台每月 6 美元的小服务器,只配了 1GB 内存、Apache 2 + PHP以及一个基本的 MySQL 数据库,却依然屹立不倒。


没有花哨的云自动扩展,也没有负载均衡器。只有我精简的配置和良好的缓存使用。


  • 主机$DigitalOcean(1GB RAM)
  • Web服务器:Apache 2
  • 环境:Ubuntu + PHP
  • 数据库:MySQL
  • 价格:6美元/月


我的博客运行在一个自定义PHP框架上。大多数页面都缓存在memcached中,因此每个页面每小时仅查询一次数据库。这种高效的配置在过去曾处理过数百万次请求,包括我关于被机器学习解雇登上 BBC 的热门故事。


事件时间表


  • 🕓 下午 4:43 (太平洋标准时间) — 帖子已提交至 Hacker News。
  • 🕓 下午 4:53 (太平洋标准时间) — 进入主页。一大群机器人蜂拥而至。
  • 🕔 下午 5:17 (太平洋标准时间) — Hacker News 排名第一。闸门打开了。
  • 🕗 晚上 8:00(太平洋标准时间) ——版主重命名了该条目(原因不明)。流量骤降
  • 🕓 凌晨 3:56 (太平洋标准时间) — 一个机器人扫描 300 个 URL 以查找漏洞。
  • 🕘 上午 9:00 (太平洋标准时间) — 流量再次激增,主要来自 Mastodon 网络。
  • 🕤 上午 9:32 (太平洋标准时间) — 大规模垃圾邮件攻击:一分钟内约有 4,000 个请求,大部分是广告暗网市场。
  • 🕓 下午 4:00(太平洋标准时间) ——在 24 小时内,我的服务器处理了46,000 个请求

房间里的大象


服务器从未崩溃过。事实上,CPU 使用率从未超过16%


但是你可能已经注意到可视化中有些奇怪的事情:我的1GB RAM 服务器内存使用率一直保持在 50%。为什么?因为MySQL


当我开始写这个博客时,我雄心勃勃地将每一个请求都记录到数据库中。这对于追踪帖子的受欢迎程度很有用。但12年后,数据库规模膨胀。为了进行简单的分析而对数百万行数据进行排序变成了一项成本高昂的操作。


病毒式传播之后,我备份了数据,并删除了表。现在是时候了。


制造ZIP炸弹方法之一

网络上的大部分流量来自机器人。这些机器人大多用于发现新内容。例如 RSS 订阅阅读器、抓取内容的搜索引擎,或者如今为 LLM 提供内容支持的人工智能机器人。

但也存在不少恶意机器人。这些机器人来自垃圾邮件发送者、内容抓取者或黑客。在我的前雇主那里,一个机器人发现了 WordPress 的一个漏洞,并在我们的服务器中插入了一个恶意脚本。然后,它将服务器变成了一个用于 DDOS 攻击的僵尸网络。我的早期网站之一就因为机器人生成垃圾邮件而完全从谷歌搜索结果中下架。后来,我不得不想办法保护自己免受这些机器人的侵害。就在那时,我开始使用 zip 炸弹。

Zip 炸弹是一种相对较小的压缩文件,但它可以扩展为非常大的文件,从而压垮机器。

早期在网络上开发的一个功能是使用 gzip 进行压缩。由于互联网速度慢且信息密集,其理念是在传输数据之前尽可能地压缩数据。因此,一个由文本组成的 50 KB HTML 文件可以压缩到 10 KB,从而节省 40 KB 的传输空间。在拨号上网的情况下,这意味着下载页面只需 3 秒,而不是 12 秒。

同样的压缩技术也适用于 CSS、JavaScript 甚至图像。Gzip 快速、简单,并能显著提升浏览体验。

浏览器发出 Web 请求时,会包含标头,告知目标服务器其支持压缩。如果服务器也支持压缩,则会返回预期数据的压缩版本。

Accept-Encoding: gzip, deflate
爬取网络的机器人也支持此功能。由于它们的任务是从网络各处获取数据,因此它们会使用压缩来最大化带宽。我们可以充分利用此功能。

在这个博客上,我经常会遇到扫描安全漏洞的机器人,而我通常都会忽略它们。但当我检测到它们试图注入恶意攻击或探测响应时,我会返回 200 OK 响应,并向它们发送 gzip 压缩包。我接收的文件大小从 1MB 到 10MB 不等,它们很乐意接收。

大多数情况下,即使它们接收了,我也再也没有收到任何消息。为什么?因为它们在接收文件后就崩溃了。

Content-Encoding: deflate, gzip
实际情况是,他们收到文件后,读取文件头,发现这是一个压缩文件。于是他们尝试解压这个 1MB 的文件,寻找所需的内容。但文件不断膨胀,直到内存耗尽,服务器崩溃。1MB 的文件解压后变成了 1GB。这足以摧毁大多数机器人。

不过,对于那些不停歇的烦人脚本,我会给他们提供 10MB 的文件。这个文件解压后变成了 10GB,脚本瞬间就被干掉了。

在告诉你如何制作 Zip 炸弹之前,我必须先警告你,你的设备可能会崩溃甚至被毁。继续操作需自行承担风险。

以下是制作 Zip 炸弹的方法:

dd if=/dev/zero bs=1G count=10 | gzip -c > 10GB.gz
我将该命令的作用解释如下:

  1. dd:dd命令用于复制或转换数据。
  2. if:输入文件,指定/dev/zero产生无限零字节流的特殊文件。
  3. bs:块大小,将块大小设置为 1 千兆字节 (1G),这意味着 dd 将一次以 1 GB 的块读取和写入数据。
  4. count=10:这告诉 dd 处理 10 个块,每个块大小为 1 GB。因此,这将生成 10 GB 的零数据。


然后,我们将命令的输出传递给 gzip,它会将输出压缩成 10GB.gz 文件。在本例中,生成的文件大小为 10MB。

在我的服务器上,我添加了一个中间件,用于检查当前请求是否恶意。我设置了一个黑名单 IP 地址列表,这些 IP 地址会反复尝试扫描整个网站。我还设置了其他启发式方法来检测垃圾邮件发送者。许多垃圾邮件发送者会尝试向某个页面发送垃圾邮件,然后再回来查看垃圾邮件是否已经到达该页面。我使用以下模式来检测它们。

它看起来像这样:


if(ipIsBlackListed() || isMalicious()) {header("Content-Encoding: deflate, gzip");  	header("Content-Length: " + filesize(ZIP_BOMB_FILE_10G));//10 MBreadfile(ZIP_BOMB_FILE_10G); 	exit;}
就是这样。我唯一的代价就是现在有时需要提供 10MB 的文件。如果我的文章要火,我会把它压缩到 1MB,效果也一样好用。

还有一点,Zip 炸弹并非万无一失。它很容易被检测到并规避。毕竟,你只能读取部分内容。但对于那些盲目爬取网页、扰乱服务器的不熟练机器人来说,Zip 炸弹已经足够保护你的服务器了。

制造ZIP炸弹方法之二

首先,我们加点量,先创建一个 10GB 的 GZIP 文件,文件内容全部为零。我们可以进行多次压缩,目前先简单处理一下。

dd if=/dev/zero bs=1M count=10240 | gzip > 10G.gzip


可以看到,它有 10G 大。我们还可以做得更好,但目前已经足够了。

现在我们已经成功创建好了这个东西,让我们设置一个 PHP 脚本来将它传递给客户端。


//prepare the client to recieve GZIP data. This will not be suspicious
//since most web servers use GZIP by default
header("Content-Encoding: gzip");
header("Content-Length: ".filesize('10G.gzip'));
//Turn off output buffering
if (ob_get_level()) ob_end_clean();
//send the gzipped file to the client
readfile('10G.gzip');
好的,就是这样!

因此我们可以用它作为一个简单的防御,像这样的:


$agent filter_input(INPUT_SERVER, 'HTTP_USER_AGENT');
//check for nikto, sql map or "bad" subfolders which only exist on wordpress
if (strpos($agent'nikto') !== false || strpos($agent'sqlmap') !== false || startswith($url,'wp-') || startswith($url,'wordpress') || startswith($url,'wp/'))
{
   sendBomb();
   exit();
}
function sendBomb(){
    //prepare the client to recieve GZIP data. This will not be suspicious
    //since most web servers use GZIP by default
    header("Content-Encoding: gzip");
    header("Content-Length: ".filesize('10G.gzip'));
    //Turn off output buffering
    if (ob_get_level()) ob_end_clean();
    //send the gzipped file to the client
    readfile('10G.gzip');
}
function startsWith($a$b{
  return strpos($a$b) === 0;
}

正如我们在上面所说的那样,这个脚本显然不是鸡蛋的黄色,但它可以防御我之前提到的那些脚本小子,他们不知道所有这些工具都有更改用户代理的参数。

图片

使用Zip炸弹反击内容窃贼!


你可能注意到了动画中的小爆炸。让我来解释一下。

有一天,我发现一个网站正在实时窃取我的内容。每当有人访问他们的页面时,他们就会抓取我的博客文章,替换掉我的品牌标识,并将其据为己有。

一开始,我手动反击,给他们输入了假数据。但很快就没用了。所以我动用了我的秘密武器:给它来一个 zip 炸弹

当他们的机器人访问我的网站时,我向它提供了一个很小的压缩文件。他们的服务器迫不及待地下载并解压了它,结果却带来了好几 GB的混乱数据。💥砰砰砰!游戏结束。

多年来,zip 炸弹已经成为我抵御试图抓取、利用或滥用我的网站的机器人的 盾牌。


送给大家的经验教训


  • Dang(Hacker News mod)会更改你的文章标题,而我们对此无能为力。
  • 大多数流量来自机器人,而非人类。它们不停地扫描、抓取信息,并发送垃圾邮件。
  • Apache 的工作线程限制很重要。我发现有两个正在运行的实例的最大工作线程数为75 个,这是我之前没有注意到的。
  • 经过优化的轻量级配置胜过昂贵的基础设施。有了适当的缓存,每月 6 美元的服务器就能承受数万次访问——无需 Kubernetes。


结论


这段血泪经历,教会我的不仅仅是服务器管理。


观察网络请求可视化的展开,让我对流量的流动方式、机器人的运作方式有了新的认识,甚至最简单的优化决策也可能决定服务器是崩溃还是顺利运行。


最重要的是:如果你想要走红,请充分做好准备哦~

作者:行动中的大雄

参考:

https://idiallo.com/blog/pc-is-not-dead-no-need-for-new-ones

https://github.com/ibudiallo/reqvis

评论