内容纲要

前言

今天回学校 , 路上看到有位师傅提出了这个问题

当时在火车上没空认真想 , 还以为是弱类型比较啥的 . 现在看完题后我十分庆幸遇到了这个问题 , 又学会了一个新姿势~ 这里记录下来~


题目

再看一遍题目

  1. 首先判断当前是否以 GET 方式获取到了 user 参数的值 , 若没有则显示源文件 .

  2. 然后定义了一个数组 : $user = ['admin','xxoo'];

  3. 最后判断从 GET 方式获取到的参数 user 是否完全等于( 值相同 , 类型也相同 ) $user 数组 , 且从 GET 方式获取到的参数 user 的第一项是否不等于 $user 数组的第一项 . 若同时满足这两个条件 , 则打印出 Flag

总的来说 , 这题需要构造 GET 参数 , 使得以 GET 方式获取到的数组完全等于已定义的数组 $user , 且以 GET 方式获取到的数组的第一项不能等于 $user 数组的第一项 .


漏洞复现

看到这里没有任何头绪 , 如果数组A完全等于数组B , 那么数组A的某一项必然等于数组B的某一项 . 因此这个 if 判断语句肯定不成立啊 , 那么如何打印 Flag 呢 ?

没办法 , 只能 Google . 然后我就在 PHP 的官网上找到了 这个提交

该链接中指出如下观点

也就是说 : [0 => 0] === [0x100000000 => 0] 这个条件是正确的

有意思哈 , 这个式子为什么会成立呢 ? 顺藤摸瓜 , 我找到了 PHP 不同数组之间比较由于整数键截断导致结果相同 这个帖子 , 其中指出如下内容

看来还需要特定的环境 , 于是我在 Ubuntu 19.10 虚拟机上编译安装了 PHP 5.6.0 , 由于以后可能也不会用 , 所以就没有配置环境变量以及安装多余的扩展 .

然后执行上述给出的代码 .

/usr/local/php/bin/php -r "var_dump([0 => 0] === [0x100000000 => 0]);"

输出结果为 " bool(true) " , 上述这个判断条件的确是成立的 .


原理分析

这是为什么呢 ? 我找到了PHP数组整数键名截断问题 这篇帖子 .

该文指出 : 此漏洞的成因位于 php-src/Zend/zend_hash.c 文件中 . 我们查看该文件的 1414 行附近 .

注意 Bucket *p1, *p2 = NULL 这句代码 , 来看下 bucket 结构体的定义 . 该定义位于 zend_hash.h 头文件的第 55

看这里 , 变量 h 的类型为 ulong , 也就是 unsigned long , 即无符号长整型数 .

回到 zend_hash.c 文件第 1417 行 , 通过 int result; 这句代码可以看出 , 变量 result 的数据类型为 int 型 , 即整型 .

再看该文件第 1446 行 .

此处进行了减法运算 , 然后将值赋给了变量 result . 这时问题来了 !

 在 C语言中 , 当赋值运算符两边的运算对象类型不同时 , 将要发生类型转换 . 转换的规则是 : 把赋值运算符右侧表达式的类型转换为左侧变量的类型 . 

等号右边的运算结果为 " 无符号长整型 " , 等号左边的变量为 " 整型 " . 我们再来看下 C语言( PHP解释器是用 C 写的 )中 , " 整型 " 和 " 无符号长整型 " 的数据类型对比 .

在 64 位系统中 , unsigned long 型变量所占的字节数为 8 字节 , 而 int 型变量仅占 4 字节 .

 这就牵扯到隐式转换的问题了 , 当将 unsigned long 型变量赋值给 int 型变量时 , 会将低 32 位值送给 int 型变量,而将高 32 位截断舍弃 . 

 这类转换必然会导致被转换者有部分数据被截断 . 从而造成精度丢失 .

结合本题的情况 , 因为 Payload 中数组键名为 0x100000000 , 转换成十进制为 4294967296 , 超过了 int 型数据的最大范围( [-2147483648,2147483647] ) , 因此在隐式转换时 , 溢出的部分将被截断舍去 .

因此 , 这里只要满足十六进制数的最后 8 位( 也就是二进制的低 32 位 )全为 0 即可 , 至于前面是多少都无所谓 , 反正高位数据都会被截断 .

现在该漏洞的产生原因已经非常清晰了 , 简单的说就是 数组键值越界 , 数据类型隐式转换导致精度丢失 , 从而引起的截断漏洞

