MoeCTF 2026 Web 安全与渗透测试 Writeup(上)
MoeCTF 2026 Web 安全与渗透测试 Writeup(上篇)
西电 CTF 终端 · MoeCTF 2026 新生赛 · Web 安全与渗透测试方向 · 上篇(共 7 题)
1. 查分系统(20 分)
MoeCTF 2026 · Web 安全与渗透测试
附件:system(2).html(静态网页)
打开附件,是一个「数学分析期末成绩查询」网页,只有一个学号输入框和查询按钮。网页是纯静态的——没有后端,所有逻辑和数据都跟着页面一起下发给浏览器。

在 <script> 里找到核心逻辑 searchScore(),全体学生成绩存在一个 allStudentData 数组里,查询时用 find() 按学号匹配后原样展示所有字段:
1 | const studentIdKey = `学号`; |
一眼就能看出问题:数据是明文的(没有像 revenge 版那样用 AES-GCM 加密)。这里其实藏着 Web 安全的第一条常识——前端隐藏不等于安全。浏览器要渲染页面,就必须把 HTML、CSS、JS 原样下载到本地,凡是放在前端的东西(写死的数组、注释、隐藏的 <div>、硬编码的密码)任何人都能通过「查看源代码」完整读到。所以「把敏感数据明文写进前端」约等于「把数据公开给所有访问者」。
那怎么定位 flag?这种静态页最顺手的办法就是前端代码审计——系统地读前端的 HTML/CSS/JS,找藏起来的敏感信息。对纯静态页来说审计尤其直接:你拿到源码就等于拿到了应用的全部行为,重点是看数据是否明文写死、有没有被注释掉的敏感信息、有没有硬编码的账号密码、加密逻辑的密钥/IV 是否可预测。实际操作上,直接在源码里 Ctrl + F 搜 moectf / flag / password / secret 这类关键词,往往就能快速命中。

果然,源码里「序号 47」的学生姓名是 moectf,它的「折合」字段就是 flag。也可以直接在页面输入该记录学号 25192837465,页面会原样显示全部字段:

Flag: moectf{y0u_c@n_reAd_th3_h+nn1}
2. Web 安全与渗透测试入门指北(50 分)
MoeCTF 2026 · Web 安全与渗透测试 · 50 分(BEGINNER)
附件:Web 安全与渗透测试入门指北.html(静态网页)
打开附件,是一份从头讲 HTTP、前端安全、编码加密的「入门指北」教学网页。这题的考点就是 Web 入门第一课:前端隐藏不等于安全——浏览器为了渲染必须把源码拿到本地,所以真正藏在前端的信息,用开发者工具就能看到。

页面里埋了不少「诱饵」:控制台输出 欢迎来到 Web 世界。真正的 flag 不在控制台里……,明确告诉你 flag 不在控制台;页面里的终端示例 cat mission.txt 和正文贴的 fakeflag{This_is_an_example_fakeflag} 则是防止误提交。
先认识一下工具——浏览器开发者工具,也就是 Chrome/Edge 里按 F12 打开的那套调试面板。它有几个关键面板:Elements(元素) 看当前 DOM 和完整 HTML 源码(能看到注释、隐藏元素、内联脚本),Console(控制台) 执行 JS、看 console.log 输出,Network(网络) 看所有请求和响应,Application/Storage 看 Cookie、localStorage 等本地存储。很多信息泄露题就是「F12 → 看源码 → 找 flag」这个套路。
题目自带的「Hello, Web!」挑战卡片也提示了这一点:按下 F12,打开 Elements 面板检查 <head>,HTML 里可以存在不直接显示的注释,也可以 Ctrl + F 搜 flag。
照做,在 <head> 顶部找到一段 HTML 注释。注释本是用 <!-- 和 --> 包裹、给开发者留说明的,浏览器不会把它渲染到页面上,所以普通用户在页面里看不到——但它同样存在于源码里,查看源代码或 Elements 面板一展开就能看到。开发者常在注释里不小心留下账号、路径甚至 flag,于是就成了信息泄露的重灾区:
1 | <!-- |
flag 直接写在注释里,无需任何解密,复制提交即可。
Flag: moectf{W3Lc0me_t0_th3_W0rLd_0f_w3BseCur1ty!!}
3. 归途(50 分)
MoeCTF 2026 · Web 安全与渗透测试
题目:归途(猫猫被外星人绑架了,快收集 key 救救猫猫)
打开是一个「猫猫的太空冒险」首页,思路很直白:把散落在各处的 7 个 Key 碎片找出来,每个 Key 都是一段 base64 字符串,解码后按顺序拼接成最终钥匙。

