MoeCTF 2026 Web 安全与渗透测试 Writeup
MoeCTF 2026 Web 安全与渗透测试 Writeup
西电 CTF 终端 · MoeCTF 2026 新生赛 · Web 安全与渗透测试方向
1. Web 安全与渗透测试入门指北(50 分)
考点:前端隐藏不等于安全,flag 藏在 HTML 注释里
这是 Web 方向的第一课。打开附件,是一份「Web 安全与渗透测试入门指北」的教学网页,从头讲 HTTP、前端安全、编码加密这些基础概念。
页面里埋了不少诱饵:控制台会输出一句「真正的 flag 不在控制台里,请继续检查 HTML」,正文还贴了个 fakeflag{This_is_an_example_fakeflag} 防止误提交。

而题目本身也直接给了提示:按下 F12,检查 <head>,或者直接 Ctrl + F 搜 flag。照做之后,在 <head> 顶部就有一段 HTML 注释:
1 | <!-- |
这道题想传达的道理很简单:浏览器为了渲染页面,源码一定得下到本地,所以藏在前端的东西永远不是秘密。
Flag: moectf{W3Lc0me_t0_th3_W0rLd_0f_w3BseCur1ty!!}
2. 查分系统(20 分)
考点:静态网页明文存储数据
附件是一个「数学分析期末成绩查询」的静态网页,只有一个学号输入框和查询按钮。

既然是纯静态页面,数据就肯定在源码里。F12 一看,所有学生成绩都明文存在 <script> 的 allStudentData 数组里,查询逻辑就是按学号 find() 匹配一下,再把该学生的所有字段原样显示出来:
1 | const targetStudent = allStudentData.find(item => String(item[studentIdKey]) === inputId); |
直接在源码里搜 moectf / flag,就定位到了藏 flag 的那条记录:

「序号 47」的学生姓名是 moectf,它的「折合」字段就是 flag。其实也可以直接在页面输入学号 25192837465 查询,页面会原样把全部字段吐出来。

Flag: moectf{y0u_c@n_reAd_th3_h+nn1}
3. 查分系统_revenge(80 分)
考点:AES-GCM 密钥由 8 位学号派生,暴力枚举
这是「查分系统」的复仇版。数据不再明文,而是被 AES-GCM 加密了。不过题目依旧是纯静态网页,所以解密逻辑都在前端。

F12 看 <script>,关键逻辑长这样:
1 | const commonIv = "bW9lY3RmMjAyNml2"; // base64 解码 -> "moectf2026iv" |

问题就出在密钥派生太弱:密钥 = SHA-256(8 位学号),学号空间只有 00000000 ~ 99999999 共 10^8 种,而 IV 又是固定的。网页里嵌了 92 条 AES-GCM 密文,其中 record 46 明显最长(510 字节,其余约 440 字节),多出来的字段就是 flag,只需爆破这一条。
爆破脚本的核心就是遍历 10^8 个学号,用 SHA-256(学号) 当密钥去解密,再用「明文以 {" 开头」快速过滤:
1 | for (let sid = 0; sid < 1e8; sid++) { |
完整脚本用 worker_threads 多核并行,大概 6 秒就爆破出学号 25178346,解密得到成绩单:

Flag: moectf{y0u_c@n_reAd_th3_htmI_Annmnd_Buwuuurp_1IllIIlIIl11llII}
4. 归途(50 分)
考点:信息收集(源码 / 接口 / 响应头 / Cookie / robots.txt)
题目剧情是「猫猫被外星人绑架了」,需要收集 7 把钥匙碎片去救猫猫。思路很直白:7 个 Key 散落在网站各处,每个都是一段 base64,解码后按顺序拼接就是最终钥匙。

前两把钥匙从首页就能拿到:Key1 藏在被 JS Packer 混淆的 <script> 里,解开后是 console.log("Key1-N2ghNUkkTQ==");Key2 直接躺在 HTML 注释里。
之后按提示一步步走:
| Key | 来源 | 值 |
|---|---|---|
| Key3 | GET /getKey?id=3 |
M0szeUVuKw== |
| Key4 | GET /getKey?id=4 |
ZXJUaDFzbGE= |
| Key5 | /getKey?id=4 的响应头 key: |
ITkzdA== |
| Key6 | POST /getKey(登录 guest / p@33vv0rd) |
ZnwxbGFA |
| Key7 | 伪造 user=admin Cookie 访问 /info |
NGFAZzY5Nmc5 |
其中 Key6 提示 “Try login!”,登录页明文写了账号 guest / 密码 p@33vv0rd。登录后访问 /info 又提示 “Try to login the account of admin”,把 Cookie 改成 user=admin 再访问就能拿到 Key7:

Key7 还提到「有一个文件限制了爬虫可以访问的内容」,说的是 robots.txt,里面披露了提交钥匙的页面 /f13ggggg。
7 个 Key 依次 base64 解码后按顺序拼接:
1 | 7h!5I$M@0m4od3K3yEn+erTh1sla!93tf|1la@4a@g696g9 |

Flag: moectf{7dfe16a8-b12f-e7e5-ba48-658955ad2466}
5. 当零来敲门(50 分)
考点:WebSocket + 本地回环低延迟
这是一道挺有意思的「报数大作战」聊天室,题目还顺手给了一个 SSH 登录凭证。

游戏规则是:POST /chat 提交 {user, msg},msg = 当前计数 + 1 就计数 +1;机器人「林林」每隔 10 秒广播一次「0」把计数清零。目标是在清零前让全局计数器连续数到 500。
难点在于计数是全局共享的,走公网 + 浏览器很难在 10 秒内稳定完成 500 次严格递增。但用题目给的 SSH 账号(challenger / moectf2026)登录进去后,发现 Web 服务就监听在 127.0.0.1:8080——直接走本地回环,延迟几乎为 0,10 秒内完成 500 次递增绰绰有余。
在容器内用 aiohttp 异步连发,同时监听 /ws 广播抓 flag:
1 | import asyncio, json, aiohttp |
Flag: moectf{PYTHoN-L4nGU@ge-15-so_siMp1E-thaT_wE_CaN-M@ke_soMe-small-and-practical-tools0}
6. 古籍翻阅(100 分)
考点:路径穿越(Path Traversal)
打开「天一藏经阁」,首页列了 1000 本古籍的目录,每本书链接形如 /read?book=zi/zhuangzi/manuscript。

点开任意一本书都返回 404「此卷散佚」,说明大部分书只是摆着当障眼法。

关键在 /read 的 book 参数——它被直接拼进文件路径,没有任何过滤。路径结构是 分类/书名/文件名 的两级结构,于是尝试 ../../ 向上跳目录,直接读根目录的 flag:
1 | curl 'http://<题目地址>/read?book=../../flag' |

Flag: moectf{re@d_the_r00t_5utr4_w1th_path_tr4v3r5@l}
7. 河湖小盗(100 分)
考点:数字型 SQL 注入(UNION)
「网信院往年试题检索系统」,只有一个按序号查询的表单,POST id 到 /。正常查 id=1 返回「序号 / 登记人 / 公开科目」三列。

id 参数存在数字型 SQL 注入。输入 1 OR 1=1 返回了全部 5 条记录,说明注入点有效且没对 OR 做过滤:

接下来就是常规的 UNION 注入流程——先判断列数(3 列),version() 返回 8.0.46 确认是 MySQL,再枚举 information_schema 找到 flag_table:
1 | -1 UNION SELECT 1,2,3 -- 确认 3 列 |
flag_table 只有 id 和 flag 两列,直接读出来即可:

Flag: moectf{h3re_i5_your_7e$7_pap3r}





