DVWA文件上传漏洞实战:从Low到High三级防御绕过与加固
2026/9/16 19:41:18 网站建设 项目流程

1. 为什么文件上传在DVWA里值得单独跑一遍

1.1 这个模块被很多人低估了

我在带新人做Web安全入门的时候,发现一个很普遍的现象:大家搭好DVWA靶场,第一件事都是直奔SQL注入和XSS,因为那种“输入一段payload立刻看到反馈”的交互方式特别有成就感。文件上传模块反而经常被冷落,要么随便传一张图片就关掉页面,要么扫一眼源码觉得“就这几行代码有什么好看的”。

但实际上,文件上传是Web应用里攻击面最广、一旦出问题后果最直接的一个入口。它不是“多存了一个文件”那么简单,而是“多存了一个可能被服务器解析执行的文件”。DVWA把文件上传单独列成一个练习模块,并且用Low、Medium、High三个等级,把三种典型的防御思路完整地演了一遍——不设防、只校验一个点、多条件联合校验。把这三级跑通,你对后端文件上传表单设计的理解,会比看十篇漏洞文档都来得透彻。

1.2 三个等级的核心差异,先看这张表

在动手之前,我习惯先建立一个整体认知,这样后面每一步操作都知道自己在验证什么。

等级校验方式盲区在哪一句话总结
Low无任何校验,直接保存文件任意类型、任意内容都能上传裸奔状态,完全没有门
Medium只检查请求头中的Content-Type请求头是客户端可控的门锁装在门框上,没装在门上
High检查扩展名、文件大小、服务端MIME识别不检查文件内部二次解析风险门锁装好了,但窗户还开着

这张表里的“一句话总结”是我自己多年工作里反复验证出来的结论。尤其是Medium那句“门锁装在门框上”——它看上去有个拦截动作,但锁的钥匙却在攻击者手里,这就是典型的“伪校验”。

1.3 搭环境时踩过的三个坑,先替你们排掉

我建议直接用Docker方式搭建DVWA,这是目前最省心的路径:

docker pull vulnerables/web-dvwa docker run -d -p 8080:80 vulnerables/web-dvwa

浏览器打开http://localhost:8080,默认账号密码是admin/password。登录后先把右侧的Security Level切到Low,再进入File Upload菜单开始实验。

但这个流程里有三个坑,我几乎每次帮人排查都遇到:

第一个坑是数据库初始化失败。打开页面会停在Setup页面,提示连接数据库失败。原因是容器里的配置文件没生成,需要进容器把/var/www/html/config/config.inc.php.dist复制成config.inc.php,把数据库账密填进去,再重启容器。

第二个坑是上传目录权限不足。上传时报错“Your image was not uploaded”,但代码逻辑没问题,那就是hackable/uploads目录没有写权限,进容器执行chmod 777 /var/www/html/hackable/uploads就能解决。

第三个坑更隐蔽,我见过有人图方便,直接把DVWA跑在云服务器上,还映射到了公网端口。低等级下任何人都能往你的上传目录扔脚本,那就不叫学习靶场了,那叫给别人送肉鸡。DVWA只适合放在本机或内网隔离环境,这个边界必须守死。

工具方面,浏览器本身够用,但Medium级别的实验需要改请求头,最方便的做法是准备Burp Suite,用它的Proxy抓包改包。如果不想装,浏览器的开发者工具也能手动构造请求,但效率低很多,我后面会讲。

2. 低等级连校验都没有,真正的问题藏在哪

2.1 源码只看三行有效代码

把等级切成Low,点开View Source看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>Your image was not uploaded.</pre>'; } else { echo "<pre>{$target_path} succesfully uploaded!</pre>"; } } ?>

整个代码里没有任何过滤、没有类型判断、没有大小限制、没有重命名。所谓的“处理”,就是把HTTP上传的临时文件移动到hackable/uploads/目录下。move_uploaded_file唯一的职责是确认这个文件确实来自HTTP POST请求,至于内容是什么、扩展名是什么,它一概不管。

2.2 实测:一个PHP脚本被直接保存并执行

低等级的验证目标不是“拿shell”,而是搞清楚两件事:第一,服务器会不会接受并保存一个非图片文件;第二,保存后的文件能不能被当作PHP直接访问执行。

我先创建了一个用于验证的最小PHP文件:

<?php echo "file-upload-test"; ?>

文件名取为test.php,在DVWA页面上传,页面返回了一行路径提示../../hackable/uploads/test.php succesfully uploaded!。接着我新开标签页访问:

http://localhost:8080/hackable/uploads/test.php

