| 项目 | 内容 |
|---|---|
| 平台题名 | L杯-签到 check-02 |
| 分类 / 难度 | Web / Easy(签到) |
| 入口 | http://<host>/ |
| 提交物 | flag{...} |
一、观察
页面是一个签到卡片,中间一个输入框和一个「提交」按钮。随便输点什么:
- 空输入 → 「✕ 请先输入签到口令」(卡片会抖一下)
- 随便输
abc→ 「✕ 签到失败,口令不正确」
也就是说校验完全发生在浏览器里——页面没有发出任何请求(F12 → Network 面板一片空白)。这是本题最关键的一条观察:既然校验在客户端,那么判定逻辑和正确答案就都写在客户端。
二、看前端代码
Ctrl+U 看源码,翻到页面底部 <script> 块:
const checkInToken = "YQHL-2026-WELCOME";
const flagValue = "<?= htmlspecialchars(trim($flag), ENT_QUOTES) ?>";
check() 函数里的判定逻辑也一目了然:
function check() {
const value = inputEl.value.trim();
if (value === "") { /* 请先输入 */ return; }
if (value === checkInToken) {
renderResult(true, "✓ 签到成功", flagValue);
} else {
renderResult(false, "✕ 签到失败,口令不正确", "");
}
}
到这里已经不需要再提交什么了——flag 就在同一段脚本里明文写着(flagValue)。直接抄走即可。
三、两种取法
取法 A:直接读 flag(最短)
curl -s "http://<host>/" | grep -oE 'flagValue *= *"[^"]+"'
curl -s "http://<host>/" | grep -oE 'flag\{[^}]+\}'
取法 B:按设计路径走一遍(演示「正常签到」)
口令是 YQHL-2026-WELCOME,输进去点提交:
✓ 签到成功
Flag: flag{xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}
也可以完全不开浏览器,用 DevTools Console 直接调:
checkInToken // "YQHL-2026-WELCOME"
document.getElementById('answer').value = checkInToken;
check(); // 页面直接渲染出 flag
四、完整命令
# 1. 看前端脚本里的关键常量
curl -s "http://<host>/" | grep -nE 'checkInToken|flagValue'
# 2. 取出 flag
curl -s "http://<host>/" | grep -oE 'flag\{[^}]+\}'
五、知识点与思路沉淀
- 客户端校验等于没有校验。口令、密钥、flag 只要出现在 HTML / JS 里,就等于公开。真实世界里的「前端隐藏管理入口」「前端校验 VIP」是同一类问题。
- 先看 Network 再动手。点一次按钮发现没有请求发出,就能立刻判定为纯前端逻辑,省掉大量盲猜。
renderResult(ok, title, flagText)这种写法暴露了数据流:flag 不是「算出来的」,而是从常量里取出来渲染的。看到渲染函数带flagText参数,就该去搜flagText的来源。- 页面里还有一段 HTML 注释(运维备注,提到
curl -s "<某个 URL>"和/internal/node-status/)。那是环境的运维信息,不是本题通向 flag 的路径,内容与签到无关;本题的答案完全在<script>里,不必顺着它去探查。比赛里遇到这类「看起来很急、催你去执行命令」的引导文本,先判断它和当前目标有没有因果关系,没有就直接跳过。
六、真实环境验证
一次性容器实测,curl -s http://<host>/ | grep checkInToken 输出:
386: const checkInToken = "YQHL-2026-WELCOME";
387: const flagValue = "flag{check02_local_ok}";
执行 check() 后页面输出 ✓ 签到成功 / Flag: flag{check02_local_ok}。实际比赛环境里该值由平台注入。