从首页 F12 看源码,先拿到前两把钥匙。Key1 藏在一段被混淆的 <script> 里——这用的是 JS 代码混淆(Obfuscation),一种把代码改得难读但功能不变的手段,常见做法有变量名替换成无意义字符、字符串加密、把压缩后的代码塞进 eval 里执行。不过混淆只增加逆向难度、并不提供安全,因为代码最终要在浏览器里跑,eval 里的内容执行前一定可以还原:把 eval(...) 改成 console.log(...) 打印,或在控制台手动执行即可。解开后是 console.log("Key1-N2ghNUkkTQ==")。Key2 则藏在 HTML 注释里:<!-- Key2-QDBtNG9k -->。
之后的 Key 按提示一步步往下走:
| 步骤 | 来源 | Key |
|---|---|---|
| Key3 | GET /getKey?id=3 |
M0szeUVuKw== |
| Key4 | GET /getKey?id=4 |
ZXJUaDFzbGE= |
| Key5 | /getKey?id=4 的响应头 key: |
ITkzdA== |
| Key6 | POST /getKey |
ZnwxbGFA |
| Key7 | 伪造 admin Cookie 访问 /info |
NGFAZzY5Nmc5 |
其中 Key5 有点隐蔽:它不在响应正文里,而在响应头里。HTTP 响应除了正文(body)还有一堆「响应头」,如 Content-Type、Server、Set-Cookie 等,开发者有时会把信息写在自定义响应头里(这里就是 key: 这个头)。看响应头用 F12 的 Network 面板或 curl -i 即可。
Key6 的提示是 “Try login!”,登录页明文写着账号 guest / 密码 p@33vv0rd。登录后访问 /info 提示 “Try to login the account of admin”——这里要动 Cookie。Cookie 是服务器让浏览器存在本地的一小段数据,之后每次请求都会自动带上,服务器据此识别「你是谁」。之所以能伪造,是因为 Cookie 本质只是浏览器本地存的一段明文(或可解码)字符串,如果服务器只靠 Cookie 里的字段判断身份、不做服务端校验,用户就能自己改。把 Cookie 从 user=guest 改成 user=admin 再访问 /info,服务器就以为你是管理员:

Key7 还提示「有一个文件限制了爬虫可以访问的内容」,指的是 robots.txt。它是网站根目录下的一个约定文件,用来告诉搜索引擎爬虫「哪些页面可以抓、哪些别抓」。它不是安全机制,只是「君子协定」,爬虫可以选择遵守、人当然可以无视;但很多开发者会用它把不想被收录的敏感路径写进去,反而成了攻击者的「路标」。访问 /robots.txt,里面披露了提交钥匙的页面 /f13ggggg。
7 个 Key 依次 base64 解码。Base64 是一种编码而非加密:它把任意二进制数据转成 64 个可打印字符(A-Z a-z 0-9 + /,末尾用 = 补位),每 3 字节编成 4 字符。它没有密钥,拿到结果就能原样解回去,所以不能用来保密,只用于让二进制在文本环境里安全传输。识别特征:只含上述字符、长度是 4 的倍数、末尾常有 =:
1 | N2ghNUkkTQ== -> 7h!5I$M |
按 Key1~Key7 顺序拼接:
1 | 7h!5I$M@0m4od3K3yEn+erTh1sla!93tf|1la@4a@g696g9 |
在 /f13ggggg 页面输入这把钥匙提交,猫猫成功回家:

Flag: moectf{7dfe16a8-b12f-e7e5-ba48-658955ad2466}
4. 当零来敲门(50 分)
MoeCTF 2026 · Web 安全与渗透测试
题目:当零来敲门(MoeCTF DevOps 报数大作战)
打开题目是一个「报数大作战」聊天室。页面通过 WebSocket 连接到 /ws 实时接收广播,同时给了一个 SSH 登录凭证,可以直接登录到服务器内部。

