目录
- 知识点
- 反序列化常用方法:
- 序列化的(构造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"; //3echo $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(){ //4return "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"; //2echo $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 属性也有自己的反序列化格式