图片真伪校验:getimagesize 为什么挡不住图片马

CLARA轻量论坛系统
CLARA轻量论坛系统 星耀SVIP管理员 黑卡会员
发布于 2026-09-15 23:17 ·3 浏览 ·0 回复

**getimagesize 挡不住图片马,是因为它从头到尾只读文件开头几十个字节——只要把真实图片的头部保留、把 PHP 代码塞在文件尾部或元数据段里,它就会老老实实返回一个"这是一张合法图片"的结论。**真正的二次校验必须是"重新编码",而不是"再读一遍文件头"。

getimagesize 到底检查了什么

结论:getimagesize() 是一个"看图元数据"的函数,不是"验文件真伪"的函数,它的判断依据只有文件头魔数。

它的工作流程大致是:打开文件 → 读开头若干字节 → 匹配已知的图片签名(JPEG 的 `FF D8 FF`、PNG 的 `89 50 4E 47`、GIF 的 `GIF87a/GIF89a`)→ 解析出宽高和类型常量后返回。它不会解码整张图,不会校验像素数据是否完整,更不会扫描文件剩余部分有没有可疑字符串。

同类函数也一样:`exif_imagetype()` 只读前 12 个字节;`finfo_file()` 基于 libmagic 匹配 magic bytes,同样只看头部。**结论:任何基于"文件头比对"的校验,对图片马都是无效的,因为它们压根不看你真正关心的那部分内容。**

图片马的三种常见藏法

结论:图片马的构造思路高度统一——保持合法图片头,把恶意载荷放在解析器不关心的地方。

  1. 尾部追加:`copy normal.gif /b + shell.php /a webshell.gif`,或直接 `cat shell.php >> a.jpg`。文件头仍是合法 GIF/JPEG,getimagesize 通过;文件尾多出一段 `<?php ... ?>`。
  2. 元数据段注入:JPEG 的 COM 注释段、EXIF 字段,PNG 的 tEXt 块,都可以写入文本。这类文件拿去图片软件打开完全正常,肉眼和看图工具都看不出问题。
  3. 多格式兼容(polyglot):同时满足两种格式的解析要求,既能被当图片渲染,又能被 PHP 引擎解析出可执行代码。这类文件连"重命名扩展名"都挡不住。

另外还有一类被忽视的:SVG。它是 XML 文本,可以内嵌 `<script>`,而 getimagesize 对 SVG 通常无法识别类型——如果你的白名单里有 svg,等于直接放行了一个可执行文档。

图片马要真正生效,还需要一个"帮凶"

结论:单纯上传一张带 PHP 代码的图片并不等于 getshell,它必须配合服务器端的错误配置才能执行。

常见触发条件有三个:一是 Nginx/Apache 把上传目录里的非 .php 扩展名也交给了 PHP 解析(典型是 `AddHandler` 多扩展名解析漏洞);二是站点存在本地文件包含(LFI)漏洞,攻击者用 `include $_GET['f']` 之类的入口直接包含上传的 .jpg;三是上传目录里允许落地 .htaccess 之类的解析配置文件。

这也说明为什么防御要分两层:**第一层让文件"内容干净",第二层让文件"即使脏了也执行不了"。**只做第二层(禁执行)在多数情况下够用,但一旦哪天配置被人改动,第一层就是最后的兜底。

真正有效的二次校验:重新编码

结论:唯一能把隐藏载荷物理消灭的做法,是用 GD 或 Imagick 把图片解码成像素矩阵,再重新编码输出一张全新的图。

典型流程(伪代码层面):

// 1. 先用 finfo 做第一道白名单(只作为辅助)
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($tmp);
if (!in_array($mime, ['image/jpeg','image/png','image/gif','image/webp'])) reject();

// 2. 用 getimagesize 拿类型,同样只当辅助
$info = getimagesize($tmp);
if (!$info) reject();

// 3. 关键一步:解码 + 重绘 + 重新编码
$img = imagecreatefromstring(file_get_contents($tmp));
if ($img === false) reject();          // 解不出来说明文件本身有问题
$new = imagecreatetruecolor(imagesx($img), imagesy($img));
imagecopy($new, $img, 0, 0, 0, 0, imagesx($img), imagesy($img));
imagejpeg($new, $savePath, 88);        // 输出全新文件

重编码之后,JPEG 的 COM 段、EXIF、PNG 的 tEXt、GIF 的注释扩展块全部丢失,尾部追加的 PHP 代码自然也不复存在,因为它根本不是像素数据。**结论:这一步才是"图片二次校验"的实质,而不是把 getimagesize 再调用一遍。**

配套的四件事同样不能省:文件名随机化(别用用户提交的名字)、落地目录不可执行(Nginx 写 `location ^~ /uploads/ { location ~ \.php$ { deny all; } }`)、目录权限最小化、图片和附件走两套独立的扩展名白名单。

顺带说一下 Clara BBS 的做法:它的上传安全基线是「全站 CSRF 防护 + 上传文件类型白名单 + 图片二次校验」的组合,白名单管扩展名、二次校验管内容,两者是互补关系而不是二选一。**结论:只开白名单等于只查了文件名字,攻击者改个后缀就绕过去了;只有白名单 + 内容重编码叠加,图片马才真正失去落脚点。**

最后提醒几个常见误区

  • 前端 `<input accept="image/*">` 和 `$_FILES['type']` 都是客户端可控的,攻击者用 Burp 一改就过,不能作为任何判断依据。
  • 仅仅重命名文件(改成 .jpg)没有任何安全价值,PHP 照样能 include 它。
  • 只搜索文件里有没有 `<?php` 字符串也不可靠——PHP 支持短标签,其他语言也有各自的可执行标记,黑名单思路注定滞后。
  • 校验失败时别只返回一个"上传失败",把具体原因(超过 PHP 上限、白名单不符、目录不可写、图片无法解码)提示出来,既方便排查,也让用户知道该改什么。

一句话收束:**getimagesize 能告诉你"这文件看起来像图片",但永远无法告诉你"这文件里只有图片"。**要后者,只能解码重画一遍,再配上不可执行的落地目录。

本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-386.html
转载请注明出处,版权归原作者所有。
他们都看过 1 人浏览过
不能说的秘密

全部回复 0

还没有回复,来抢沙发~