浏览器直接输出了file-upload-test。说明Apache容器已经把uploads目录下的PHP文件当作PHP脚本交给解释器执行了。

很多入门者做到这一步就兴奋得不行,急着传一句话木马。我建议克制一下。低等级实验最有价值的产出,是总结出三条关键观察:服务端没有对文件内容做任何判断、没有修改文件名、没有限制目录的执行权限。这三条缺一不可,共同构成完整的漏洞链路。只要其中任何一条是安全的,危害都会大幅降低。

2.3 低等级在真实业务里的影子

有人可能会觉得,这都什么年代了,还会有后端代码对上传文件完全不做检查吗?还真有。我前几年做代码审计时,在一个内部系统的附件上传接口里就见过几乎一模一样的实现,唯一区别是换成了Java语言。当时的开发理由是“这个接口只供内部人员使用”,但内部系统被人拿到一个上传点,基本等于内网沦陷。历史漏洞库里大量“任意文件上传”事件,根因就是这么朴素——服务端无条件信任了用户提交的文件。

所以低等级存在的意义,不是教你一个攻击手法,而是让你亲眼看见:当程序对“外部输入”不设任何防线时,攻击成本可以低到一次普通点击那么低。

2.4 如果只修一个点,先修什么

低等级的加固非常明确:白名单后缀校验

只允许.jpg.jpeg.png.gif这类明确的白名单扩展名,其它一律拒绝。注意,这里说的是一律拒绝,而不是“把不认识的扩展名改掉”或者“记录警告”。黑名单永远有绕过空间,因为攻击者总能找到一个你没想到的扩展名;白名单才是真正可闭环的防御。

更进一步的加固是把上传目录的脚本执行权限彻底关掉,在Apache里配置php_admin_flag engine off,或者在Nginx里让该目录的PHP请求全部返回404。这样即使有人在文件内容里塞了恶意脚本,服务器也没有执行它的入口。

低等级教训就一句话:不要信任任何客户端送过来的内容,校验必须发生在本不该发生的时候。数据到达服务端的那一刻,就要假设它是恶意的。

3. 中等级用Content-Type做了一道门,可门却没有锁

3.1 看源码,它到底加了哪几个条件

把DVWA等级切成Medium,再回来看源码:

<?php if( isset( $_POST[ 'Upload' ] ) ) { $target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/"; $target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] ); $uploaded_name = $_FILES[ 'uploaded' ][ 'name' ]; $uploaded_type = $_FILES[ 'uploaded' ][ 'type' ]; $uploaded_size = $_FILES[ 'uploaded' ][ 'size' ]; if( ( $uploaded_type == "image/jpeg" ) && ( $uploaded_size < 100000 ) ) { if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) { echo '<pre>Your image was not uploaded.</pre>'; } else { echo "<pre>{$target_path} succesfully uploaded!</pre>"; } } else { echo '<pre>Your image was not uploaded.</pre>'; } } ?>

中等级比低等级多了两个条件:上传类型必须是image/jpeg,文件大小必须小于100000字节。但这里有一个致命的细节——它检查的$uploaded_type来自$_FILES['uploaded']['type'],而这个值是PHP从浏览器提交的HTTP头部里直接取出来的。

换句话说,服务端确实做了检查,但检查的数据来源是客户端说了算的请求头。这不是真正的服务端校验,而是“让客户端自己报告自己的身份”。

3.2 直接上传会被拦,但绕过只需要改动一个字段

我先按正常路径上传一份内容为<?php echo "medium"; ?>test.php,页面提示“Your image was not uploaded”,说明筛选条件确实生效了。

但绕过这一步非常简单。用Burp Suite开启代理,重新上传,抓到POST请求后找到这一行:

Content-Type: application/x-php

改成:

Content-Type: image/jpeg

放行。页面立刻提示上传成功,访问/hackable/uploads/test.php,PHP代码正常执行。

整个过程不需要改文件内容、不需要改扩展名、不需要混淆编码,只动了HTTP头部里的一个字符串。这就叫“钥匙就放在锁旁边”。

3.3 不带Burp怎么办,给一条备用路径

我在帮朋友远程排查时,经常遇到一端没有Burp环境的情况。其实浏览器开发者工具可以应急,方法不复杂:先把请求在Network面板里复制为cURL命令,然后用curl手动指定Content-Type重放。比如:

curl -X POST http://localhost:8080/dvwa/vulnerabilities/upload/ \ -b "PHPSESSID=你的会话ID; security=medium" \ -F "uploaded=@test.php;type=image/jpeg" \ -F "Upload=Upload"

注意-F参数里可以显式指定type=image/jpeg,curl构造multipart包时会把Content-Type按你指定的值提交。这种方式不用额外装任何工具。

但我仍然建议把Burp Suite装上。抓包改包不是只在这一关用,后面遇文件包含、命令注入、越权排查,几乎全靠它。趁早把工具链磨熟,比死记几个payload有价值得多。

3.4 中等级最值得反思的一点

中等级给我的感受是“增加了拦截面,但没有增加安全护栏”。它确实能拦住“老老实实用浏览器上传PHP文件”的普通用户,但对任何懂一点HTTP协议的人来说,这个校验形同虚设。

我后来在真实代码审计里,看到一个接口只检查Content-Type和文件大小,然后直接保存并通过内网请求访问,迅速意识到这里有同样的问题。但凡你在一段文件上传代码里看到$_FILES['xxx']['type'],就应该立刻提高警觉——这个值是直接取请求头的,完全可控。

3.5 中等级的正确修法

如果让我来修补中等这个状态,我会做三件事,按顺序执行:

  1. 后端用finfo_file读取临时文件的真实类型,不再信任任何客户端声明;
  2. 扩展名白名单只允许.jpg.jpeg,并且全部转成小写比较;
  3. 真实MIME与扩展名必须匹配,比如image/jpeg只能对应.jpg.jpeg

这三条同时满足,才算把“服务端校验”这四个字落到实处。否则哪怕多写几百行代码,只要最终依据的是请求头数据,都等于没有。

4. 高等级确实认真校验了,但拦截逻辑仍有边角

4.1 高等级源码到底加了哪些判断

切到High,源码逻辑明显丰富起来:

<?php if( isset( $_POST[ 'Upload' ] ) ) { $target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/"; $target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] ); $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" ) ) { if( $uploaded_size < 100000 ) { $finfo = finfo_open( FILEINFO_MIME_TYPE ); $type = finfo_file( $finfo, $uploaded_tmp ); finfo_close( $finfo ); if( $type == "image/jpeg" ) { if( move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) { ... } } } } } ?>