这么看来该题的 Payload 也不唯一了 , 随便写一个 , 如下所示 :

Payload : ?user[1]=xxoo&user[4294967296]=admin


漏洞的修复

官方给出的修复方法如下

现在返回值不再是截断后的数据 , 而是 " 1 " 或者 " -1 " , 自然也就不用担心截断数据带来的影响了~


关于 x86 ( 32位系统 )下该漏洞的分析

关于 32 位系统环境中是否也有此问题

PHP 不同数组之间比较由于整数键截断导致结果相同 一文中明确指出 : 该漏洞在 32 位 PHP 上并不能成功,只能在 64 位 PHP 上测试成功

 在 32 位系统的 PHP 下 , unsigned long 和 int 两种数据类型都只占 4 个字节 , 因此在隐式转换时不会有数据被截取 , 精度也就不会丢失 . 因此上述理论是完全说的通的 .

 但还有一种可能 , 因为上述两种数据类型都只占 4 个字节 , 所以在处理超出范围的数据时 , 仅取其低 32 位 , 而将高位数据直接舍去 .  那么该攻击将会是成功的 , 但原因就不再是数据类型的隐式转换了 , 而是直接是对越界数据的截断 . 

既然还有疑问 , 本着严谨的态度 , 我找来了 32 位的 Ubuntu . 但官网上 Ubuntu 在 16.04 LTS 后就不再支持 32 位系统了, 因此这里只能使用 Ubuntu 16.04 LTS 32 Bit

同样编译安装好 PHP 5.6.0 , 然后验证一下 .

根据这三条指令可以判断出当前安装的是 PHP 5.6.0 32bit , 然后开始测试

  • 32位系统中 , 相关代码的测试

这个输出是不是出人意料 ? 按照上述输出内容 , 该类攻击在 32 位系统和 64 位系统下都是可以成功的 , 只不过原理不一样 !

32 位系统下的 Payload 的测试

  • 32位系统环境

    由于数组键的数据类型为 unsigned long , 而 32 位系统中 unsigned long 为 4 字节 . 因此遇到键值越界的情况时 , 会直接取其低 32 位 , 将高位数据舍去 . 所以可以攻击成功

  • 64位系统环境

    由于数组键的数据类型为 unsigned long , 而 64 位系统中 unsigned long 为 8 字节 , 范围扩大的很多 . 但是根据 C 语言源代码 , 在赋值时会将一个 unsigned long 型值赋给 int 型值并返回 , 从而导致精度丢失 , 高位数据被截断 . 所以可以攻击成功 .

这么看来 , 32 位环境中该漏洞是可以复现成功的 . 但是 PHP 不同数组之间比较由于整数键截断导致结果相同 中既然明确指出 32 位系统下该漏洞无法复现成功 , 那么肯定有他的道理


关于 32 位 Web 环境中将越界数字转换为字符串的问题

在该文中作者还提到了如下的结论

我并不确定在 32 位 PHP 下是否有这个将越界数字解析转换成字符串的过程 , 这可能与 Web 中间件有关 . 因为作者使用的是 Apache 环境 , 因此这里我们也试验一下~

  1. 32 位系统

    当键为 2147483647 时( 即 32 位系统中 unsigned long 型数据的最大长度 )

    当键值为 2147483648 时( 即越界时 )

    的确 , 在 32 位系统环境下 , 当键值超出最大长度时 , 会将其转换成字符串再输出 .

  2. 64 位系统

    当键为 2147483647

    当键值为 2147483648

    在 64 位系统环境下 , 是不会在键值超过 2147483647 时将其转换为字符串的 .

现在能解释通了 , 虽然从命令行输出来看 , 32 位系统和 64 位系统都可以复现该漏洞 . 但是在 32 位的 Web 环境中 , 中间件( 比如Apache )会把超过 2147483647 的值转换为字符串 , 自然而然就无法替换 $user[0] 的值了 .

这个问题在 PHP数组键值Key越界后涉及的数据类型及值范围浅谈 一文中也提到过


总结

关于 PHP 数组键值越界截断 的问题就分析到这吧 , 又学到了一个新姿势~

如果您还有什么疑问或者见解 , 欢迎您的留言 .

最后修改日期:2020年3月9日

作者

留言

师傅太强了0rz,学习了,要是我也有耐心这样研究就不至于这么菜了,5555.

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。