【web漏洞】PHP反序列化

【web漏洞】PHP反序列化

目录

    • 知识点
    • 反序列化常用方法:
    • 序列化的(构造payload)运行顺序
    • 反序列化的(实现payload)运行顺序
    • 绕过__wakeup()
    • __tostring()

知识点

  • 序列化(serialize): 对象的状态信息转换为可以存储或传输的形式的过程 在序列化期间,对象将当前的状态写入到临时或持久性的存储区【将状态信息保存为字符串】。
  • 反序列化(unserialize): 将字符串转换为状态信息 序列化 <—>反序列化。
  • 构造POP链:通过用户可控的反序列化操作,其中可触发的魔术方法为出发点,在魔术方法中的函数在其他类中存在同名函数,或通过传递,关联等可以调用的其他执行敏感操作的函数,然后传递参数执行敏感操作,即

用户可控反序列化→魔术方法→魔术方法中调用的其他函数→同名函数或通过传递可调用的函数→敏感操作

  • PHP反序列化只能修改成员变量的值和构造方法,其他的代码区不能修改
  • (1)属性声明是由关键字public,protected或者private开头,然后跟一个普通的变量声明来组成。 属性中的变量可以初始化,但是初始化的值必须是常量。所以修改属性值时,不能修改成属性=对象。

序列化的内容只有类名和成员变量,所以可控点是,类名和成员变量的值。
通过控制类名、可以指定反序列化的类,通过控制变量的值,就可以影响代码执行流程。然后按照我们的期望,将这些“影响”连接在一起,就可以控制代码的执行。

  • 序列化之后的字符串构成:
    序列化的内容只有类名和成员变量 成员方法不会被序列化

  • 在序列化时,会执行构造函数,所以就可以通过构造函数来控制各属性的值,得到我们想要属性值然后序列化这些属性值

在这里插入图片描述
多加了几个方法,最后输出的还是一样的:
在这里插入图片描述
但是添加构造方法之后,序列化的值就改变了,因为序列化的过程会新建对象,新建对象就会调用构造函数:
在这里插入图片描述

<?php
class key{
    public $a="abc";
    public $b=123;
    public function __construct(){
        $this->a="flag{123456}";
        $this->b=new key1();
    }
}
class key1{}
echo serialize(new key());
//O:3:"key":2:{s:1:"a";s:12:"flag{123456}";s:1:"b";O:4:"key1":0:{}}
类型 结构
String s:size:value;
Integer i:value;
Boolean b:value;(保存1或0)
Null N;
Array a:size:{key definition;value definition;(repeated per element)}
Object O:strlen(object name):object name:object size:{s:strlen(property name):property name:property definition;(repeated per property)}
  • 想象自己是一个特工,你的目标是监控一个重要的人,有一天你怀疑目标家里的窗子可能没有关,于是你上前推了推,结果推开了,这是一个POC。之后你回去了,开始准备第二天的渗透计划,第二天你通过同样的漏洞渗透进了它家,仔细查看了所有的重要文件,离开时还安装了一个隐蔽的窃听器,这一天你所做的就是一个EXP,你在他家所做的就是不同的Payload,就把窃听器当作Shellcode吧!

反序列化常用方法:

__wakeup() //执行unserialize()时,先会调用这个函数
__sleep() //执行serialize()时,先会调用这个函数
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据或者不存在这个键都会调用此方法
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当尝试将对象调用为函数时触发

序列化的(构造payload)运行顺序