高等级的变化可以概括为四点:

  • 扩展名白名单,只接受jpgjpeg,并且用strtolower统一成小写,堵住.JPG之类的棱角;
  • 文件大小限制保持在100KB以下;
  • finfo_open(FILEINFO_MIME_TYPE)读取文件的真实MIME类型,不再依赖客户端请求头;
  • 真实MIME必须是image/jpeg

直接改扩展名、改请求头在高等级面前都行不通了,因为服务端会读文件二进制内容去判断到底是不是JPEG。

4.2 绕过高等级的一条完整路径

高等级依然有绕过的逻辑空间,因为image/jpeg只说明文件头部符合JPEG格式,并不等于文件每个字节都必须被图像解码器消费完。JPEG文件结构里允许存在附属数据段,很多图像解析器遇到不认识的段会跳过,但PHP解释器遇到<?php ... ?>却不一定会跳过。

这就引出经典的“图片马”手法。先在本地准备一张正常JPEG图片shell.jpg,再把PHP验证代码追加到图片末尾。Windows下的做法:

copy /b shell.jpg + payload.txt payload.jpg

Linux下更常见:

cat logo.jpg payload.php > payload.jpg

上传payload.jpg,扩展名是jpg,finfo_file识别出来的还是image/jpeg,服务端正常放行。

但这里有一个非常关键的技术细节,也是我最初反复踩坑的地方:直接访问/hackable/uploads/payload.jpg,浏览器只会把它当图片输出,并不会执行里面的PHP。因为Apache解析脚本的依据是扩展名,.jpg不是可执行脚本类型。真正的绕过链条,通常是“文件上传”配合“文件包含”——当站点里存在一个动态包含点,比如?page=xxx,攻击者把包含路径指向上传目录里的这个jpg,PHP解释器在include读取时,才会把文件内容当作PHP代码解析并执行。

也就是说,高等级把“上传”这一步的漏洞几乎堵平了,但“上传”和“执行”是两个不同的环节。如果服务器上还有其他入口能导致文件内容被二次解析,攻击链还是能被接上。

4.3 我实测高等级时的切身体会

我印象最深的一次,是自己在本地验证整条链路,上传完payload.jpg后直接访问uploads目录,看到的是乱码和图片残影,一点PHP输出的迹象都没有。当时第一反应是合并命令用错了,反复试了几次都一样。后来沉下心去查了Apache的PHP解析规则,才意识到问题出在自己对“执行条件”的假设错了。

这种“看似失败、实则需要换一个触发入口”的经历,比直接看到命令执行成功更有价值。它提醒我,漏洞利用从来不是单点事件,而是多条路径、多个组件叠加的结果。安全测试最忌讳的就是一条路走不通就慌,要习惯画链路图,把“上传”“存储”“访问”“解析”拆成独立节点逐个排查。

