DVWA靶场里的文件上传模块,我前前后后大概打了三遍,从第一次盲目上传被各种拦截,到后来能根据服务端代码逻辑推算绕过方式,这个过程中的收获比单纯看教程多得多。文件上传这个漏洞在所有Web漏洞里属于特别“直白”的一种——你上传进去的是一个文件,得到的可能是服务器的执行权限。DVWA把同一漏洞拆成Low、Medium、High三个等级,恰好覆盖了从“完全无防护”到“多层校验”的典型演变路径,特别适合用来理解Web应用信任边界的本质。
这篇内容就是把我在DVWA靶场上反复操练文件上传模块的完整过程整理出来,从环境搭建、漏洞原理、三个等级的具体绕过手法,到那些没人提前告诉你、但几乎人人都会踩的坑。适合刚学Web安全、已经会一点Burp Suite基础操作,想找一个安全可控的靶场来动手练一练的读者;如果你正在准备安全测试相关的面试,把这一套链路吃透也很有用。
1. 先弄明白:文件上传漏洞到底在测什么
1.1 一个被低估的高危入口
文件上传本身是Web应用再常见不过的功能,头像上传、附件上传、文件管理器,到处都是。但也正因为常见,很多开发者只想着“功能能用就行”,忽略了上传接口本质上是在向服务器开放一条接收任意文件的通道。
一旦这条通道没有被严格审查,攻击者上传的就不是普通图片、文档,而是一段可以被服务器解析执行的脚本——这就是常说的WebShell。攻击链通常是这样的:上传一个恶意脚本文件,通过在浏览器中访问该文件触发脚本执行,最终获得服务器上的命令执行权限。接下来的事情就脱离Web层面了,内网探测、横向移动、数据提取,都是有可能的。
我经常把文件上传漏洞比作“往快递柜里塞包裹”。快递柜本身是方便取件的,但如果你不检查包裹里装的是什么,什么都往里塞,迟早会收到一个不该存在的东西。服务器也一样,上传功能是一个入口,校验不严就是门户大开。
1.2 DVWA的三个等级为什么值得逐个打
DVWA(Damn Vulnerable Web Application)是安全学习圈里非常经典的本地靶场,整个平台故意内置了大量Web漏洞,专门用来做授权范围内的安全测试练习。文件上传模块把同一种漏洞做成三个等级,不只是难度递进,它模拟的其实是真实项目中三层典型的防御水位:
- Low等级:服务端完全不设防,等于只做了前端展示,文件直接落入Web目录。
- Medium等级:服务端开始检查文件类型,但只检查了MIME类型(Content-Type),而MIME是客户端提交的请求头里带的字段,可信度为零。
- High等级:服务端对文件内容做真实校验,用getimagesize()验证文件头是否为有效图片,并把文件重命名后强制改为jpeg后缀。
把三个等级打完,你会很清楚看到一种演化逻辑:从“完全信任客户端”,到“半信半疑地检查客户端声明”,再到“基于文件内容做服务端验证”。这个演化过程恰恰是现实中很多系统文件上传功能从脆弱到加固的缩影,理解了它,再看真实业务系统的上传接口,一眼就能判断大概能怎么测、需要怎么防。
1.3 本地环境搭建与工具准备
DVWA的搭建不算复杂,我试过直接在Windows下用phpStudy包环境,也在Linux上用Docker跑过。如果你不想折腾环境,Docker是目前最省心的方式。一条命令就能把DVWA拉起来:
docker run -d -p 8080:80 vulnerables/web-dvwa跑起来之后,浏览器访问http://localhost:8080,默认账号密码是admin / password,登录后先把页面下方的“Create / Reset Database”点一下,初始化数据库就能开始通关了。
需要准备的工具不多,但有几个必须用顺手的:
- Burp Suite:用来抓包、修改请求内容,Medium等级的MIME绕过靠它完成。
- 浏览器开发者工具(F12):查看上传后的文件路径、网络请求和响应信息,很方便。
- 蚁剑(AntSword):WebShell连接工具,上传一句话木马后用它来建立连接并操作文件。同类工具里,我用蚁剑比较多,跨平台且社区活跃。
- 一个顺手的代码编辑器:写一句话木马或者做图片马拼接时用。
这里必须强调一句:DVWA是本地靶场环境,所有测试操作都必须在你自己搭建的、有授权的环境里做。没有授权就对着别人的系统测试,那不是学习,是越界。安全技能的底线就是“先授权,后测试”。
2. 三个等级的防御逻辑与绕过原理拆解
2.1 Low等级:完全信任客户端,处处是漏洞
Low等级对应的服务端代码,核心逻辑就是把上传的文件直接保存到目标目录,几乎没有做任何校验。DVWA里这一关的代码逻辑,说白了就是“上传到哪个路径、保存成什么名字”这两件事。
攻击者在这个场景下做的事情很简单:准备一个PHP一句话木马,选文件、点上传,得到一个可访问的Web路径。因为没有任何过滤,上传的PHP文件就能直接被Web容器解析执行。
这一等级揭示的核心问题是:如果服务端对上传文件完全不做任何校验,那整个接口就是个“文件投递口”,攻击者可以用它把任意内容写进服务器磁盘,而且还能猜得到写入路径。这是最原始也最致命的文件上传漏洞形态,严格说连“漏洞利用技巧”都不需要,纯粹是防守方的裸奔。
从学习角度来看,Low等级不是一个“难度等级”,它更像是漏洞的参考基线,用来对比后面两个等级到底修了什么。
2.2 Medium等级:只查MIME的“纸糊防线”
Medium等级加入了服务端校验,但校验逻辑很经典地犯了新手错误。它的代码逻辑大概是这样的:检查上传文件的MIME类型是否等于image/jpeg或image/png,同时检查文件大小是否小于100KB,两个条件都通过才允许上传。
关键在于:服务端拿到的uploaded_type字段值,来自HTTP请求头里的Content-Type,而这个字段是由浏览器或客户端工具自由填写的。服务端信任了客户端主动声明的内容,就像快递柜管理员只看快递单上写的“手机”两个字就放行,完全不拆箱检查里面到底是什么。
所以Medium等级的绕过方式非常直接:先用Burp Suite开启代理,上传一个PHP文件,在请求发送到服务器之前把Content-Type从application/x-php改成image/jpeg,服务器一看MIME类型是图片,就放行了。文件名后缀、文件内容,它都没有检查。
这一类校验方式在真实系统里并不少见,尤其是一些早期开发或者外包项目中,开发者可能听说过“要检查上传文件类型”,但没理解MIME校验是不可信的,就成了纸糊防线。想到这里你就能明白,做安全测试时遇到MIME类型校验,基本等于告诉你有绕过空间。
2.3 High等级:校验文件头后的“白名单+重命名”策略
到了High等级,开发者明显学聪明了,它做了三层关键操作:
- 用
getimagesize()读取上传文件的信息,只有文件头是真实图片结构才通过校验,这能过滤掉绝大多数简单的伪造文件。 - 用
md5(uniqid())生成随机文件名,而不是沿用用户上传的原始文件名。 - 强制把文件后缀改为
.jpg,格式写死。
这个方案在“防直接上传可执行脚本”这个目标上很有效。你就算传一个内容为PHP代码的文件,只要文件头不是合法图片,第一步就被拦了;就算你伪造了文件头,服务器也会把它重命名成随机名并强制保存为.jpg,你无法预测文件名,也很难让服务器把jpg当作PHP来解析。
但防线真的完美吗?并不。High等级真正的考点,是“当单个漏洞不好打穿的时候,就去找组合链路”。既然服务器会让你上传一张“看起来”完全正常的图片,那么就可以做一张图片马——文件头是真实的JPEG图片结构,但在图片数据尾部拼接一段PHP代码。getimagesize()只校验文件头,它会认为这张图片合法,允许上传。
文件上传后得到一个xxx.jpg文件,直接访问这个jpg,Web容器只会把它当普通图片输出,不会执行里面的PHP代码。这个时候就需要第二个漏洞来配合,比如文件包含漏洞(File Inclusion),通过文件包含接口把这张图片“当作PHP文件解析”,图片尾部的代码就会执行。DVWA本身还有文件包含模块,两个模块组合起来,正好演示了真实攻击中的漏洞链思维。
3. 一台电脑打穿三个等级:完整实操记录
3.1 先写好你的“实验载体”
射击之前先造子弹。要实验文件上传漏洞,你得有一个“上传后能被执行”的样本。最常见的样本就是PHP一句话木马,核心功能是接收POST参数并把它当作PHP代码执行。代码很简单:
<?php @eval($_POST['x']); ?>这是一个经典版本,但有些新版本PHP环境里eval是语言结构,不能通过可变函数调用,导致这个写法会有兼容问题。更稳的写法可以是:
<?php @assert($_POST['x']); ?>assert在较老版本的PHP里是一个函数,可以被这种写法调用;在PHP 7.2之后assert的行为有调整,也需要根据具体版本尝试。如果你测试的靶场PHP版本比较奇怪,一步到位的方法是用phpinfo()先验证文件能不能执行:
<?php phpinfo(); ?>这个文件不需要连接工具,只要在浏览器里访问它,能看到PHP信息页就说明“上传后可以执行”这一环已经打通。
我这里假设你在DVWA三个等级上都用一句话木马来验证,不同等级会调整上传的“包装形式”。工具方面我用蚁剑连接,连接时URL填上传后的文件地址,连接密码填x,对应$_POST['x']。
3.2 Low等级:上传即执行
Low等级的实操过程没有什么玄学,就是裸传。
在DVWA页面上传一个名为shell.php的文件,内容用完整PHP标签写一句话木马。上传成功后页面上会显示文件保存路径,通常是../../hackable/uploads/shell.php这样的相对路径。在浏览器里拼出完整路径访问一次,如果页面没有异常报错,再用蚁剑连接这个URL,密码填x,成功的话就能看到一个文件管理界面。
这里有一个细节值得注意:DVWA上传后的文件保存路径在实战中属于“上传接口返回信息”,很多真实系统上传成功后会返回文件URL,这个URL本身就是信息泄露。即使服务端过滤了内容,文件路径也是攻击者拼凑利用链条时的重要拼图。
Low等级实验的关键收获不是“怎么上传”,而是让你直观感受到“无校验上传成功是什么状态”,以及理解WebShell连接工具的工作方式:蚁剑本质上就是一个客户端,通过HTTP POST提交代码参数,服务器端PHP执行这段代码,再把结果返回。
3.3 Medium等级:Burp抓包一秒绕过
Medium等级开始出现拦截。我第一次用shell.php直接上传,页面提示文件类型不允许,这一步很正常。接下来就是改包。
先把Burp Suite的代理打开,浏览器走代理访问DVWA,上传shell.php时把请求拦下来。抓到的POST请求里有一个关键字段:
Content-Type: application/x-php把它改成:
Content-Type: image/jpeg然后放行。服务器查看这个字段时发现是image/jpeg,再检查文件大小小于100KB,就会认为是一张合法的JPEG图片,允许上传。但实际保存到磁盘上的文件内容依然是PHP代码,文件名也没有被改动,所以最终上传成功的还是shell.php。
访问这个文件路径、用蚁剑连接,整个流程和Low等级没有区别。区别只在于中间多了一道“改包”动作。
我见过有人在Medium等级卡很久,原因是把Content-Type改错了地方,或者改了Content-Length导致请求被拒。实际上Burp抓包之后,直接改Content-Type那一行就够了,其他字段不用动。
为了让这个过程更接近真实场景,你也可以先正常上传一张真实图片,抓包后把文件名改成shell.php,同时把Content-Type保持为image/jpeg。这种情况下,即便服务端加了“检查文件名后缀为图片后缀”的校验,也可能被你的双保险骗过去。动手多试几组组合,远比只看教程记得牢。
3.4 High等级:图片马与攻击链思维
High等级的实操不再是简单的“上传一个PHP文件”,而是要上传一个能够骗过getimagesize()和重命名机制的文件。这里我使用的方案是制作一张图片马。
图片马的制作有多种方式,我用的方法比较简单:找一张小于100KB的JPEG小图(DVWA High等级会检测文件大小),在Linux里用命令把一段PHP代码追加到图片尾部:
cp small.jpg shell.jpg echo '<?php @assert($_POST["x"]); ?>' >> shell.jpgWindows下也可以用16进制编辑器打开图片,把PHP代码粘贴到文件末尾保存。这种方式不会破坏图片前部的JPEG文件头信息,getimagesize()依然能识别为有效图片。
上传这张shell.jpg,DVWA的High等级会接受它,并返回一个像xxxxx.jpg这样的随机文件名。此时访问这个文件,浏览器大概率会输出图片或直接下载,不会执行PHP代码,因为后缀是jpg,Web容器按静态图片处理了。
到这里,相信你已经体会到了High等级的意义:它逼着你从“单纯打上传接口”升级到“串联漏洞”。接下来的思路通常有两条:
- 配合DVWA里的文件包含模块,把这张图片当作PHP文件包含进去,图片尾部代码就会被解析执行。这是最经典的组合打法。
- 如果环境是Apache且开启了某些解析特性,可以考虑上传包含特定内容的文件配合解析漏洞,但我在DVWA默认环境下还是推荐走“文件包含”这条链路。
另外一个思路是利用.htaccess或.user.ini配置文件。如果上传接口没有限制这两个文件名的上传,攻击者可以先上传一个.htaccess把同目录下的jpg文件强制解析为PHP,再上传图片马。DVWA默认实验里可能不涉及这个玩法,但你在真实测试中遇到High等级类似场景时,这是一个必须想到的思路。
High等级最终的收获是:不要试图用一把钥匙开所有锁,防御升级了,攻击路径也要跟着升级。单点漏洞难以突破时,花时间寻找相邻漏洞,把它们串起来,往往能打开局面。
3.5 通关后的三个等级对比
三个等级全部走完之后,我建议你做一个简单的对照记录:
| 等级 | 服务端校验 | 绕过方式 | 上传后文件类型 |
|---|---|---|---|
| Low | 无 | 直接上传 | PHP |
| Medium | MIME类型+大小 | 修改Content-Type | PHP |
| High | 文件头+随机名+jpg后缀 | 图片马+文件包含链 | JPG(需配合解析) |
这个表格看起来简单,但它概括了文件上传漏洞测试的三种典型场景。以后你在真实项目里看到一个上传接口,可以按这张表去推断它大概做了哪层防护,再决定从哪里找突破口。
4. 新手必踩的坑与排查思路
4.1 文件上传成功,但访问后变成了下载
这种情况非常常见。你在Low等级上传了shell.php,浏览器访问却弹出下载框,而不是执行PHP代码。原因通常不在DVWA本身,而在于Web服务器没有把.php交给PHP解释器。
我在Windows上用phpStudy搭环境时遇到过一两次这样的问题,排查重点是Apache或Nginx的配置里是否启用了PHP模块,以及PHP版本是否和配置匹配。最省事的检查方法是:在Web根目录放一个test.php,内容写<?php phpinfo(); ?>,如果能正常显示说明解析环境没问题;如果也是下载,那就先修环境,不要急着打靶场。
4.2 一句话木马连接失败,页面白屏或500
蚁剑连接失败的常见原因大概有这几类:
- PHP版本和一句话写法不兼容。老版本
@eval($_POST['x'])在部分PHP 7+环境不好用,换成@assert($_POST['x'])试试,或者用phpinfo()文件先验证执行能力。 - 函数被禁用。有些环境在
disable_functions里禁用了assert等函数,导致代码执行不了。 - 路径写错。上传后显示的路径是
../../hackable/uploads/xxx.php,拼完整URL时要根据DVWA目录层级调整,尤其注意端口号和上下文路径。 - 用蚁剑连接时密码和代码里的参数名不一致。代码是
$_POST['x'],连接密码就该填x,这个细节最容易忽略。
排查的时候不要瞎猜,先用浏览器直接访问上传后的文件,看页面返回什么;再用curl手动提交一个参数试试,比如:
curl -X POST http://localhost:8080/dvwa/hackable/uploads/shell.php -d "x=phpinfo();"能输出PHP信息,就说明问题出在客户端连接配置上;没有输出,就要回头查PHP环境了。
4.3 改完Content-Type还是被拦截
如果你在Medium等级改了Content-Type,页面还是提示拦截,那就说明你用的DVWA版本或环境里,服务端代码可能不只是检查MIME类型,可能还额外加了扩展名或文件内容校验。遇到这种变体情况,不能机械只改MIME,要相应地调整测试方式:
- 试试双重扩展名,比如
shell.php.jpg,有些校验只看最后一个后缀。 - 试试大小写变形,
shell.PhP在不同系统上的处理逻辑可能不一样。 - 试试在文件内容里拼接图片头,做一个“既像图片又含PHP代码”的文件,再配合解析漏洞。
说到底,测试文件上传漏洞最忌讳的是“背三步操作”,一定要先看服务端返回的提示、再猜测代码逻辑、再设计测试用例。DVWA这三个等级已经给了很好的训练场,多打几遍变体,手感自然就出来了。
4.4 图片马访问了还是没有执行
这是High等级最常见的困惑。你把图片马传上去了,返回一个随机jpg路径,访问它却只看到一个图片或下载框,这是正常的。原因很简单:jpg后缀的文件不会被当作PHP执行,除非有解析漏洞或文件包含漏洞把它“带入”PHP执行流程。
所以High等级的正确打法不是访问jpg,而是去找DVWA里的文件包含模块,用类似http://localhost:8080/dvwa/vulnerabilities/fi/?page=../../hackable/uploads/随机名字.jpg这样的路径把图片包含进来,服务端才会以PHP方式解析文件内容,图片尾部的代码才得以执行。
如果你在测试中遇到了类似场景,却没有文件包含接口,那也还有思路:检查服务器是否允许上传.htaccess或.user.ini,通过配置文件改变同目录文件的解析行为。这些属于进阶玩法,但在真实渗透测试中经常用到。
4.5 DVWA环境本身的小毛病
搭建DVWA时也容易遇到几个老问题:
- Docker跑起来后页面样式丢失或跳转不对,大多是容器端口映射和DVWA配置里的路径设置不一致,检查
config/config.inc.php里的数据库账号密码,以及确认容器内的Apache文档根目录。 - 数据库连接不上,报错信息里提示
Access denied,去config.inc.php里把数据库用户名和密码改成db对应的账号密码。 - PHP版本太高导致DVWA某些模块报废弃函数警告,可以用Docker里自带的PHP版本,或者本地环境切换到PHP 7.x版本,问题通常会消失。
这些都是体力活,不用害怕,耐心看报错信息,按提示一步步配,基本都能解决。
5. 从“会打”到“会防”:上传防线该怎么设计
5.1 服务端必须守住的几条底线
DVWA三个等级走完之后,我最大的感受是:单纯靠某一项检查是防不住文件上传的,必须组合多层校验。如果让我给一个真实的文件上传模块做安全设计,至少要有这几层:
- 扩展名白名单,而不是黑名单。明确允许
jpg、png、gif、pdf等少数类型,其他一律拒绝。黑名单穷举不完,白名单才能收敛范围。 - MIME类型校验只作为辅助,不能作为唯一依据,因为MIME是客户端可控的。
- 文件内容校验,用类似
getimagesize()或其他格式检测手段,确认文件确实是声明的格式。 - 随机重命名+存储目录不可执行。用户上传的文件一律改成随机文件名,存放到没有脚本执行权限的目录,或者直接放独立的对象存储,不给Web容器机会去解析。
- 限制文件大小,防止恶意超大文件拖垮服务。
- 对上传接口做频率限制和鉴权,避免接口被批量滥用。
这些策略叠起来,攻击者想上传一个直接执行的脚本会变得非常困难。
5.2 黑名单为什么永远不够用
我见过很多项目在文件上传处用黑名单拦截危险后缀,比如禁止上传php、jsp、asp、exe,但效果通常不理想。原因在于攻击者永远能找到黑名单之外的可执行后缀,比如phtml、php3、php5、phar、pht,以及各种编码变体、大小写变体、双重扩展名。尤其在一些老旧的中间件上,解析规则千奇百怪,黑名单根本堵不住。
把DVWA三个等级打通之后,你应该能理解一个核心原则:安全设计里的“允许”永远比“禁止”可靠。黑名单是“我认识所有坏人,所以坏人来了我拦住”,白名单是“我只让认识的好人进来,陌生人一律拒绝”。后者在现实中的防御效果远好于前者。
5.3 多漏洞组合才是真实世界的样子
High等级如果只靠上传模块本身,很难直接攻破,但一旦和文件包含漏洞组合,攻击链就立刻“活”了。这其实是在提醒我们:真实业务系统的安全问题,往往不是一个漏洞单独存在,而是多个小问题组合在一起才形成致命风险。
做防御时,只修复一个文件上传接口是不够的,还要看解析配置、文件包含、路径可控性、目录权限等周边因素;做测试时,只盯着一个点闷头打也是不够的,要把整个应用当成一张网来看。DVWA的价值就在于:它把这张网拆成了一个个模块,让你先看清每一个节点,再学会把节点串联起来。
最后分享一个我自己的体会:DVWA这类靶场,最好每隔一段时间回来重打一遍。第一次你是在学操作,第二次可以尝试不看任何资料独立完成,第三次可以试着给自己设定一些自定义目标,比如绕过更多变体、写一段自动化检测脚本。每一次重打,你都会发现自己对漏洞的理解又深了一层。文件上传看似简单,但它映射出的“输入校验”思想,几乎是整个Web安全的核心命题,值得反复琢磨。