很多刚入门的兄弟问我:上传漏洞到底怎么练?我的回答一直是同一句——先把 upload-labs 刷明白,再谈别的。这不是推销,而是我在带人和做项目里总结出的结论:文件上传功能几乎在所有业务系统里都会出现,可它对应的防御点又多又杂,普通漏洞平台很难把这些场景系统性地组织起来。
upload-labs 是一个开源的本地靶场项目,由 c0ny1 维护,设计上是把文件上传漏洞的常见形态拆成 20 关。每一关就是一个独立的上传页面,你可以理解为把“一个不安全的文件上传功能”在代码层面做了 20 种不同的糟糕实现。你作为测试者要做的是:想办法让服务器认为你上传的是一个无害文件,但实际执行逻辑却会把它当作脚本处理。整个过程都在本地环境里完成,不需要触碰任何第三方目标,适合学习、复现和实验。
我推荐它的另一个原因是,它不靠背答案就能刷完,但也很难无脑通到底。前几关基本是送分题,中间开始出现扩展名黑名单的各种变形,后几关涉及文件内容头、二次渲染、条件竞争,再往后还有中间件解析差异。你每过一关,其实是在强迫自己回答同一个问题:当前的校验逻辑到底信任了什么、忽略了什么。把这个问题想清楚,比记住一条具体 payload 重要得多。
这篇文章不打算给你贴一份“每关标准答案”式的通关指南,我更想把刷这本靶场时的判断路径和踩坑记录整理出来。你会看到哪些思路值得沉淀成习惯,哪些坑是环境版本造成的,以及最后这些东西在实际工作里如何落地。
1. 先看本质:upload-labs 到底在训练什么能力
1.1 它不是工具包,而是一套情景化靶场
很多第一次接触 upload-labs 的人,以为它是一个字典库或者利用工具,其实不是。它就是一个网站项目,你把它部署在本地 Web 环境里,然后通过浏览器访问每一关的上传页面,手工上传文件并观察结果。它真正训练的是你对“文件上传功能内部处理链”的敏感度。
一个经典的上传处理流程大概是这样的:客户端先选择文件,浏览器把它以 multipart/form-data 的格式发到服务端;服务端通常会做几件事——确定文件保存位置和文件名,用不同的方式校验文件内容或扩展名,把文件移动到目标目录;必要时还会生成缩略图、重命名、甚至二次渲染。漏洞往往藏在这些步骤之间的缝隙里。upload-labs 的每一关,都相当于把其中一步故意做错,让你去发现那个缝隙。
举个很通俗的例子:你把一个快递放进小区快递柜,系统以为你放的是书,因为它只看你填写的“物品类型”一栏。但柜子里到底装了什么,它根本没拆开看。等有人按“书籍”分类去取件,结果取出来一个会唱歌的闹钟,你才知道类型标注根本不可靠。上传漏洞大致就是这个模型——服务端对文件“身份”的判断方式,和它最终使用文件的方式没有对齐。
1.2 部署环境版本与常见坑
最常见的是 20 关版本,依赖 PHP 环境。我在自己机器上用 Windows + phpStudy 的组合,把项目目录放到 WWW 根目录就能访问。过程不复杂,但下面几个坑如果不注意,刷起来会非常困惑。
- PHP 版本选择:部分关卡设计时针对的是老版本 PHP 的解析特性,比如低版本对特殊文件名、对
.htaccess的支持程度。你用太新的 PHP 版本,可能复现不出预期效果。建议准备一个 5.x 或 7.x 的 PHP 环境,而不是直接上最新版。 - 上传目录路径一致性:每一关页面里会显示目标路径,比如
../upload。这个路径决定访问文件的 URL,如果改过项目配置,记得同步调整路径,否则会出现“上传成功但访问 404”的情况。 - 服务重启与缓存:改完代码后如果不重启 Web 服务,部分关卡现象会不一致,尤其是涉及配置文件的那几关。我一般在每关开始前先确认上传目录干净、服务状态正常,能省掉很多“明明按思路来却不生效”的烦恼。
这些看似是环境问题,其实也是工作习惯问题。在真实项目里做上传测试时,同样要先确认中间件版本、路径映射和临时目录行为,否则排查起来会绕远路。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 前几关的送分题里,藏着最容易被忽略的信任边界问题
2.1 前端 JS 校验:只能拦普通人,拦不住测试者
第一关非常直白,直接在浏览器前端用 JavaScript 拦截非图片文件,选择非图片时甚至传不到服务端。你以为这算防护?把脚本事件禁掉,或者直接抓包改请求报文,文件照样能传上去。
这个关卡暴露的问题很基础也很容易被忽视:前端任何校验都只是用户体验的一部分,服务端必须假设请求体完全不可信。听起来像废话,但不少人做测试时对前端规则有一种莫名的敬畏,看到提示“只允许上传图片”就真的只传图片。绕过前端校验并不是高深技巧,在浏览器里把文件后缀改一下,或者用工具直接改请求,服务端照样把文件收下。第一关的意义不是教你怎么绕过 JS,而是让你建立一个意识:你看到的规则只是第一层皮,真正做决策的在服务端。
2.2 MIME 类型检测:客户端自报家门的不可靠性
第二关和第三关开始看 MIME 类型,也就是 HTTP 报文里的 Content-Type 字段。很多人觉得这总该比前端靠谱一点,可 MIME 本身就是客户端自报家门的字段。你上传一个 PHP 文件,顺手把报文里的 Content-Type 改成 image/jpeg,如果服务端只校验这个字段,文件照样收下。
这个位置的关键不是“怎么改字段”,而是你要意识到:靠请求头自报类型来做安全决策,等于把门锁交给客人自己拿钥匙。我在讲课时喜欢让学员先抓包看看,很多人在这一步第一次意识到,原来浏览器在表单上传时发出的 Content-Type 是可以随意修改的。这个认知一旦建立,后面很多关卡的思路就顺了。
2.3 这几关的共同底层逻辑:信任边界放错了位置
连续刷完前三关,你会发现它们有一个共同特征:服务端把本该自己做主的安全判断,全部交给了客户端或请求本身。前端的 JS 校验、扩展名后缀、MIME 类型,都属于“外部输入”,而安全决策必须依赖服务端能够独立验证的信息。
早期关卡的意义不在难度,而在帮你建立一个判断模型:看到一处校验时,先问它是基于什么做的,这个依据是否可以被请求方篡改。把这个模型带在身上,后面遇到任何过滤逻辑,你都会本能地去观察它到底挡在哪一层。
用生活化的类比就是:你进小区,保安问你“你是不是业主”,你说是他就放行。等你意识到业主身份要查证件,已经晚了。前端和 MIME 就相当于“自报业主”,服务端连查证件的概念都没有。这类问题在真实系统里其实很常见,尤其是快速迭代的运营后台,很多上传接口只做了表面限制。
3. 中段关卡的黑名单绕过:规则永远存在例外
3.1 扩展名黑名单的绕过面
从第四关开始,靶场进入扩展名黑名单阶段。服务端终于做了“服务端校验”,但用的是黑名单思路——把常见危险后缀,比如 .php、.asp、.jsp 等列出来,命中就拒绝。黑名单天生有两个问题:一是覆盖面有限,二是规则匹配方式容易出偏差。
常见的玩法包括:大小写变形 .pHp,在 Windows 和部分中间件上会被当作 PHP 执行;后缀前加空格 .php ,某些系统保存后看不出问题,但访问时却因为解析特性触发脚本执行;还有加点、双写后缀 .pphphp,以及利用 .php. 这类让系统在重命名或解析时产生歧义的做法。
这些玩法单看都不复杂,但它们背后的原理是一致的:黑名单在匹配字符串时,和你最终希望服务端“怎么解释这个文件”之间,存在解释差异。你骗过的是匹配逻辑,真正想利用的是文件系统或者中间件对文件名的处理方式。本质上,这叫“校验逻辑与执行逻辑不一致”。
3.2 大小写、空格、点号与解析差异的关系
举几个常见的系统差异:Linux 下文件名大小写敏感,Windows 默认不敏感,同样的 shell.php 和 shell.Php,在两端可能带来完全不同的行为。空格、点号这类特殊字符,在某些环境下会让系统在保存时自动去掉尾部的点或空格,于是你传上去的是 shell.php.,落盘变成 shell.php。这些行为不是代码作者故意留后门,而是开发时没有考虑系统差异。
对测试者来说,刷到这类关卡最大的收获是建立“环境感知”:同一个文件名,在 Linux 和 Windows、在 Nginx 和 Apache 下,结果可能完全不一样。判断现象时必须带上运行环境,不能孤立地看文件名。我在真实项目中就遇到过一次:开发在 Windows 上测试时说“后缀已经被过滤了”,但部署到 Linux 后,大小写写法和路径分隔符行为都不同,测试结论就得重新评估。
3.3 关于 .htaccess 的玩法与防护启示
中段关卡里还有一类思路属于“利用配置型文件”。在 Apache 环境下,.htaccess 会被当作配置文件加载,如果服务端没有拦截这个后缀,你可以上传一个自定义的 .htaccess,让后续满足特定条件的文件被当成 PHP 解析。
这个点本质上是把一个“服务器配置途径”伪装成普通文件传上去,属于扩展名黑名单没覆盖到的系统文件类型。把 .htaccess 加进黑名单成本很低,但很多项目确实没考虑到。
我要多说一句:练习时理解这个思路没问题,但在真实系统里,成功率不会像靶场里这么高。现代中间件对 .htaccess 的启用程度、目录权限、重写规则复杂度都会影响结果。刷完靶场不要误以为这是银弹,它只是帮你理解一件事:文件上传不只是传内容,还有可能传“策略”或“配置”。
4. 文件内容校验与逻辑漏洞:后半程关卡的真正分水岭
4.1 文件头校验和“图片马”的对抗本质
往后的关卡开始检查文件内容,最常见的是检查文件头几个字节,比如 JPEG 以 FF D8 FF 开头,PNG 有固定头部。你以为在 PHP 文件前面拼上图片头就能过?部分关卡确实可以,这就是所谓的“图片马”。
但更值得琢磨的是:检查文件头到底能证明什么?它只能证明文件“看起来”是图片,并不能证明文件的整体内容都是图片。服务端保存后,后续解释权又交给了中间件,只要你把可执行代码放在图片数据之后,并且中间件根据路径或 Content-Type 把它当作脚本处理,依然可能被解析。
从这里你可以看到,绕过手段再怎么升级,核心仍然是“多个校验环节之间的缝隙”。文件头校验挡不住内容拼接,内容拼接又挡不住二次渲染,所以就有了下一类更难缠的关卡。
4.2 二次渲染与条件竞争:两种必须建立的心智模型
二次渲染的思路是:不仅校验内容,还会把上传的图片重新生成一遍,用程序重新画一张图,原文件里夹带的代码就没了。此时要过关,需要在不破坏图片结构的前提下,把代码嵌入到渲染后仍然保留的区块中,比如 GIF 的注释区或图片的某些数据段。这个过程非常考验你对图片格式结构的理解,而不是简单“拼个头部”。
它的价值在于让你明白:真正的白名单校验是对内容做完整解析,而不是采样判断。只检查文件头,本质上还是一种偷懒的近似判断。我见过不少真实系统号称“做了内容校验”,实际上代码里只是一个 getimagesize(),这类校验的对抗空间是很大的。
条件竞争则是另一种思维:处理流程里存在时间窗。比如系统先把文件上传到临时目录,再检查后缀,检查失败后删除文件。但删除操作和你访问文件之间有一个极小的时间差。在这个窗口内反复请求,让服务端还没来得及删除,脚本就已经被当作可执行文件访问到了。
这类关卡训练的是你对“异步流程中状态不一致”的敏感度。真实业务里,上传处理往往还会接队列、异步任务、第三方存储,这类窗口会更隐蔽。我在项目里遇到过上传后再异步杀毒的场景,当时就想到条件竞争这个模型——校验动作和数据落盘动作分离的地方,时间窗就可能存在。
4.3 中间件解析漏洞与后缀解析顺序的错位
再往后,靶场会把中间件解析漏洞拿出来单独练。Apache 的特性是遇到不认识的后缀时会继续往前找,比如文件名是 shell.php.xyz,它先看 .xyz 不认识,就往前看 .php,于是按 PHP 解析。Nginx 在某些配置下会对路径里的文件名做解析,把类似 a.jpg/x.php 的请求交给 PHP 处理,从而绕过扩展名限制。
这些漏洞本质上是“服务端对文件名的解释顺序”和“校验逻辑对文件名的理解顺序”不一致。你校验的是最后一个点的后缀,它解析的是中间某个点后面的后缀,一步错位,防御就失效。
刷到这类关卡时,我建议你多给自己加一个步骤:不看答案,先列出中间件和版本,再判断它在这类文件名上会怎么解析。这种练习多了以后,你看到任何上传功能都会下意识地问一句:最终执行或解释这个文件的到底是哪一层?它按什么规则找文件类型?
5. 刷完靶场的意外收获:从攻击视角走向安全设计
5.1 测试思维的变化:从“想绕过”变成“想防御”
很多人以为刷完 upload-labs 收获的是一堆绕过手段,其实真正沉淀下来的是测试思维。你现在看到一个文件上传功能,天然会从“信任边界在哪”开始拆解:前端约束能不能改、扩展名校验是白名单还是黑名单、文件名会不会经过系统自动处理、文件内容有没有二次解析、保存路径和后缀会不会被重新解释。
这个过程本身也是业务理解的过程,因为每个决定都对应着产品设计者当时的一个假设。比如有的系统只给用户传证件照,设计者默认不会有人传脚本;有的系统要传合同 PDF,开发只校验 PDF 扩展名。一旦你理解了这个假设,就能判断它的薄弱环节在哪里。
5.2 真正有效的防御组合拳
反过来,从攻击视角走了一遍之后,你再去做防御会清楚很多。最稳的办法其实谈不上高明,无非是几条:
- 白名单限制扩展名,而不是黑名单;
- 文件名由服务端重命名,使用随机字符串加白名单后缀;
- 文件内容用完整解析类库校验,而不是只读文件头;
- 上传目录禁止脚本执行;
- 对危险文件的管理不只考虑删除,还要考虑缓存、缩略图、CDN 回源链路会不会残留。
把这几条列出来,你会发现大部分真实系统的上传漏洞,问题不是“少了一个黑名单”,而是缺少一到多条根本性控制。我这里也给一段最基础的服务端校验思路,供参考:
php复制// 白名单后缀检查(伪代码)
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
$allowList = ['jpg', 'png', 'gif', 'webp'];
if (!in_array($ext, $allowList)) {
exit('非法文件类型');
}
// 重命名成随机文件名,保留白名单后缀
$newName = md5(uniqid()) . '.' . $ext;
// 移动文件前,再用 getimagesize 或专用解析库验证内容
$info = @getimagesize($tmpFile);
if ($info === false) {
exit('文件内容非法');
}
这段代码不是万能方案,但它体现了三层思路:扩展名白名单、文件名重命名、内容二次验证。真实项目里可以在这个骨架上继续加:独立存储域、禁止执行权限、对象存储签名访问等。你能把测试中积累的绕过视角翻译成这些防护措施,才算真正消化了靶场。
5.3 可复验性与报告习惯
还有一个被我长期忽略的收获是“可复验性”。靶场每一关只要失败,你马上能看到结果差异:上传成功、上传失败、访问报错、白屏。但很多初学者一旦看到某个返回结果,就急着下结论“过了”,根本没去验证文件是否真的被当作脚本执行。
我现在的习惯是:不管测试环境还是正式项目,每一步都要有明确的“验证行为”,而不只是看响应码 200。在靶场里,上传完一个脚本文件,至少还要访问一下,确认它是否真的被解析。这个习惯带到渗透测试里,能省掉大量误判。类似地,在编写测试报告时,我也会把“验证过程”和“复现条件”写清楚,而不是只写结果。
6. 刷完以后怎么继续进阶
6.1 授权边界和定位问题
最后必须认真说一点。upload-labs 作为本地靶场的价值,在于授权环境下的实验,它是帮你理解攻击与防护边界、积累经验用的。任何把这里学到的思路直接用到未授权目标上的行为,都不符合安全从业者的职业操守,也违背了我写这篇文章的本意。
真实环境差异很大:业务逻辑、云存储、对象存储、容器化部署,都可能导致绕过条件不复存在。靶场是训练“理解和判断”的地方,不是让你带着一套 payload 去横扫互联网的武器库。我见过一些人刷完靶场后自信心爆棚,去测别人的站,最后不仅什么都没测出来,还因为未授权访问吃了法律风险。这个提醒不是客套话,是真实发生过的事。
6.2 后续可以延伸的方向
刷完 upload-labs 之后,如果你想继续深入,我建议往这几个方向走:
一是把每一个绕过点对应的“防御手段”写成代码,亲自架一个环境验证它的有效性。只有自己搭过白名单、重命名、内容解析、目录权限这套组合拳,才知道哪些点是真正难被绕过的。
二是研究现代存储方案下上传链路的新变化。现在的业务很少直接把文件落地在 Web 目录里,很多走对象存储、CDN、临时凭证上传。令牌失效逻辑、签名 URL、回源鉴权都可能成为新的对抗点,这些是 upload-labs 没覆盖到,但在工作中一定会遇到的。
三是系统学一次主流中间件的解析规则,而不是背漏洞名。Apache 对未知后缀的向前解析、Nginx 的路径别名配置、IIS 的分号截断,这些规则搞清楚后,你便能在新环境里自己推理出问题,而不是依赖旧 payload 列表。
按我自己的经验,这二十关并不规定你必须在多少时间内通关,重要的是你在每一关面前都真的停下来想一想“它为什么能成立”。如果你刷完以后,看到一个上传框的时候,脑子里出现的不是“我该用什么工具”而是“这一关服务器在信任什么”,那你这个过程就没有白费。
