upload-labs 这个靶场,我在不少入门群里见过两种极端:一种是把 21 关的“通关答案”小抄背得滚瓜烂熟,一问为什么这关加个空格能过、那关必须双写,答不上来;另一种是每关都手动传、手动改、手动猜,21 关折腾一下午,换台机器换套环境又从头开始。
我自己的看法是,这个靶场最值得练的其实不是“绕过技巧清单”,而是“判断校验逻辑”的能力。upload-labs 的关卡看起来花样百出,本质上不过是前端校验、MIME 类型校验、扩展名黑名单、扩展名白名单、文件内容校验、条件竞争这几类逻辑的变体。而 Burp Suite 在这种场景下几乎是天生的自动化工具——Intruder 可以替你把同一个请求的几十上百种变体一次性发出去,Repeater 负责精细调整,Turbo Intruder 处理并发请求,组合起来能把“死记硬背”变成“带着假设批量验证”。
这篇文章就按这个思路展开:先教你拆解每一关的过滤点,再讲不同场景该用 Burp 的哪个模块,然后是一套可以直接照做的自动化测试流程,最后挑几个典型关卡做实战拆解。文章末尾会说明配套项目文件里有什么、怎么组织。适合刚学完文件上传基础、想系统刷靶场,或者准备面试梳理上传漏洞知识点的同学。
1. upload-labs 闯关的本质:先看懂每一关在过滤什么
1.1 别急着背答案,先把 21 关按校验类型归档
很多朋友一上来就盯着每一关的“唯一解”,然后照着答案挨个试。这其实把靶场用反了。upload-labs 设计的核心不是让你记住 21 个绕过姿势,而是让你看清服务端到底在哪一层做了校验。校验位置不同,绕过思路完全不同。
我把常见的关卡按校验维度整理成了一下面这张表,刷靶场的时候先翻它,再动手。
| 校验维度 | 常见实现方式 | 典型绕过方向 | 靶场代表关卡 |
|---|---|---|---|
| 前端校验 | JS 检查扩展名或文件类型 | 直接改包重放、禁用 JS | 第 1 关 |
| MIME 类型 | 检查 Content-Type 头 | 改成 image/jpeg、image/png | 第 2 关 |
| 扩展名黑名单 | 过滤 php、jsp、asp 等后缀 | 遗漏扩展名、大小写、空格、点、::$DATA、双写、.htaccess | 第 3~10 关 |
| 扩展名白名单 | 只允许 jpg、png、gif | %00 截断、路径可控、数组覆盖 | 白名单类关卡 |
| 文件内容 | getimagesize、文件头、二次渲染 | 图片马、渲染保留区写入代码 | 图片马类关卡 |
| 业务逻辑 | 先传后删、先存后改名 | 条件竞争 | 竞争类关卡 |
注意,很多关卡不是在单一维度上做校验的,而是叠加。比如同样是黑名单,第 5 关可能只过滤了小写 .php,却漏了大小写变形;第 6 关补了大小写,但没过滤文件名末尾加空格;第 7 关又补了空格,但没过滤末尾加“.”;第 8 关可能又漏了 Windows 的“::$DATA”……关卡数量看着多,其实就是在同一个逻辑上不断打补丁。这种“同一请求模板、只替换一个变量的微调场景”,恰好是 Burp Suite 自动化最擅长的活。
1.2 自动化测试不是乱跑字典,而是“假设-验证-归类”
我知道你一听到“自动化测试”,第一反应可能是搞一个脚本把几千条 payload 全部打一遍。我一开始也这么干过,结果跑完一脸懵:前端的 JS 校验、服务端的 MIME 校验、扩展名黑名单,面对同一个 payload 集合的表现完全不同,密密麻麻的结果根本没法看。后来才明白,自动化测试的第一步不是“跑”,而是“分类”。
拿修水管打比方:你得先判断是漏水还是堵塞,然后再挑工具,而不是把扳手、螺丝刀、胶带全部往水管上招呼一遍。upload-labs 也一样,动手之前先做两个基线测试:
- 上传一个正常的
test.jpg,观察响应。 - 再上传一个正常的
test.php,观察响应。
如果正常 jpg 能过、php 被拦,说明这里存在校验,从响应文案和拦截特征基本能判断是前端、MIME 还是扩展名层面的校验。如果 jpg 也被拦,可能是内容校验或白名单校验。定位到校验类型之后,再去选对应的 payload 集合,让 Burp 批量验证。
这才是自动化的正确打开方式:你先提出一个假设——“这关是黑名单校验,而且只过滤了小写 .php”,然后构造一个包含大小写、空格、点、双写变体的 payload 列表去验证。如果假设错了,响应会立刻告诉你:所有变体全挂了,那就回退一步重新归类。这个“假设-验证-归类”的循环,才是刷 upload-labs 真正的收获。
2. 工具选型:Burp Suite 里哪几个模块适合干这活
2.1 Intruder:批量验证“一个变量、多个取值”的主力
Burp Suite 的 Intruder 在 upload-labs 里是使用频率最高的模块。它做的事情很简单:拿一条真实的上传请求,把文件名参数标记成一个变量,然后把你准备好的 payload 列表依次填充进去,逐个发送并记录响应。
Intruder 有四种攻击模式,很多新手一上来就 Cluster bomb 猛干,其实大部分场景用不上:
- Sniper:单变量模式,只在一个位置注入 payload。99% 的上传关卡用这个就够了。
- Battering ram:多个位置共用同一个 payload。适合同时替换文件名和 Content-Type 的场景。
- Pitchfork:多个位置分别取 payload 列表的第 n 个,像拉链一样一一对应。适合“文件名和 Content-Type 需要配套变化”的场景。
- Cluster bomb:全组合笛卡尔积。payload 数量会爆炸式增长,我一般只在精确控制两个变量时才用,不然结果会多到没法看。
实操的时候,右键抓到的上传请求,选择 Send to Intruder,在filename="test.php"的位置把test.php选中,点 Add §,标记成§payload§,然后在 Payloads 选项卡里加载你自己的字典,选 Sniper 模式,直接开跑。
这里要提醒一句:Community 版 Intruder 有内置延迟限速,跑几十条 payload 没问题,跑上万条会等到你想砸电脑。我的建议是,先根据校验类型精选 20~50 条高概率 payload,而不是一上来就全量字典。
2.2 Repeater:精细控制复杂请求的调试台
Intruder 解决的是“量”的问题,Repeater 解决的是“精度”的问题。有些请求不适合批量跑,比如白名单关卡的%00截断、数组参数覆盖、需要修改十六进制字节的请求。这些场景下,用 Repeater 手工逐条调试反而更高效。
Repeater 最大的优势是能直接看到请求的原始字节。切换到 Hex 标签页,你可以把%00从“三个 ASCII 字符”改成真正的“一个字节 0x00”,这种精细控制在 Intruder 里设置起来比较绕,但在 Repeater 里就是选中、替换、发送三步的事。另外,Repeater 能保留每次发送的历史记录,方便你对比不同字节组合下服务端的响应差异。
我的习惯是:先用 Intruder 批量定位“大概方向”,再用 Repeater 做“精确确认”。比如 Intruder 跑出来.php变形能上传成功,下一步一定用 Repeater 手动发一遍同样请求,仔细看响应体里的每一句话,确认是不是真的成功,而不是被某个提示词骗了。
2.3 Turbo Intruder:并发与条件竞争的专属武器
upload-labs 里有几关是条件竞争,核心逻辑是服务端先接收文件,然后再检查或改名,中间存在一个极短的时间窗口。要抓住这个窗口,普通 Intruder 的串行或者说低并发模式不够看,这时候就轮到 Turbo Intruder 出场。
Turbo Intruder 是 Burp 的一个扩展,核心特点是让你用 Python 脚本控制并发请求。一个最简单的条件竞争脚本长这样:
def queueRequests(target, wordlists): engine = RequestEngine(endpoint=target.endpoint, concurrentConnections=50) for i in range(500): engine.queue(target.req)上面的脚本会建立 50 个并发连接,连续向目标发送 500 次相同的上传请求。配合另一个窗口持续访问已上传文件的 URL,就能在服务端删文件或者改名之前,抢先把恶意文件加载执行。Turbo Intruder 这类扩展主要是在专业版环境下使用,Community 版老老实实用 Intruder 跑小并发也够入门。
2.4 宏与会话处理:对付动态 token 和随机文件名
上传接口如果带了 CSRF token、或者每次请求需要从页面提取随机值,Intruder 的重放就失效了——因为所有请求携带的是同一个 token,服务端会拒绝后续请求。
解决办法是使用 Burp 的会话处理规则(Session Handling Rules)和宏(Macros)。思路是:在 Project options 的 Sessions 里新建一条规则,规则触发时先去执行一个“获取 token”的宏请求,从响应里用正则提取出最新 token,再替换到当前请求的对应参数位置。这样 Intruder 跑每条请求前都会自动刷新 token,自动化流程才真正成立。
这个配置稍微有点绕,但值得掌握。upload-labs 本身不太依赖 token,不过你以后测真实目标接口时,这是个高频刚需。
2.5 为什么不直接写 Python 脚本一把梭
说到自动化,很多人会问:为什么不直接写个 Python 脚本用 requests 库跑?我也这么想过,后来发现 upload-labs 这类靶场的请求不是纯粹的静态 POST——它往往有登录态、有 token、有的关卡上传后还要跟进访问文件路径。用脚本写当然可以,但每遇到一个新场景就要改代码,调试成本非常高。
Burp 的工作流天然是“拿一条真实请求,改一个点,重放对比”,不需要你理解整个业务流程,只需要专注于“在校验点附近做变化”。至于 Python 脚本,我更建议把它用在独立的文件处理环节,比如生成图片马、对比二次渲染前后的图片差异。两者配合,各自干自己最擅长的事。
3. 实操流程:把自动遍历跑起来
3.1 环境准备:本地靶场、Burp 代理、基线请求
先把环境理顺。upload-labs 是 PHP 靶场,推荐在本机用集成环境跑起来,注意 Apache + PHP 的版本匹配。很多关卡依赖老版本特性,比如某些扩展名在较新的 PHP 版本里已经不再作为可执行后缀,跑不通不一定是你姿势不对,也可能是环境问题。
Burp 的代理配置是老生常谈:浏览器设置 HTTP 代理指向127.0.0.1:8080,Burp 默认监听这个地址。本地 http 靶场不需要装证书,如果你后面要测 https 的站点,再去安装 Burp 的 CA 证书。配置完成后,随便访问一下靶场页面,确认代理能正常抓到流量。
动手之前,做一个基线测试。用浏览器正常上传一张test.jpg,然后在 Burp 的历史记录里找到这条上传请求,右键存到 Repeater。再上传一个test.php,把请求也存下来。这两个基线请求就是你所有自动化测试的起点模板。
3.2 第一次自动化:用 Intruder 批量验证扩展名黑名单
假设你判断当前关卡是黑名单校验,目标就是找出黑名单里漏掉了哪些变形。操作流程如下:
- 在 Repeater 里打开刚才保存的上传请求,找到
filename="test.jpg",右键 Send to Intruder。 - 选中
test.jpg,点击 Add §,请求变成filename="§test.jpg§"。这里的 payload 就是完整的文件名。 - 在 Payloads 选项卡选择 Simple list,把准备好的文件名变体列表粘贴进去。列表要包含
test.php、test.php3、test.phtml、test.pHp、test.php、test.php.、test.php::$DATA、test.p.phphp这类典型变形。 - 在 Settings 选项卡找到 Grep - Match,添加几个关键词,比如“成功”“不允许”“fail”“error”。这样每条 payload 是否命中关键字,会在结果表里直接打勾。
- 点 Start attack,跑完以后按 Grep - Match 的结果列排序,凡是命中“成功”的条目就是值得进一步验证的候选。
这一步做完,你就能直观看到:这个黑名单到底漏了哪些变体。接下来右键命中结果,发到 Repeater逐条确认,最后看文件是否真的落盘、是否真的被解析执行。
3.3 全关卡参数化矩阵与项目文件的组织方式
既然 upload-labs 的关卡是同一批校验逻辑的变体,那完全可以做一个“参数化矩阵”:每一关准备一个请求模板,标注好需要替换的变量,再按校验类型准备对应的 payload 集合。
我整理配套项目文件时,目录是这样组织的:
upload-labs-auto/ ├── README.md ├── scripts/ │ └── gen_payload.py ├── payloads/ │ ├── blacklist_filename.txt │ ├── mime_types.txt │ └── exec_extensions.txt └── templates/ ├── pass01.txt ├── pass02.txt └── ...payloads/目录放字典。blacklist_filename.txt是文件名变形字典,覆盖大小写、空格、点、::$DATA、双写等;mime_types.txt是常见的 Content-Type 绕过值;exec_extensions.txt是历史上可被当作 PHP 执行的后缀列表。templates/目录放每一关的原始请求模板,去掉会话相关的动态值,需要替换的位置统一用<payload>标记。scripts/gen_payload.py是生成字典的脚本,方便你往后扩展自己的 payload。README.md里写清每一关的校验点、推荐测试思路和预期结果。
有了这套结构,你刷靶场的节奏会变成:打开某一关的模板,识别校验类型,选对应字典,导入 Intruder,跑完看结果,更新 README 里的结论。而不是每关都从零开始抓包、改包。
3.4 怎么判断一次自动化跑得成不成功
自动化跑完,大量结果堆在面前,判断标准要分层。第一层是响应文本,页面直接提示“上传成功”或“文件类型不允许”,用 Grep - Match 就能批量筛选。第二层是状态码和响应长度,同样是上传请求,返回 200 但长度明显不同的条目,多半是走进了不同的逻辑分支。第三层也是最终标准,文件是否真的落盘、是否能被访问解析。
很多关卡上传成功后会把文件名随机重命名,或者传到某个特定目录。这时候不要只看响应文本,直接用 Repeater 发一个 GET 请求去访问http://靶场地址/upload/文件名,看返回 200 还是 404。这个“落盘检测”步骤,比任何响应提示都可靠。
4. 几个典型关卡的自动化实战拆解
4.1 黑名单变体类:一次跑完大小写、空格、点、双写
黑名单关卡是 upload-labs 里数量最多的一类,从第 3 关到第 10 关基本都是这个套路,区别只在于补丁打到了哪一层。手工一关一关试,每关都要手动改文件名,纯体力活。用 Intruder 的思路是,把同一个“变形字典”应用到所有黑名单关卡上,一次跑完。
我写过一个简单的生成脚本,逻辑类似这样:
base = "shell" extensions = ["php", "php3", "php5", "phtml", "pht"] variants = [] for ext in extensions: for case in [ext.lower(), ext.upper(), ext.title()]: variants.append(f"{base}.{case}") variants += [ f"{base}.php ", f"{base}.php.", f"{base}.php::$DATA", f"{base}.pphphp", f"{base}.p.phphp", ] print("\n".join(variants))这套字典生成之后,在第 5~10 关的模板里分别跑一遍,你会很快发现规律:有的关卡漏了大小写,有的漏了空格,有的漏了双写。跑完后把这些结果填到自己的 README 里,你对“黑名单匹配到底是怎么实现的”会有非常直观的理解。
4.2 白名单加 %00 截断类:字节层面的精细操作
白名单关卡比黑名单麻烦,因为服务端只允许 jpg、png、gif 这类后缀。绕过依赖的是另一个漏洞:服务端在校验扩展名和保存文件名时,用的是两套逻辑,或者保存路径可控。经典的%00截断,就是让校验看到.jpg,但实际保存时在%00处截断,最终落盘成.php。
在 Burp 里操作这类请求,关键是要搞清楚编码问题。GET 请求里直接写shell.php%00.jpg通常可以,因为服务端会对 URL 进行一次解码,%00变成截断字节。但 POST 请求里,如果只是把%00当成普通字符串发过去,后端不一定按字节截断处理。这时候需要切到 Repeater 的 Hex 视图,找到对应位置,把 ASCII 的%00三个字符改成真正的字节0x00。
这类关卡我建议直接用 Repeater 一条条试,因为每次都要处理字节编码,Intruder 反而容易出错。批量跑的意义不大,毕竟核心不在“量”,而在“你有没有真的把字节送对”。
4.3 图片马与二次渲染:脚本和 Burp 配合的典型场景
遇到内容校验关卡,比如 getimagesize、文件头校验、二次渲染,Intruder 的价值会下降,因为问题从“文件名变体”变成了“文件内容构造”。上传一个带 PHP 代码的图片马,绕过文件头校验的思路是:文件头必须是合法的 GIF、PNG 或 JPEG 数据,代码追加在图片尾部。
二次渲染关卡更狠,服务端会把图片重新编码一遍,你尾部追加的代码大概率被丢得一干二净。自动化的思路变成:先上传一张正常图片,下载服务端渲染后的图片,用脚本逐字节对比原图和渲染图,找出渲染后仍然保留的数据区域,再把 PHP 代码嵌入到这些区域内,重新上传。
在这个场景里,Burp 更多是充当抓包和重放的角色,核心的自动化逻辑在 Python 脚本里。你动手之前,把“渲染前 vs 渲染后”的图片差异分析清楚,比跑一千条 payload 都有用。
4.4 条件竞争类:Turbo Intruder 并发抢时间窗
条件竞争关卡的设计很巧妙:服务端不是先校验再保存,而是先保存再校验,或者先保存再改名。这个顺序上的时间差,就是漏洞窗口。自动化思路很简单:一边疯狂上传恶意文件,一边疯狂访问已上传文件的路径,总有一次能抢在校验或改名之前让文件被执行。
我用 Turbo Intruder 时是开两个窗口:一个窗口放上传请求,一个窗口放访问请求,两个脚本同时启动,并发数从 50 开始调。上传脚本往队列里塞大量相同的上传请求,访问脚本用同样的频率去 GET/upload/shell.php。跑的时候观察响应,一旦访问请求出现 200 并且返回的是 PHP 执行结果,说明竞争成功。
这里必须强调:条件竞争测试只在本地靶场玩没问题,到任何真实目标上测试,都需要提前拿到书面授权,否则就是违规行为。靶场是给你练手用的,不是让你练胆用的。
5. 常见问题与排查实录
5.1 抓不到包:从代理到证书的排查顺序
常用的本地靶场是 http 协议,抓不到包时我一般按这个顺序排查:
- Burp 是不是在监听
127.0.0.1:8080?看 Proxy 选项卡的监听状态。 - 浏览器代理是不是真的指到了 Burp?有些浏览器用系统代理,有些用独立代理设置,还有代理插件开了分流规则,请求根本没走 Burp。
- 浏览器装了其他过滤流量的插件?先全部禁用试试。
- 用命令行直接验证代理通道,比如配置好代理后发送一个请求,看 Burp 里有没有记录。这一步能快速判断问题出在浏览器还是 Burp。
本地 http 靶场不需要证书,如果你目标站点是 https 才需要装 Burp 的 CA 证书。
5.2 Intruder 跑不动或者结果全是灰的
Intruder 跑完结果全是灰的(Grep - Match 没命中),先别急着加字典,检查两件事:
- 是不是 token 或会话失效了?上传接口如果依赖 Cookie,而你的请求模板是几百毫秒前抓的,可能已经过期。更新一下模板里的 Cookie 再跑。
- 是不是 Grep - Match 的关键词跟实际响应文案不匹配?不同版本的靶场提示文案可能不同,先用 Repeater 手动发一条,看看成功和失败时页面到底输出什么字,再去配置匹配词。
Community 版的 Intruder 有延迟限速,大字典跑起来特别慢。策略是缩小 payload 集合,先跑 20~50 条高概率样本,确认方向后再考虑扩大范围。
5.3 上传成功却访问 404
响应里明明提示“上传成功”,但访问文件路径就是 404。常见原因有三个:
- 文件名被服务端重命名了,你需要从响应体里找实际文件名,或者通过目录爆破猜文件名。
- 文件落盘目录不是默认的 upload,先去页面源码或者配置文件里找上传目录。
- 文件确实存在,但访问路径写错了,注意大小写和目录层级。
用 Repeater 直接对可能的路径发 GET 请求,比在浏览器里手动猜快得多。
5.4 跑完一长串结果,不知道哪些算有效
这是自动化最常见的问题。我的做法是三层筛选:
- 按 Grep - Match 结果排序,先看命中“成功”关键词的条目。
- 对比响应长度,和基线请求(正常 jpg 上传)长度差异明显的条目重点看。
- 对候选条目逐条用 Repeater 确认,然后直接 GET 访问落盘文件,验证是否真的可访问。
不要把响应提示当成唯一标准,尤其遇到二次处理逻辑时,页面说“上传成功”和“你真的拿到了一个可执行的 webshell”是完完全全两回事。
5.5 动态 token 让自动化白跑
如果关卡或者目标环境引入了动态 token,Intruder 每次发送的都是同一个旧 token,自然全部失败。解决方案是配置宏,让 Burp 在每次发送请求前先去目标页面获取一次最新 token,再动态替换到请求参数里。配置路径在 Project options -> Sessions -> Macros。几百条跑完后,对比一下 token 是否每轮都在变化,就能确认宏是否生效。
最后分享一点个人的体会
我把 upload-labs 完整刷过几遍之后最大的感受是:真正值钱的东西不是那些 payload 字典,而是“先判断校验类型、再选 payload 集合、最后用工具批量验证”的思路。Burp 在这里扮演的是放大镜和机械臂,它替你完成重复劳动,但判断方向还得靠你自己。
配套项目文件里的字典和模板,你可以直接拿去跑,但我建议你自己动手先过前面三关,感受一下请求和响应之间的关系,再让工具去跑剩下的关卡。等你能在三秒内说清楚某一关的过滤点到底是白名单、黑名单、内容校验还是条件竞争,你才算真正走出了文件上传这个坑。