3.1 反序列化漏洞
在各类语言中,将对象的状态信息转换为可存储或可传输的过程就是序列化,序列化的逆过程便是反序列化,主要是为了方便对象的传输,通过文件、网络等方式将序列化后的字符串进行传输,最终通过反序列化可以获取之前的对象。
很多语言都存在序列化函数,如Python、Java、PHP、.NET等。在CTF中,经常可以看到PHP反序列化的身影,原因在于PHP提供了丰富的魔术方法,加上自动加载类的使用,为构写EXP提供了便利。作为目前最流行的Web知识点,本节将对PHP序列化漏洞逐步介绍,通过一些案例,让读者对PHP反序列漏洞有更深的了解。
3.1.1 PHP反序列化
本节介绍PHP反序列化的基础,以及常见的利用技巧。当然,这些不仅是CTF比赛的常备,更是代码审计中必须掌握的基础。PHP对象需要表达的内容较多,如类属性值的类型、值等,所以会存在一个基本格式。下面则是PHP序列化后的基本类型表达:
❖ 布尔值(bool):b:value=>b:0。
❖ 整数型(int):i:value=>i:1。
❖ 字符串型(str):s:length:"value";=>s:4:"aaaa"。
❖ 数组型(array):a:<length>:{key,value pairs};=>a:1:{i:1;s:1:"a"}。
❖ 对象型(object):O:<class_name_length>:。
❖ NULL型:N。
最终序列化数据的数据格式如下:
接下来通过一个简单的例子来讲解反序列化。序列化前的对象如下:
通过serialize()函数进行序列化:
其中,O表示这是一个对象,6表示对象名的长度,person则是序列化的对象名称,3表示对象中存在3个属性。第1个属性s表示是字符串,4表示属性名的长度,后面说明属性名称为name,它的值为N(空);第2个属性是age,它的值是为整数型19;第3个属性是sex,它的值也是为空。
这时就存在一个问题,如何利用反序列化进行攻击呢?PHP中存在魔术方法,即PHP自动调用,但是存在调用条件,比如,__destruct是对象被销毁的时候进行调用,通常PHP在程序块执行结束时进行垃圾回收,这将进行对象销毁,然后自动触发__destruct魔术方法,如果魔术方法还存在一些恶意代码,即可完成攻击。
常见魔术方法的触发方式如下。
❖ 当对象被创建时:__construct。
❖ 当对象被销毁时:__destruct。
❖ 当对象被当作一个字符串使用时:__toString。
❖ 序列化对象前调用(其返回需要是一个数组):__sleep。
❖ 反序列化恢复对象前调用:__wakeup。
❖ 当调用对象中不存在的方法时自动调用:__call。
❖ 从不可访问的属性读取数据:__get。
下面对一些常见的反序列化利用挖掘进行介绍。
3.1.1.1 常见反序列化
PHP代码如下:
这段代码存在一个test类中,其中__destruct魔术函数中还存在eval($_GET['cmd'])的代码,然后通过参数u来接收序列化后的字符串。所以,可以进行以下利用,__destruct在对象销毁时会自动调用此方法,然后通过cmd参数传入PHP代码,即可达到任意代码执行。
在利用程序中,首先定义test类,然后对它进行实例化,再进行序列化输出字符串,将利用代码保存为PHP文件,浏览器访问后即可显示出序列化后的字符串,即O:4:"test":0:{}。代码如下:
通过传值进行任意代码执行,u参数传入O:4:"test":0:{},cmd参数传入system("whoami"),即最后代码会执行system()函数来调用whoami命令。
漏洞利用结果见图3-1-1。
图3-1-1
有时我们会遇到魔术方法中没有利用代码,即不存在eval($_GET['cmd']),却有调用其他类方法的代码,这时可以寻找其他有相同名称方法的类。例如,图3-1-2是存在漏洞的代码。
以上代码便存在normal正常类和evil恶意类。可以发现,lemon类正常调用便是创建了一个normal实例,在destruct中还调用了normal实例的action方法,如果将$this->ClassObj替换为evil类,当调用action方法时会调用evil的action方法,从而进入eval($this->data)中,导致任意代码执行。
图3-1-2
在Exploit构造中,我们可以在__construct中将Classobj换为evil类,然后将evil类的私有属性data赋值为phpinfo()。Exploit构造见图3-1-3。
保存为PHP文件后访问,最终会得到一串字符:
注意,因为ClassObj是protected属性,所以存在“%00*%00”来表示它,而“%00”是不可见字符,在构造Exploit的时候尽量使用urlencode后的字符串来避免“%00”缺失。
图3-1-3
最终使用Exploit可以执行phpinfo代码,结果见图3-1-3。
图3-1-3
3.1.1.2 原生类利用
实际的挖洞过程中经常遇到没有合适的利用链,这需要利用PHP本身自带的原生类。
1.__call方法
__call魔术方法是在调用不存在的类方法时候将会触发。该方法有两个参数,第一个参数自动接收不存在的方法名,第二个参数接收不存在方法中的参数。例如,PHP代码如下:
通过unserialize进行反序列化类为对象,再调用类的notexist方法,将触发__call魔术方法。
PHP存在内置类SoapClient::__Call,存在可以进行__call魔术方法时,意味着可以进行一个SSRF攻击,具体利用代码见Exploit。
Exploit生成(适用于PHP 5/7):
上面是new SoapClient进行配置,将uri设置为自己的VPS服务器地址,然后将location设置为http://vps/aaa。以上生成的字符串放入unserialize()函数,进行反序列化,再进行不存在方法的调用,则会进行SSRF攻击,见图3-1-4。
图3-1-4
图3-1-4便是进行一次Soap接口的请求,但是只能做一次HTTP请求。当然,可以使用CRLF(换行注入)进行更加深入的利用。通过“'uri'=>'http://vps/i am here/'”注入换行字符。CRLF利用代码如下:
注入结果见图3-1-5,CRLF字符已经将“i am evil string”字符串放到新的一行。
图3-1-5
这里进而转换为如下两种攻击方式。
(1)构造post数据包来攻击内网HTTP服务
这里存在的问题是Soap默认头中存在Content-Type:text/xml,但可以通过user_agent注入数据,将Content-Type挤下,最终data=abc后的数据在容器处理下会忽略后面的数据。
构造POST包结果见图3-1-6。
(2)构造任意的HTTP头来攻击内网其他服务(Redis)
例如,注入Redis命令:
图3-1-6
若Redis未授权,则会执行此命令。当然,也可以通过写crontab文件进行反弹Shell。攻击redis结果见图3-1-7。
图3-1-7
因为Redis对命令的接收较为宽松,即一行行对HTTP请求头中进行解析命令,遇到图3-1-7中的“config set dir/root/”,便会作为Redis命令进行执行。
2.__toString
__toString是当对象作为字符串处理时,便会自动触发。PHP代码如下:
Exploit生成(适用于PHP 5/7):
主要利用了Exception类对错误消息没有做过滤,导致最终反序列化后输出内容在网页中造成XSS,构写Exploit生成时,将XSS代码作为Exception类的参数即可。
通过echo将Exception反序列化后,便会进行一个报错,然后将XSS代码输出在网页。最终触发结果见图3-1-8。
图3-1-8
3.__construct
通常情况下,在反序列化中是无法触发__construct魔术方法,但是经过开发者的魔改后便可能存在任意类实例化的情况。例如,在代码中加入call_user_func_array调用,再禁止调用其他类中方法,这时便可以对任意类进行实例化,从而调用了construct方法(案例可参考https://5haked.blogspot.jp/2016/10/how-i-hacked-pornhub-for-fun-and-profit.html?m=1),在原生类中可以找到SimpleXMLElement的利用。可以从官网中找到SimpleXMLElement类的描述:
通常进行以下调用:
调用时注意,Libxml 2.9后默认不允许解析外部实体,但是可以通过函数参数LIBXML_NOENT进行开启解析。xxe_evil内容见图3-1-9。
图3-1-9
攻击分为两个XML文件,xxe_evil是加载远程的xxe_read_passwd文件,xxe_read_passwd则通过PHP伪协议加载/etc/passwd文件,再对文件内容进行Base64编码,最后通过拼接方式,放到HTTP请求中带出来。
最终通过反序列化的利用也能够获取/etc/passwd的信息,结果见图3-1-10。
图3-1-10
3.1.1.3 Phar反序列化
2017年,hitcon首次出现Phar反序列化题目。2018年,blackhat提出了Phar反序列化后被深入挖掘,2019年便可以看到花式Phar题目。Phar之所以能反序列化,是因为PHP使用phar_parse_metadata在解析meta数据时,会调用php_var_unserialize进行反序列化操作,其中解析代码见图3-1-11。
图3-1-11
可以生成一个Phar包进行观察,需要注意php.ini中的phar.readonly选项需要设为Off。生成Phar包的代码见图3-1-12。
图3-1-12
通过winhex编辑器对Phar包进行编辑,可以看到,文件中存在反序列化后的字符串内容,见图3-1-13。
那么,如何触发Phar反序列化?因为在PHP中Phar是属于伪协议,伪协议的使用最多的便是一些文件操作函数,如fopen()、copy()、file_exists()、filesize()等。当然,继续深挖,如寻找内核中的*_php_stream_open_wrapper_ex函数,PHP封装调用此类函数,会让更多函数支持封装协议,如getimagesize、get_meta_tags、imagecreatefromgif等。再通过传入phar:///var/www/html/1.phar便可触发反序列化。
例如,通过file_exists("phar://./demo.phar")触发phar反序列化,结果见图3-1-14。
图3-1-13
图3-1-14
3.1.1.4 小技巧
反序列化中的一些技巧使用频率较高,但是目前很难出单纯的考点,更多的是以一种组合的形式加入构造利用链。
1.__wakeup失效:CVE-2016-7124
这个问题主要由于__wakeup失效,从而绕过其中可能存在的限制,继而触发可能存在的漏洞,影响版本为PHP 5至5.6.25、PHP 7至7.0.10。
原因:当属性个数不正确时,process_nested_data函数会返回为0,导致后面的call_user_function_ex函数不会执行,则在PHP中就不会调用__wakeup()。
具体代码见图3-1-15。
图3-1-15
可以使用图3-1-16的代码进行本地测试,输入:
可以看到,图3-1-17触发了wakeup中的代码。
当更改demo后的属性个数为2时(见图3-1-18):
可以发现,“i am wakeup”消失了,证明wakeup并没有触发。
图3-1-16
图3-1-17
图3-1-18
这个小技巧最经典的真实案例是SugarCRM v6.5.23反序列化漏洞,它在wakeup进行限制,从图3-1-19中的__wakeup代码可以看出,它会对所有属性进行清空,并且抛出报错,这也限制了执行。但是通过改变属性个数让wakeup失效后,便可以利用destruct进行写入文件。sugarcrm代码见图3-1-19。
图3-1-19
2.bypass反序列化正则
当执行反序列化时,使用正则“/[oc]:\d+:/i”进行拦截,代码见图3-1-20,主要拦截了这类反序列化字符:
这是反序列中最常见的一种形式,那么如何进行绕过呢?通过对PHP的unserialize()函数进行分析,发现PHP内核中最后使用php_var_unserialize进行解析,代码见图3-1-21。
图3-1-20
图3-1-21
上面的代码主要是解析“'O':”语句段,跟入yy17段中,还会存在“+”的判断。所以,如果输入“O:+4:"demo":1:{s:5:"demoa";a:0:{}}”,可以看到当“'O':”后面为“+”时,就会从yy17跳转到yy19处理,然后继续对“+”后面的数字进行判断,意味着这是支持“+”来表达数字,从而对上面的正则进行绕过。
3.反序列化字符逃逸
这里的小技巧是出自漏洞案例Joomla RCE(CVE-2015-8562),这个漏洞产生的原因在于序列化的字符串数据没有被过滤函数正确的处理最终反序列化。那么,这会导致什么问题呢?我们知道,PHP在序列化数据的过程中,如果序列化的是字符串,就会保留该字符串的长度,然后将长度写入序列化后的数据,反序列化时就会按照长度进行读取,并且PHP底层实现上是以“;”作为分隔,以“}”作为结尾。类中不存在的属性也会进行反序列化,这里就会发生逃逸问题,而导致对象注入。下面以一个demo为例,代码见图3-1-22。
图3-1-22
阅读代码,可知这里正确的结果应该是“a:2:{i:0;s:5:"apple";i:1;s:6:"orange";}”。修改数组中的orange为orangex时,结果会变成“a:2:{i:0;s:5:"apple";i:1;s:7:"orangehi";}”,比原来序列化数据的长度多了1个字符,但是实际上多了2个,这个肯定会反序列化失败。假设利用过滤函数提供的一个字符变两个的功能来逃逸出可用的字符串,从而注入想要修改的属性,最终我们能通过反序列化来修改属性。
这里假设payload为“";i:1;s:8:"scanfsec";}”,长度为22,需要填充22个x,来逃逸我们payload所需的长度,注入序列化数据,最后反序列化,就能修改数组中的属性orange为scanfsec,见图3-1-23。
图3-1-23
4.Session反序列化
PHP默认存在一些Session处理器:php、php_binary、php_serialize(处理情况见图3-1-24)和wddx(不过它需要扩展支持,较为少见,这里不做讲解)。注意,这些处理器都是有经过序列化保存值,调用的时候会反序列化。
图3-1-24
php处理器(PHP默认处理):
php_serialize处理器:
当存与读出现不一致时,处理器便会出现问题。可以看到,php_serialize注入的stdclass字符串,在php处理下成为stdclass对象,对比情况见图3-1-25。可以看出,在php_serialize处理下存入“|O:8:"stdClass":0:{}”,然后在php处理下读取,这时会以“a:2:{s:20:"”作为key,后面的“O:8:"stdClass":0:{}”则作为value进行反序列化。
图3-1-25
其真实案例为Joomla 1.5-3.4远程代码执行。在PHP内核中可以看到,php处理器在序列化的时候是会对“|”(竖线)作为界限判断,见图3-1-26。
但是Joomla是自写了Session模块,保存方式为“键名+竖线+经过serialize()函数反序列处理的值”,由于没有处理竖线这个界限而导致问题出现。
图3-1-26
5.PHP引用
题目存在just4fun类,其中有enter、secret属性。由于$secret是未知的,那么如何突破$o->secret===$o->enter的判断?
题目代码见图3-1-27,PHP中存在引用,通过“&”表示,其中“&$a”引用了“$a”的值,即在内存中是指向变量的地址,在序列化字符串中则用R来表示引用类型。利用代码见图3-1-28。
图3-1-27
图3-1-28
在初始化时,利用“&”将enter指向secret的地址,最终生成利用字符串:
可以看到,存在“s:6:"secret";R:2”,即通过引用的方式将两者的属性值成为同一个值。解题结果见图3-1-29。
图3-1-29
6.Exception绕过
有时会遇上throw问题,因为报错导致后面代码无法执行,代码见图3-1-30。
B类中__destruct会输出全局的flag变量,反序列化点则在throw前。正常情况下,报错是使用throw抛出异常导致__destruct不会执行。但是通过改变属性为“O:1:"B":1:{1}”,解析出错,由于类名是正确的,就会调用该类名的__destruct,从而在throw前执行了__destruct。
图3-1-30
3.1.2 经典案例分析
前面讲述了PHP反序列化漏洞中的各种技巧,那么在实际做题过程中,往往会出现一些现实情况下的反序列化漏洞,如Laravel反序列化、Thinkphp反序列化以及一些第三方反序列化问题,这里以第三方库Guzzle为例。Guzzle是一个PHP的HTTP客户端,在Github上也有不少的关注量,在6.0.0<=6.3.3+中存在任意文件写入漏洞。至于Guzzle如何搭建环境,这里不做赘述,读者可自行查阅。
下面对该漏洞进行讲解,环境假设为存在任意图片文件上传,同时存在一个参数可控的任意文件读取(如readfile)。那么,如何获取权限呢?
首先,在guzzle/src/Cookie/FileCookieJar.php中存在如下代码:
而save()函数定义如下:
可以发现,在第二个if判断的地方存在任意文件写入,文件名跟内容都是我们可以控制的;接着看第一个if判断中的shouldPersist()函数:
我们需要让$cookie->getExpires()为true,$cookie->getDiscard()为false或null。这两个函数的定义如下:
接着看$json[]=$cookie->toArray():
而SetCookie中的toArray()如下,即返回所有数据。
所以最后的构造如下:
然后将生成的1.gif传到题目服务器上,利用Phar协议触发反序列化即可。