☰
【八个月网安课程】第十三周·周二:条件竞争上传进阶——多线程并发突破与防御绕过
2026/10/30 2:29:11 网站建设 项目流程

在第十一周已掌握了条件竞争的基本原理和单文件并发上传攻击。今天我们将把难度提升到实战级:面对文件名随机化、更复杂的校验时序、以及多文件上传场景,如何设计并发策略并利用细微的时间差突破防线。还会学到如何将条件竞争与其他漏洞(如文件包含)结合,形成更稳定的攻击链。


第十三周·周二:条件竞争上传进阶——多线程并发突破与防御绕过

🎯 今日学习目标(达成效果)

  • 能分析目标上传逻辑中条件竞争的具体窗口位置,并判断窗口大小(校验前保存、重命名前访问、移动前等);
  • 能编写定制化的并发脚本(Python 多线程或 Turbo Intruder 高级脚本),在文件名随机化的情况下仍实现竞争写入或访问;
  • 能利用多文件上传、并发请求的不同组合,扩大竞争窗口命中率,包括:上传同时访问、上传同时触发包含、多线程同时写同一文件等;
  • 能在 Upload-labs 高级关卡或自建复杂靶场中,面对文件名随机化、先校验后移动等加固防御时,仍通过条件竞争获取 Webshell;
  • 能总结防御条件竞争的正确姿势:原子操作(先校验后移动)、文件锁定、限制并发请求频率、存储目录不可访问等,并理解为何简单的随机文件名不能阻止条件竞争。

📘 一、条件竞争的深层理解:不止是“上传+访问”

在基础实战中,我们利用了“先保存后校验”的窗口,通过并发“上传”和“访问”两个动作来竞争。但在真实环境中,防御方可能会:

  • 随机重命名:上传文件被赋予不可预测的名称,攻击者无法直接访问。
  • 先校验后移动:文件先在临时目录校验,通过后才移动到 Web 可访问目录,基本消除了窗口。
  • 重名校验覆盖:如果允许覆盖,可以尝试多线程同时上传同一文件名,利用文件写入的非原子性,在覆盖间隙执行代码(更难)。
  • 上传后即被包含:结合业务逻辑,例如上传头像后页面立即刷新包含该头像,如果头像路径可控且能被包含,可在包含发生的瞬间竞争。

今日核心思维:条件竞争攻击 =并发执行+精确的时序预测。你需要仔细审计代码或通过黑盒测试推断服务端的处理顺序,然后设计并发任务,让恶意操作正好落在“已保存但未清除”的瞬间。


📘 二、绕过随机文件名的条件竞争策略

当服务端使用uniqid()或random_bytes()重命名文件后,攻击者无法提前知道文件名。因此常规的“访问/upload/shell.php”线程失效。怎么办?

策略 A:竞争写同一文件(Race to Overwrite)

如果服务器允许覆盖已存在的文件(即目标目录中存在同名文件时会被覆盖),攻击者可以多线程并发上传相同文件名。设想以下逻辑:

$target='uploads/important.jpg';move_uploaded_file($tmp,$target);// 线程1 上传 shell.jpg -> 覆盖 important.jpg

如果另一个线程在某个瞬间正在读取important.jpg(比如通过include或直接访问),而这个文件正好被我们的恶意文件覆盖了,就可能执行我们的代码。

利用场景:头像上传、站点 Logo 替换、配置文件覆盖等。

策略 B:竞态包含(Race to Include)

即使文件名随机,如果网站存在文件包含漏洞并且上传功能与包含点在同一请求/会话中相关联,我们可以并发发送:

  • 线程 A:不断上传恶意文件(每次文件名不同,但我们从服务器响应中获取新文件名)。
  • 线程 B:不断请求包含最新文件名(通过解析线程 A 的响应实时获取)。
    这需要实时通信,可通过 Python 多线程共享变量或 Turbo Intruder 的gate机制实现。

典型场景:用户上传头像后,页面立即包含显示。若我们能预测时间,可提前发送包含请求,使其恰好包含我们刚上传的恶意临时文件。

策略 C:临时文件竞争(Race the Temp File)

PHP 在上传时,文件先保存在临时目录(如/tmp/phpXXXXXX),脚本通过$_FILES['file']['tmp_name']获取路径。如果脚本在处理过程中执行了某些文件操作(如include),且临时文件尚未被自动删除,可能被包含执行。需要极短时间内完成。


✍️ 三、动手实践:搭建高难度条件竞争靶场并突破

实践 1:自建“随机文件名 + 即时删除”靶场

创建race_advanced.php:

<?php$upload_dir='uploads/';if($_FILES['file']['error']===UPLOAD_ERR_OK){$tmp=$_FILES['file']['tmp_name'];$ext=pathinfo($_FILES['file']['name'],PATHINFO_EXTENSION);// 随机文件名,攻击者无法预知$new_name=bin2hex(random_bytes(16)).'.'.$ext;$dest=$upload_dir.$new_name;if(move_uploaded_file($tmp,$dest)){// 检查文件内容是否合法(模拟校验)$content=file_get_contents($dest);if(strpos($content,'<?php')!==false){unlink($dest);// 发现恶意代码,删除!echo'危险文件已拦截';}else{echo'上传成功,文件:'.$new_name;}}}?>

分析:文件先移动到目标目录,然后读取内容检查,发现 PHP 代码则删除。存在窗口:从move_uploaded_file到unlink之间,文件以随机名存在于uploads/。由于文件名随机,攻击者无法事先知道完整路径。

绕过思路:使用并发写同一文件?但文件名随机,每次不同。我们可以并发上传大量文件,并同时并发列出目录(list),一旦发现某个文件存在(在我们删除之前),立即访问它。但访问线程也需要知道文件名。解决办法:利用目录列表(如果有),或暴力破解短时间内的文件名(不太现实)。更好的办法是:如果服务端在删除前返回了文件名,我们的访问线程可以解析响应获取。然而这个逻辑中,返回文件名在删除之后?不,代码先move,然后file_get_contents,再strpos,如果匹配则unlink,最后输出“危险文件”。所以,在strpos到unlink之间,文件存在且名字已知(在脚本变量中,但尚未发送给客户端)。客户端要等到整个 PHP 执行完才能收到响应。因此客户端无法提前获取文件名。窗口对远程攻击者极短,几乎无法利用。

转向:我们需要寻找更实际的场景,比如文件上传 + 文件包含组合,或者覆盖式竞争。

实践 2:覆盖式竞争——头像上传场景

模拟一个用户头像上传功能,头像固定存储在avatars/user123.jpg。

$target='avatars/user123.jpg';move_uploaded_file($tmp,$target);// 不校验,直接覆盖

攻击者并发大量上传请求,每个都携带不同的恶意内容。同时,不断向目标头像 URL 发送请求。如果在某个请求中,头像文件正好被我们的恶意 PHP 代码覆盖,且服务器以include或require方式调用了该头像(比如在用户主页中include "avatars/user123.jpg"),则代码执行。

实践步骤:

  1. 编写竞争脚本(Pythonthreading示例):
importrequestsimportthreadingdefupload():whileTrue:files={'file':('shell.php','<?php system("id");?>','image/jpeg')}requests.post('http://target/upload.php',files=files)defaccess():whileTrue:requests.get('http://target/avatars/user123.jpg')for_inrange(20):threading.Thread(target=upload).start()threading.Thread(target=access).start()
  1. 运行脚本,观察访问线程的响应中是否出现uid=...等命令输出。

注意:头像通常直接作为<img src>加载,不会执行 PHP。因此需要配合文件包含点或解析漏洞。真实的覆盖竞争多用于覆盖配置文件(如config.php),使其包含我们的恶意定义。

实践 3:结合 LFI 的竞态包含

场景:上传功能将文件存到/uploads/(随机名),同时网站有include.php?file=xxx。上传功能在返回文件名前有短暂处理。我们并发执行:

  • 线程 A:不断上传恶意图片马,并记录每个新文件名(通过解析上传响应或目录列表)。
  • 线程 B:不断尝试包含/uploads/下最新的文件名(从线程 A 的反馈中获取)。
    可以在 Python 中使用线程安全队列传递文件名。

这需要精确同步,难度高,但在 CTF 中常见。今天无需完全复现,重点是理解这种攻击模型。


📝 四、课后测试题与解析

测试题 1:如果文件上传后文件名被随机化,为何条件竞争攻击仍有可能成功?请描述一种攻击场景。

参考答案:
即使文件名随机,攻击者仍可通过以下方式竞争:

  • 竞态包含:如果网站同时存在文件包含漏洞,攻击者可以并发执行上传和包含操作。上传线程不断提交恶意文件,包含线程不断尝试包含新生成的文件。如果包含线程能够在文件被删除或重命名之前的极短时间内请求到正确文件名,即可执行代码。更高级的方法是上传线程每次上传成功后,立即将新文件名传递给包含线程(通过内存共享或实时解析响应),实现精准打击。
  • 覆盖式竞争:当上传功能覆盖固定文件(如头像、Logo)时,文件名是已知的。攻击者并发上传同一文件名,同时并发请求该文件的 URL。如果请求恰好在文件被恶意内容覆盖后发生,且服务器以可执行方式处理该文件(如包含),则攻击成功。

✅ 今日学习效果自检清单

  • 我理解了条件竞争不只是“上传+访问”,还包括覆盖文件、竞态包含等高级形式
  • 我能设计一个针对随机文件名防御的竞争方案(如多线程传递文件名)
  • 我使用 Python 多线程或 Turbo Intruder 脚本实现了覆盖式竞争攻击
  • 我知道如何结合文件包含漏洞来放大条件竞争的效果
  • 我能够分析源码中条件竞争窗口的具体位置(move_uploaded_file 与 unlink 之间等)
  • 我明白防御条件竞争的核心是原子操作、先校验后移动、限制并发、存储目录不可访问

⚠️ 阶段避坑重点

  • 不要在高延迟网络练习条件竞争:成功率依赖极低的时延,本地环境最佳。
  • 并发线程数不宜无脑增大:过多的并发会导致服务器崩溃或触发防火墙,失去测试意义。从低线程开始逐步增加,找到平衡点。
  • 注意请求顺序与依赖:如果包含线程依赖上传线程提供的文件名,必须设计好通信机制,避免死锁。
  • 覆盖竞争需确认后端权限:覆盖文件需要 Web 进程对目标文件有写权限,且无特殊保护(如文件锁定)。
  • 始终在授权环境测试:条件竞争可能对服务器造成较大压力,未经授权测试属于违法行为。

今天你已具备应对复杂条件竞争场景的能力。明天我们将进入逻辑漏洞专项,学习如何挖掘业务逻辑中的越权、支付绕过等漏洞,这些往往比技术漏洞更难防御,也更具价值。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询