4.4 高等级在真实业务里是什么强度

平心而论,高等级的水平已经超过很多真实业务系统了。现实中大量应用至今仍停留在“只做扩展名校验”甚至“什么都不做”的状态,这一点做代码审计时屡见不鲜。但高等级距离真正的安全仍有距离,它没有做到下面两件事:

第一,它没有对上传内容进行重新编码。真正稳妥的图片上传处理,会调用图像处理库把图片重新解码、缩放、剥离元数据再重新保存。这个过程的本质,是让存储下来的字节流不再等于用户上传的原始字节流,用户夹带的恶意载荷在重新编码过程中基本被洗掉了。

第二,它没有改变存储与执行的关系。高等级保存后文件名不变、目录不换、执行权限不关闭,直接把文件放在Web可访问且可能被解析的目录里。如果把上传目录放到Web根目录之外,或者关掉该目录的脚本执行权限,整个攻击链的最后一环就被切断了。

所以高等级给我的启发不是“再想一个更刁钻的绕过姿势”,而是意识到:安全检测只有和数据处理、架构设计绑定在一起,才具备真正的防护强度。

5. 三关跑完之后,真正值得带走的一套加固清单

5.1 从源码层面复盘三套方案

跑完三个等级再回头看,最清晰的是把三套方案并排放在一起比较。

维度LowMediumHigh
扩展名校验白名单,只允许jpg/jpeg,大小写转换
MIME数据来源不校验来自客户端请求头服务端finfo读取真实内容
文件大小限制100KB100KB
上传后文件名不修改不修改不修改
典型绕过方式直接上传PHP抓包改Content-Type合并图片马,再配合文件包含触发
防御水平不存在形式大于实质有一定强度,仍有间接路径

这张表用来记“差异”就够了。但落到实际工作中,更重要的是建立一条认知:安全等级不是由校验条件的数量决定,而是由数据可信度决定。低等级是不管可信度,中等级是误把客户端声明当作可信数据,高等级是开始从文件本质读取特征,但还没有做到让数据彻底脱离用户控制。

5.2 一套可以直接落地的四层防御

如果你问我在实际项目里,文件上传接口到底怎么设计,我建议直接按这四层来,不必东拼西凑:

第一层:白名单与真实MIME双校验。文件的扩展名必须落在白名单内,同时服务端必须用finfo_file读取文件的真实MIME类型,两者匹配才允许通过。任何直接信任请求头的行为都应该被视为漏洞。

第二层:压缩重建与元数据剥离。图片上传后,立刻用ImageMagick、GD等图形库重新解码、缩放到目标尺寸,输出一份新图片。这一步能把藏在文件尾部或元数据区里的载荷清掉大部分,而且不影响正常业务。非图片文件更简单,直接走独立通道处理。

第三层:目录隔离与随机重命名。上传文件落到Web根目录之外或者独立域名,目录关闭脚本执行权限。文件名用UUID或随机字符串重命名,让攻击者无法预料文件存储路径,也堵死“按名字猜地址”的路径探测。

第四层:存储环境治理。上传文件建议走对象存储或CDN,不与企业业务域共享Cookie、Session。在一些重要场景,比如允许上传文档和压缩包的接口,接入反病毒扫描或沙箱检测,在写入前把WebShell、宏病毒这类恶意脚本过滤掉。

这四层每一层单独看都有效,但真正的纵深防御要求它们组合使用。你在网上看各大厂的文件上传方案,翻来覆去本质上就这三板斧:要么不让它进Web可执行目录,要么进去了也重新编码一遍,要么执行路径直接断开。

5.3 我的一点额外提醒

文件上传这个知识点,很多人学完DVWA就觉得自己会了。但真实业务远比武靶场复杂:多文件上传、分片上传、云存储直传、签名URL过期、图片渐进加载……每一处新花样都会带来新的风险点。DVWA的价值,是帮你把最核心的攻击与防御原理刻进脑子里,而不是给你一套可以照搬所有场景的万能公式。

最后分享一个我自己工作里的习惯:凡是看到后端出现文件上传接口,我不会先去判断它“能不能绕过”,而是先问三个问题——用户上传的字节流是否会被原样保存?存储位置是否能被Web容器解析执行?文件内容是否可能在另一个入口被二次读取解析?这三个问题只要有一个回答“是”,这个接口就该列入重点审查名单。带着这个框架去重看DVWA的三个等级,你会有完全不一样的收获。

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

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

立即咨询