<?php
class A{
    public  $a=11;
    public function  __construct($b){  //2
        $this->a=$b;
        echo "A构造函数\n";
    }
    public function __wakeup(){
        echo "A苏醒函数\n";
    }
    public function  __destruct(){  
        echo "A析构函数first\n";        //3
        echo $this->a."aaa\n";
        echo "A析构函数second\n";       //5
    }
}
class B{
    public $x=1;
    public function  __construct($b){  //1
        $this->x=$b;
        echo "B构造函数\n";
    }
    public function  __toString(){     //4
        return "flag{a_b_c}";
    }
    public function  __destruct(){
        echo "B析构函数\n";    		   //6
    }
}
echo urlencode(serialize(new A(new B("Aa"))));
//O:1:"A":1:{s:1:"a";O:1:"B":1:{s:1:"x";s:2:"Aa";}}
//O%3A1%3A%22A%22%3A1%3A%7Bs%3A1%3A%22a%22%3BO%3A1%3A%22B%22%3A1%3A%7Bs%3A1%3A%22x%22%3Bs%3A2%3A%22Aa%22%3B%7D%7D
//先执行每个对象的构造函数,再执行析构函数
//结果:
//B构造函数
//A构造函数
//A析构函数first
//flag{a_b_c}aaa     (只要在构造payload的时候打印出了我们想要的东西,就是构造成功了)
//A析构函数second
//B析构函数
  • 运行顺序:

serialize()之后,依次执行:

__construct()
__toString()(如果被特定条件触发了)
__destruct()

析构方法在所有的代码被执行结束之后进行

反序列化的(实现payload)运行顺序

<?php
<?php
class A{
    public  $a=11;
    public function  __construct($b){
        $this->a=$b;
        echo "A构造函数\n";
    }
    public function __wakeup(){
        echo "A苏醒函数\n";             //1
    }
    public function  __destruct(){
        echo "A析构函数first\n";        //2
        echo $this->a."aaa\n";        
        echo "A析构函数second\n";       //4
    }
}
class B{
    public $x=1;
    public function  __construct($b){
        $this->x=$b;
        echo "B构造函数\n";
    }
    public function  __toString(){
        return "flag{a_b_c}";         //3
    }
    public function  __destruct(){
        echo "B析构函数\n";            //5
    }
}
unserialize($_GET[1]);
//echo urlencode(serialize(new A(new B("Aa"))));
//O:1:"A":1:{s:1:"a";O:1:"B":1:{s:1:"x";s:2:"Aa";}}
//1=O%3A1%3A%22A%22%3A1%3A%7Bs%3A1%3A%22a%22%3BO%3A1%3A%22B%22%3A1%3A%7Bs%3A1%3A%22x%22%3Bs%3A2%3A%22Aa%22%3B%7D%7D
//先执行wakeup函数,再执行触发了的toString函数,最后执行析构函数,就算用到了构造函数改变的值,也**不会主动去执行构造函数**
//结果:
//A苏醒函数
//A析构函数first
//flag{a_b_c}aaa
//A析构函数second
//B析构函数
  • 运行顺序:

unserialize()之后,依次执行:

__wakeup()
__toString()(如果被特定条件触发了)
__destruct()

析构方法在所有的代码被执行结束之后进行

绕过__wakeup()

  • 因为unserialize() 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源。__destruct()析构最后执行。所以有时执行wakeup的时候防止wakeup干坏事,我们就要绕过wakeup()函数:
PHP版本:
php5 < 5.6.25
php7 < 7.0.10

把类的属性值加1即可绕过wakeup:

O:5:"hello":1:{s:5:"test4";s:11:"hello,world";}

换成

O:5:"hello":2:{s:5:"test4";s:11:"hello,world";}

在这里插入图片描述

__tostring()

  • __toString 触发的条件比较多,也因为这个原因容易被忽略,常见的触发条件有下面几种
(1)echo ($obj) / print($obj) 打印时会触发
(2)反序列化对象与字符串连接时
(3)反序列化对象参与格式化字符串时
(4)反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)
(5)反序列化对象参与格式化SQL语句,绑定参数时
(6)反序列化对象在经过php字符串函数,如 strlen()addslashes()(7)in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用
(8)反序列化的对象作为 class_exists() 的参数的时候

由于是私有属性,他有自己特殊的格式会在前后加两个 %00 ,所以我们在传输过程中绝对不能忘掉. 反序列化字符串中存在 \x00字符,这个其实是类的私有属性反序列化后的格式,protected 属性也有自己的反序列化格式

                       

点击阅读全文

上一篇 2023年 6月 9日 pm9:01
下一篇 2023年 6月 9日 pm9:01