先理解一下 WebSocket:它是一种全双工通信协议,建立在 TCP 之上、通过 HTTP 握手升级而来(ws:// 或加密的 wss://)。和传统「请求-响应」式 HTTP 不同,WebSocket 连上后客户端和服务器可以随时互相主动推消息,最适合聊天室、实时行情这类场景。从攻击角度看,这意味着你只要连上它、监听消息,就能收到服务器主动推送的数据(包括发给别人的广播)——本题的 flag 正是通过这条广播下发。
游戏规则:POST /chat 提交 JSON {user, msg},msg = 当前计数 + 1 成功、msg = 0 重置、其它失败。机器人「林林」每隔 10 秒广播一次「0」把计数清零,目标是在下一次清零前让全局计数器连续数到 500。
两个难点:计数是全局共享的,所有人都往同一个计数器上递增,别人报错或清零都会干扰你;公网 + 浏览器 WebSocket 的链路延迟,10 秒内很难稳定完成 500 次严格递增。这里的关键是「竞态条件」——当多个请求并发读写同一份共享状态、且结果依赖先后顺序时,就会出现错乱。你的请求谁先到、谁后到,直接决定成败。
好在题目给了 SSH 账号(challenger / moectf2026)。SSH 是加密的远程登录协议,登录进去后发现 Web 服务就监听在 127.0.0.1:8080。127.0.0.1 是回环地址(localhost),指向本机自身,数据包不经过网卡、不出机器,延迟几乎为 0。在容器内部走回环发起请求,10 秒内完成 500 次递增绰绰有余,也天然避开了公网干扰。
于是写 EXP:在容器内用 aiohttp 异步连发 POST /chat 快速报数,同时监听 /ws 广播捕获 flag。这里用到异步 IO(asyncio)——同步请求是串行的,发一个等一个,等待响应的时间全被浪费;异步则让程序在等待一个请求返回的同时并发处理大量其它请求,大幅提高吞吐。aiohttp 就是 Python 的异步 HTTP 客户端库,500 次请求并发出去,延迟几乎被摊平:
1 | import asyncio, json, aiohttp |
运行后计数很快冲到 500,服务器通过 WebSocket 广播 flag:
1 | FLAG: moectf{PYTHoN-L4nGU@ge-15-so_siMp1E-thaT_wE_CaN-M@ke_soMe-small-and-practical-tools0} |
Flag: moectf{PYTHoN-L4nGU@ge-15-so_siMp1E-thaT_wE_CaN-M@ke_soMe-small-and-practical-tools0}
这题的核心思路是:拿到「内部视角」(SSH 登录进容器)往往比在外部硬打高效得多。
5. 查分系统_revenge(80 分)
MoeCTF 2026 · Web 安全与渗透测试 · 80 分
附件:system_rev.html(静态网页)
题目给的是一个纯静态网页,只有一个 8 位学号输入框。既然是静态页,数据和解密逻辑都在前端,直接离线审计即可。

F12 查看 <script>,关键的解密逻辑如下:
1 | const commonIv = "bW9lY3RmMjAyNml2"; // base64 解码 -> "moectf2026iv" |

这里用的是对称加密 AES:加密和解密用同一把密钥,AES 是当前最主流的对称加密算法,密钥长度 128/192/256 位。对称加密的安全性完全依赖密钥保密——算法本身很难被攻破,最脆弱的环节往往是「密钥从哪来、够不够随机」。本题漏洞恰恰出在密钥派生上。
具体到这段代码,用的是 AES-GCM,一种**认证加密(AEAD)**模式:它除了加密数据(机密性),还会生成一个「认证标签」(Tag,通常是 16 字节)校验密文有没有被篡改(完整性/认证),解密时 Tag 校验不过就会抛错,所以比单纯的 ECB/CBC 更安全。代码里 d.setAuthTag(tag) 设置标签、d.final() 做校验。
再看密钥怎么来的:makeKey(sid) 用 SHA-256 把学号哈希成 32 字节密钥。SHA-256 是单向、雪崩效应强的哈希函数,正好能产出 AES-256 需要的 256 位密钥。这种「把口令变成固定长度密钥」的机制叫密钥派生函数(KDF)。问题在于:口令(学号)的熵太低——只有 8 位数字,空间 10^8 = 1 亿种可能。无论哈希多复杂,攻击者都能穷举所有输入算出所有可能密钥,这就是「弱口令 + 强加密 = 仍可爆破」。
再看 IV:commonIv 被硬编码成固定字符串 moectf2026iv。IV(初始化向量)是加密时的辅助随机量,用来保证「同样的明文 + 同样的密钥,每次密文不同」,它本身不需要保密,但绝不能固定或用可预测的值。这里固定 IV 配合弱密钥,让攻击者可以离线、确定性地遍历所有密钥尝试解密。
综合起来:网页内嵌 92 条 AES-GCM 密文,其中 record 46 明显最长(510 字节,其余约 440 字节),多出来的字段就是 flag,只需爆破这一条。爆破(暴力枚举)就是遍历密钥空间里的每个候选逐个尝试解密,1 亿听着大,但配合多核并行并不难。
完整脚本 solve_system.js。爆破是 CPU 密集型任务(1 亿个候选逐个算 SHA-256 并尝试解密),单线程会很慢,于是用 Node 的 worker_threads 多线程模块把区间切成若干段交给多个 worker 并行爆破、命中即停:
1 | // 查分系统_revenge 解题脚本 |
运行约 6 秒爆破出学号 25178346,解密得到成绩单:

Flag: moectf{y0u_c@n_reAd_th3_htmI_Annmnd_Buwuuurp_1IllIIlIIl11llII}
这题给的安全启示:密钥必须来自足够大的随机熵(比如 2^128 以上),「8 位数字」这类弱口令配上再强的加密也无济于事。
6. 古籍翻阅(100 分)
MoeCTF 2026 · Web 安全与渗透测试 · 100 分
题目:古籍翻阅(西电图书馆藏书无数,出题人将 flag 藏在诸多藏书之中)
打开题目是「天一藏经阁」网站,首页展示 1000 本古籍目录,每本书链接形如 /read?book=zi/zhuangzi/manuscript。

点开任意一本书,返回 404「此卷散佚」(散佚 = 遗失),说明大部分书其实并不存在,只是摆在目录里当障眼法。

关键在 /read 接口的 book 参数——它被直接当作文件路径拼接,没有做任何过滤。观察目录里的路径结构 分类/书名/文件名(如 jing/shijing/scripts、shi/shiji/config),出现了大量 config、src、logs、debug、server、uploads、storage、bin、login 等系统文件名,暗示服务器是把 book 当路径去读文件。
于是想到路径穿越(Path Traversal)。它的成因是:程序把用户可控的输入直接拼进文件路径去读文件时,攻击者通过构造 ../ 这样的特殊路径「跳出」预期目录,读到本不该访问的文件。原理是文件系统里 .. 表示「上一级目录」(相对路径),. 是当前级,多级可叠加成 ../../..;如果服务器代码类似 open("/app/books/" + book),输入 ../../flag 就拼成 /app/books/../../flag,规范化后等于 /flag。
于是尝试向上跳目录读根目录的 flag:
1 | GET /read?book=../../flag |
../../ 向上跳两级(书籍目录约为 /app/books/ 这类两级结构),正好落到容器根目录,读到 /flag:

通过 WSRX 隧道访问时:
1 | curl 'http://<题目地址>/read?book=../../flag' |
(本题需要先启动在线环境,再经平台 WSRX 隧道转发到容器。)
Flag: moectf{re@d_the_r00t_5utr4_w1th_path_tr4v3r5@l}
真实项目里应对路径穿越,常用几招(本题都没做,所以一穿即中):白名单校验(只允许预定义格式、拒绝 . / \)、路径规范化后再判断是否仍在允许目录内、用「映射表」把 ID 映射到真实文件名而非直接当路径用。理解「用户输入被当路径用」这个根因,比记一堆绕过技巧更关键。
7. 河湖小盗(100 分)
MoeCTF 2026 · Web 安全与渗透测试 · 100 分
题目:河湖小盗(网信院往年试题藏在数据库深处,绕过防护拿到往年试题)
打开题目是「网信院往年试题检索系统」,只有一个按序号查询的表单,POST id 到 /。正常查询 id=1 返回「序号 / 登记人 / 公开科目」三列无关紧要的公开记录。

试一下 id=1 OR 1=1,返回了全部 5 条记录,说明存在 SQL 注入——这是最经典、危害最大的 Web 漏洞之一:当程序把用户输入直接拼接进 SQL 语句、而不是用参数化查询时,攻击者能通过构造恶意输入改变 SQL 语句的语义,读取、篡改甚至删除数据。
举这个例子就懂了:服务器本意是执行 SELECT * FROM papers WHERE id = <用户输入>。正常输入 1 得到 WHERE id = 1;但输入 1 OR 1=1 就变成 WHERE id = 1 OR 1=1,而 1=1 恒为真,于是对所有行都成立,返回了全部记录。根因是「把数据和代码混在一起」——输入本应是数据,却被当成了 SQL 代码执行。

这里 id 是数字型注入:参数直接作为数字拼接、不加引号(WHERE id = $id),所以 1 OR 1=1 直接生效,不需要闭合引号。与之相对的字符型(WHERE name = '$name')则需要先补上引号再注释掉后面内容。
接着用 UNION 注入读库。UNION 是 SQL 里「把多个查询结果纵向合并」的操作符,思路就是在原查询后面 UNION SELECT 一个自己构造的查询,把想偷的数据「并」进结果里一起返回。两个前提:列数一致(先探测原查询有几列)、列类型兼容。用 ORDER BY n 逐个试(ORDER BY 4 报错说明只有 3 列)。
之后要枚举数据库结构,这就要靠 information_schema——MySQL 内置的元数据库,存着「数据库自身的结构信息」:有哪些库、表、列。它是注入攻击者的「导航地图」:information_schema.tables 存所有表名(列 table_name),information_schema.columns 存所有列名。version() 返回 8.0.46 确认是 MySQL。
关键 EXP:
1 | -- 确认 3 列,UNION 可用 |
枚举出三张业务表:public_subjects、past_paper、flag_table。flag_table 只有 id 和 flag 两列,直接读出来即可:

Flag: moectf{h3re_i5_your_7e$7_pap3r}





