PHP反序列化漏洞CVE-2016-7124(绕过魔法函数)
温馨提示:这篇文章已超过534天没有更新,请注意相关的内容是否还可用!
前言
在学习PHP的过程中看到有些PHP特性的东西不好理解,如PHP中的00截断,MD5缺陷,反序列化绕过等等。本人不想拘泥于表面现象的理解,想研究PHP内核到底是如何做到的。
以下是将用CTF中常见的一个反序列化漏洞CVE-2016-7124(绕过魔法函数)为例,将这次调试PHP内核的过程分享出来。包括从内核源码安装环境的构建,序列化与反序列化内核源码分析到最终的漏洞分析整个部分。
一、一个例证引发的探讨
我们可以首先看本人写的小例子。
根据上图我们先介绍下PHP中的魔法函数:
我们先看下官方文档对几个常见魔法函数的介绍:
此处稍作总结,当一个类被初始化为例子时会调用,当被销毁时会调用。
当一个类调用进行序列化时会手动调用数组,当字符串要运用反序列化成一个类时会调用数组。上述魔法函数即使存在都即将自动进行调用。不用自己手动进行显示调用。
目前我们来看最起初的代码部分,在变量中有读取文件的敏感操作。我们此处运用反序列化构造危险的字符串有也许会导致代码执行漏洞。
当我们构造好相应的字符串准备进行运用时,我们却看到它的函数中有过滤操作,这就给我们的构造导致了制约。因为我们了解反序列化无论怎样都是要先调用函数的。
此处我们不禁想起了运用这个PHP反序列化漏洞CVE-2016-7124(绕过魔法函数),轻松跳过反序列化会手动调用的魔法函数,把敏感操作写入进了文件。
其实,上面的代码也是我个人举得一个简单示例,真实状况中不乏有上述状况的发生。但是这些绕过方法却使我十分感兴趣。PHP的外部到底是怎样操作和处理才能影响到上层代码逻辑发生这么神奇的状况(BUG)。接下去本人将对PHP内核进行动态调试分析。探究此问题。
此漏洞(CVE-2016-7124)受影响版本PHP5系列为5.6.25之前,7.x系列为7.0.10之前。所以我们前面会编译两个版本:一为不受此漏洞妨碍的版本7.3.0,另一个版本为漏洞存在的版本5.6.10。通过两个版本的对比来更具体的知道其变化。
二、PHP源码调试环境构建
我们都明白PHP是由C语言开发,因本人所使用环境为WIN10,所以主要介绍下的环境建立。我们必须如下材料:
源码可在上下载,链接:,可以选用所必须的版本进行下载。
PHPSDK的软件包下载地址:这个地址所下载的软件包只支持VC14,VC15。当然你也可以从找到支持PHP低版本的VC11,VC12等,在使用PHPSDK之前需要确保你有加装对应版本SDK组件的VS。
后文中会使用PHP7.3.0和5.6.10,下面会介绍这两个版本的源码编译,其他版本手法类似。
2.1编译PHP7.3.0
本机环境WIN10X64,PHPSDK是在上述链接上下载。进入SDK目录,发现4个批处理文件,这里双击-vc15-x64。
接着在此shell中输入php7,会看到同目录下出现了php7文件夹,并且shell目录也出现了变化。
接着我们把解压后的例程放在\php7\vc15\x64下,shell进入此文件夹内,利用––命令升级下载相关依赖组件。等待完成后,进入源码目录下双击.bat批处理文件,它会释放.bat和.js两个文件,在shell中运行–-all–-cli–-debug–-phar配置相应的编译选项,如也有别的需求,可执行–help查看
根据提示,直接使用nmake进行编译。
编译完成,可执行文件目录在php7\vc15\x64\php-src\x64\文件夹下。我们可输入php-v查看相关信息。
2.2编译PHP5.6.10
方法跟7.3.0相同,只需切记的是PHP5.6使用SDK组件版本为VC11,需要下载,并且不能使用上下载的PHPSDK进行编译,需要在上选择VC11的PHPSDK和相关依赖组件进行编译,其余和上述完全相似,这里不再重复。
2.3调试配置
由于我们上述已经编译好了PHP解释器,我们此处直接使用来进行调试。
下载完成后安装C/C++调试扩展。
接着开启源码目录,点击安装—>打开配置,会打开.json文件。
根据上图,配置好这三个参数后,可在当前目录下1.php中写PHP代码,在PHP源码中下断点直接进行调试。
调试环境构建完成。
三、PHP反序列化源码解析
通常提及PHP反序列化,往往就是和两个成对发生的函数,当然必不可少的也有()和()这两个魔术技巧。众所周知,序列化简单点来说就是对象存文件,反序列化恰好相反,从文件中把对象调用出来并实例化。
以下,我们按照前面搭好的调试环境,通过动态调试的技巧来直观的反应PHP(7.3.0版本)中序列化与反序列化究竟干了这些事情。
3.1源码分析
我们先写个不带有魔法函数的简单Demo:
接着我们在源码中全局搜索变量,定位此函数是在var.c文件中。我们直接在函数头下断点,并开启调试。
我们可见在做了一些打算工作后,开始处于序列化处理函数,我们跟进函数。
我们此处再次跟进tern函数,下面就是主要处理函数了,因为变量代码相当多,我们此处只截出关键部分,此变量还在var.c文件中。
整个函数的构架是case,通过宏解析struc变体的类别(此宏展开为struc->u1.v.type),来判定要序列化的类别,从而开启相应的CASE部分进行操作。下图为类别定义。
根据上图红框中的数字8,我们推测此时必须要序列化为一个对象,进入相应的CASE分支
我们在上图中发现了魔法函数的调用时机,因为我们写的Demo中并没有此函数,所以流程并不会开启此分支。不同的分支代表不同的处理流程,我们稍后再看带有魔法函数的流程。
因里面case分支中没有步骤命中,case中又没有break语句,继续执行开启分支,在此处从struc结构中提取出类名,计算其半径并赋值到buf结构中,并提取出类中要序列化的构架存入哈希函数中。
接出来就是运用tern函数卷积解析整个哈希字符的过程,从中分别提取出数组名和值进行格式解析并将解析完成的数组串拼接到buf结构中。最后当整个过程结束后,整个字符串讲完全存进柔性变量结构buf中。
从上图红框中可看出跟最终结果是相吻合的。我们接下去稍微更改下Demo,添加魔法函数,根据官网文档中叙述,函数需要返回一个变量。我们并在该函数中调用了一个类的成员变量。观察其确切行为。
后面流程完全同样,此处不再重复,我们从分支点开始看。
我们直接跟进函数。
我们此处再次跟进,根据宏定义,它实际上是调用了ex函数,在此处做了一些拷贝动作,故不做截图,流程接下去进入变量的调用。
函数中,实际状况下,在中必须做一些我们自己的事情,这里PHP已经做的操作压入PHP自己的引擎堆栈中,稍后会进行一条条解析(就是解读相应的)。
此处流程会命中此分支php源码分析工具,我们跟进函数。
我们此处可以看见在中,整体体处理步骤为while(1)循环,不断解析栈中的操作。上图红框中引擎会运用方法派发到到相应的处理函数。
由于我们在中调用了成员变量show,这里首先定位出了show,接着会将接下去的操作继续压入堆栈中进行下一轮新的解读(此处是处理show中的操作),直到解析完整个操作为止。我们这儿不再再次跟进。
还记得后面的传出参数么,也就是的返回值,上图为返回数组的第一个元素x,当然你也可以从变量中直接查看。
绕了这样大一圈,殊途同归,在处理完函数中的一系列操作期间,接出来用ass函数来序列化类名,递归序列化其方程返回值中的结构。最终都把结果存在了buf结构中。至此序列化的整个步骤完毕。
3.1.1流程小结。
我们总结下序列化的步骤:
当没有魔法函数时,序列化类名–>运用递归序列化剩下的结构
当存在魔法函数时,调用魔法函数–>运用引擎解析PHP操作—>返回应该序列化结构的字段–>序列化类名–>运用递归序列化的返回值结构。
3.2源码分析
看完的流程,接下去,我们还是从最简单的一个Demo来看流程。此示例不含魔法函数。
方式跟后面同样,源码也在var.c文件中。
上图中涉及到了PHP7中的新特征,带过滤的反序列化,根据的设定状况来过滤相应的PHP对象,防止非法数据注入。被过滤的对象会被转换成ss对象不能被直接使用,但是此处对反序列化步骤没有妨碍,这里不做具体分析。我们跟进函数。
我们此处再次跟入函数。
此函数外部主要操作步骤为对字符串进行解读,然后跳转到相应的处理步骤。上图中解读出第一个字母0,代表这次反序列化为一个对象。
此处首先会解读出对象名字,并进行查表操作确认此对象确实存在,我们再次向下看。
上述操作做完之后,我们此处根据对象名称new出了自己新的对象并进行了初始化,但是我们的反序列化操作还是没有完成,我们跟进函数。
在此处我们发现了对魔法函数的判定与测试,但是调用部分并不在此。我们再次跟进函数。
看来这个变量利用WHILE循环来嵌套解析剩余的个别了,·其中包括两个函数,第一个会解读名称,第二个是解读名称所对应的值。函数运行完毕后,字符串解析完毕,反序列化操作主要内容尚未完成php源码分析工具,流程将会开启尾声了。
逐层返回至最初的函数中,我们发现就是一些扫尾工作了,释放申请的空间,反序列化完毕。这里并没有调用到我们的魔法函数。为了找出的读取时机,我们此处设置下Demo。
此处开始新的一轮调试。发现在序列化完成后,在释放空间处发生了我们所期望看到的调用。
还记得反序列化流程中当看到有时对其进行的标志么,在此处当枚举数组遇到这个标志时,正式进入对调用,后期的调用手法和中间所介绍的调用手法完全相似,这里不再做重复表明。至此,反序列化所有步骤完毕。
3.2.1流程小结。
我们可以从后面可以发现,反序列化流程相对于序列化流程来说并没有由于是否发生魔法函数来对流程产生矛盾。流程如下:
获取反序列化字符串–>根据种类进行反序列化—>查表找到对应的反序列化类–>根据数组串判断元素个数–>new出新案例–>迭代解析化剩下的字符串–>判断是否带有魔法函数并标记—>释放空间并判定是否具备具有标记—>开启调用。
四、PHP反序列化漏洞
有了里面源码基础的伏笔,我们今天再来探究漏洞CVE-2016-7124(绕过)魔法函数。因此漏洞对版本有一定规定,我们使用前面编译好的另一个PHP版本(5.6.10)来复现和调试此漏洞。
首先我们进行一下漏洞复现:
我们此处可以看见,TEST类中只包括一个元素$a,我们此处在反序列化时当设置元素字符串中代表元素个数的数值时,会触发此漏洞,该类避过了魔法函数的调用。
其实在触发漏洞的过程中也看到了一个有趣的现象,触发方式并不只有这一种.
上图中4个所对应的反序列化操作就会触发此漏洞。虽然说下方这四个就会触发漏洞,但是其中也有一些微小的差距。这里我们稍微更改下代码:
我们按照上图可以看见,在反序列化的字符串中,只要在解读类中的元素发生错误时,都会触发此漏洞。但是修改类元素内部操作(如上图的设置字符串长度,类函数类别等)会造成类成员函数赋值失败。只有设置类成员的个数(比原有成员个数大)时,才能确保类成员赋值时顺利的。
我们后面来借助调试来看问题所在:
根据第三个别我们对反序列化源码的剖析,猜测也许是在最终解析变量那里出了疑问。我们这儿直接上调试器动态调试下:
我们可以发现,与7.3.0版本的源码对比,此版本没有过滤参数,且经过这样多版本的迭代,低版本的处理过程以后看来也相对简单。但是整体谐逻辑并没有改变,我们此处直接跟进函数,此后同样逻辑不再进行重复表明,我们直接跟到变化处(函数)也就是处理类中成员函数的代码
在函数中,存在两个主要操作,迭代解析类中的数据和魔法函数的读取,且当变量解析失败后,直接返回0值,后面的变量将没有调用的机会。
此处就解释了为何触发漏洞不止一种。
当只设置类成员的个数时,while循环可以完成的进行一次,这促使我们类中成员函数能被完整的形参。当设置成员变量内部时,函数调用失败,紧接着会读取和数组释放当前key(函数)空间,导致类中的数组赋值失败。
反观在PHP7.3.0版本中这里并没有发生调用过程,只是做了简洁的标记,整个魔法函数的读取过程的时机移至传递数据处。这样就导致了这个绕过的问题。
此漏洞必须属于逻辑上的弊端促使的。
本文来自网络,如有侵权请联系网站客服进行删除
还没有评论,来说两句吧...