1. 这类题不是考“黑客技术”,而是考“Web工程师的日常盲区”
“攻防世界 web高手进阶区 2分题”——看到这个标题,很多刚接触CTF的新手第一反应是:又要学SQL注入、XSS绕过、反序列化链了?得翻《Web安全深度剖析》第7章,再配个Burp Suite抓包调半天?我试过,结果卡在第一步就动不了:连题目页面都打不开,或者点提交按钮没反应,更别说找漏洞了。
其实完全想偏了。这类2分题的设计逻辑,根本不是筛选“渗透能力最强”的人,而是精准识别“对Web基础运行机制理解最扎实”的那一小撮人。它不考你能不能写出0day,而考你有没有真正把HTTP协议当“说明书”读过;不考你记了多少Payload,而考你看到一个403响应头时,第一反应是不是去查服务器配置文档;不考你能不能爆破密码,而考你看到/api/v1/user?uid=123这个URL,会不会下意识问一句:“这个123,是数据库主键,还是Redis里的自增ID,还是前端生成的随机字符串?”
我带过几期某高校的CTF实训营,发现一个特别有意思的现象:那些在公司里天天写Vue+Spring Boot、能独立上线中后台系统的开发同学,往往比专门学过渗透测试的学员更快解出2分题。为什么?因为他们每天都在和Cookie域、SameSite策略、CSP头、Referer校验、Content-Type解析逻辑打交道——这些不是“攻击面”,而是他们写登录态管理、文件上传、跨域接口时,必须亲手填平的坑。而2分题,就是把这些“填坑过程”反过来设计成一道题。
比如一道典型2分题:页面显示“欢迎回来,[用户名]”,但右键查看源码找不到用户名明文,Network面板里所有请求返回都是200,也没有明显JSON数据包。很多人立刻开始怀疑XSS或DOM型漏洞。但真正解法是:打开Application标签页,点开Cookies,发现有个user_token=xxx,Base64解码后是{"name":"admin","role":"user"},而修改role为admin再发包,权限就变了。这题考的不是Base64解密,而是“前端是否信任本地存储”这个工程常识。
所以别被“攻防”二字吓住。把它当成一份Web开发岗位的实操面试题来看——考的是你对浏览器怎么发请求、服务器怎么回响应、中间件怎么过滤、缓存怎么生效这些链条上每个环节的肌肉记忆。下面我就按真实解题动线,拆解这类题从打开页面到提交flag的完整闭环。
2. 第一关:用开发者工具“看穿”页面,而不是“猜漏洞”
拿到一道2分题,绝大多数人会立刻打开Burp Suite或Fiddler,等着抓包看哪里有参数可改。但这是效率最低的做法。真正的起点,永远是浏览器原生的开发者工具(DevTools),而且要从Elements和Application两个标签页切入,不是Network。
2.1 Elements页签:别只盯着HTML结构,重点看“看不见的属性”
很多人用Elements只是右键“Edit as HTML”改文字,这完全浪费了它的价值。真正要盯的是三类隐藏信息:
第一类:被注释掉的调试信息
比如在<script>标签末尾看到:
<!-- DEBUG: api_base_url = /v2/internal/ --> <!-- DEBUG: feature_flag = {"enable_waf": false, "mock_data": true} -->这种注释在生产环境绝不会出现,但在2分题里就是明晃晃的提示。它直接告诉你:后端接口路径在/v2/internal/下,且WAF(Web应用防火墙)当前是关闭状态——这意味着你可以放心用常规Payload测试,不用花时间绕过规则。
第二类:内联JavaScript里的硬编码逻辑
比如一段代码:
const token = localStorage.getItem('session_id'); fetch('/api/profile', { headers: { 'X-Auth-Token': btoa(token + '_2024') } });这里暴露了两个关键点:认证Token存在localStorage里,且服务端要求在原始token后拼接_2024再Base64编码。如果你只看Network里发出去的请求头,看到的是X-Auth-Token: dG9rZW4xMjM=_2024,但根本不知道_2024是固定后缀。而这段JS直接把规则写死了。
提示:按Ctrl+F搜索关键词
debug、test、dev、flag、secret,能快速定位这类注释。搜索localStorage、sessionStorage、cookie能发现客户端存储逻辑。
第三类:表单元素的隐藏约束
比如一个登录表单:
<input type="text" name="username" maxlength="16" pattern="[a-zA-Z0-9_]+" required> <input type="password" name="password" minlength="8" pattern="(?=.*[A-Z])(?=.*\d)">表面上看是前端校验,但2分题里往往暗示后端也做了同样校验。如果尝试提交admin' OR '1'='1,表单直接报错“用户名包含非法字符”,说明后端正则和前端一致。这时候你就该放弃SQL注入思路,转去检查pattern规则是否有可利用的边界(比如[a-zA-Z0-9_]+允许下划线,而某些ORM会把user_name解析为字段名)。
2.2 Application页签:把浏览器当“数据库”来查
Network标签页只告诉你“发生了什么”,Application页签才告诉你“为什么发生”。它有四个核心区域必须逐个检查:
Cookies区域:重点看Domain和Path
比如看到一个Cookie:
Name: session_id Value: abc123def456 Domain: .example.com Path: /admin/ HttpOnly: true Secure: true SameSite: StrictDomain: .example.com意味着这个Cookie会被发送到api.example.com、static.example.com等所有子域名;Path: /admin/说明只有访问/admin/开头的路径才会携带;SameSite: Strict表示跨站请求(比如从恶意网站发起的POST)绝不会带上这个Cookie——这直接排除了CSRF攻击的可能性。而HttpOnly: true则告诉你,JavaScript无法读取这个值,所以XSS利用难度陡增。
Storage区域:不止看localStorage,更要查IndexedDB
很多2分题把关键数据存在IndexedDB里。比如打开Storage → IndexedDB → 点击数据库名 → 查看object store,可能发现一个叫user_cache的表,里面存着{id: 1, name: "admin", token: "flag{...}"}。这时候你不需要任何网络请求,直接在Console里执行:
indexedDB.open("myapp_db").onsuccess = e => { const db = e.target.result; const tx = db.transaction("user_cache"); const store = tx.objectStore("user_cache"); store.get(1).onsuccess = e => console.log(e.target.result); };就能把flag打印出来。这题考的不是数据库知识,而是“你知不知道现代浏览器连本地数据库都有”。
Cache区域:别忽略Service Worker的缓存策略
如果页面注册了Service Worker,点开Cache,可能看到缓存了/api/config.json,内容是:
{ "debug_mode": true, "backend_url": "http://127.0.0.1:8080", "feature_toggles": ["show_flag_in_console"] }show_flag_in_console这个开关,就是让你在Console里输入getFlag()就能弹出flag的线索。而backend_url指向本地地址,暗示后端服务可能就在同一台机器上,为后续SSRF(服务器端请求伪造)埋下伏笔。
注意:Application页签里的所有数据,都是浏览器实际使用的状态。如果你改了Cookie但没刷新页面,新值不会生效;如果你清空了localStorage但没重启Service Worker,旧缓存依然存在。解题时务必养成“改完就刷新”的习惯。
3. 第二关:用HTTP协议本身做“探测器”,而不是盲目发包
当Elements和Application没找到突破口,很多人就开始用Burp Suite暴力扫目录、爆破参数。但2分题的精妙之处在于:它把答案藏在HTTP协议最基础的交互细节里,而不是靠海量请求堆出来。
3.1 从Status Code读懂服务器的“潜台词”
HTTP状态码不是冷冰冰的数字,而是服务器给你写的“使用说明书”。2分题里,4xx和5xx错误码往往比200更有价值。
403 Forbidden:不是“没权限”,而是“权限校验逻辑有漏洞”
比如访问/admin/dashboard返回403,但访问/admin/../dashboard却返回200。这说明服务器用了不安全的路径拼接,比如PHP里include($_GET['page'] . '.php'),而没做规范化处理。这时候你应该尝试:
/admin/./dashboard(当前目录)/admin/%2e%2e/dashboard(URL编码)/admin%2f%2e%2e%2fdashboard(双重编码)
但更关键的是:为什么/admin/../dashboard能绕过?因为中间件先做了权限检查(看到/admin/就放行),再交给路由处理(此时..被解析,实际访问了/dashboard)。这暴露了“鉴权与路由分离”的设计缺陷。
405 Method Not Allowed:不是“方法禁用”,而是“方法切换能触发不同逻辑”
比如对/api/user发GET返回用户信息,发POST返回405。但如果你发HEAD请求,服务器却返回200,且响应头里有X-Debug-Info: flag{...}。这是因为HEAD请求只返回响应头,不返回响应体,而开发者为了调试方便,在HEAD处理逻辑里偷偷加了调试信息。这题考的是你有没有把HTTP方法当“功能开关”来思考。
500 Internal Server Error:不是“服务器崩了”,而是“错误信息泄露了技术栈”
比如返回:
Traceback (most recent call last): File "/app/routes.py", line 42, in user_profile user = User.query.filter_by(id=request.args.get('id')).first() File "/venv/lib/python3.9/site-packages/sqlalchemy/orm/query.py", line 3570, in first return self.limit(1).all()[0] if self.limit(1).all() else None IndexError: list index out of range这段Python+SQLAlchemy的报错,直接告诉你:后端用的是Flask框架,数据库是SQLAlchemy ORM,且request.args.get('id')没做类型转换。这时候你该尝试传id=abc,看是否会触发SQL语法错误(证明是直连数据库),或者传id=1 or 1=1看是否返回所有用户(证明是ORM查询)。
3.2 从Response Headers发现“被遗忘的配置”
响应头是服务器最诚实的自白。2分题里,Server、X-Powered-By这些头往往被忽略,但它们指向最脆弱的环节。
X-Content-Type-Options: nosniff
这个头本意是防止MIME类型嗅探,但如果你发现它缺失,就意味着浏览器可能把/upload/test.jpg当作HTML执行。这时候上传一个内容为<script>alert(1)</script>的图片文件,再访问该URL,就可能触发XSS。这题考的不是上传漏洞,而是“你知不知道浏览器的默认行为”。
Content-Security-Policy(CSP)头的绕过线索
比如CSP头是:
default-src 'self'; script-src 'self' 'unsafe-inline'; img-src *'unsafe-inline'允许内联脚本,但img-src *意味着可以加载任意域名的图片。这时候你可以构造一个图片URL:https://evil.com/xss.png?callback=<script>alert(document.cookie)</script>
然后在页面里插入:
<img src="https://evil.com/xss.png?callback=<script>alert(document.cookie)</script>">虽然<script>标签被CSP阻止,但<img>标签的src属性里如果包含可执行代码(取决于服务端如何解析参数),就可能绕过。这题考的是你对CSP各指令作用域的理解。
X-Frame-Options: DENY
这个头禁止页面被嵌入iframe,但如果你发现它不存在,就可以尝试点击劫持(Clickjacking)。比如构造一个透明iframe覆盖在“删除账户”按钮上,诱导用户点击。而2分题里,往往只需要在iframe里加载目标页面,再用CSS把iframe设为opacity:0.001,就能完成攻击——因为它考的是“你知不知道这个头的作用”,而不是真要你写攻击页面。
实测心得:用curl命令快速抓取响应头比开Burp更快。比如
curl -I http://target.com/api/user,一眼就能看到所有头。遇到复杂场景,再开Burp详细分析。
4. 第三关:把“业务逻辑”当漏洞来读,而不是只盯技术点
2分题的终极陷阱,是让你在技术细节里打转,却忽略了页面上最显眼的业务描述。它考的不是“你会不会黑”,而是“你有没有像产品经理一样读需求”。
4.1 从UI文案里挖出“功能开关”
比如页面顶部有一行小字:“当前版本:v2.3.1(Beta版,仅对VIP用户开放)”。很多人扫一眼就过了。但这句话里藏着三个线索:
- “Beta版”意味着功能可能未完善,比如日志没清理、调试接口没关闭;
- “VIP用户”暗示系统有会员体系,应该去找
/vip/、/premium/等路径; - “v2.3.1”是版本号,可以尝试访问
/CHANGELOG.md、/version.txt,或者用Git Dumper工具扒源码(如果网站用Git部署且.git目录可访问)。
我遇到过一道题,页面写着“支持语音输入(测试中)”,点麦克风图标没反应。但查看Elements发现,按钮绑定了onclick="startVoiceInput()",而全局搜索startVoiceInput函数,发现它调用了navigator.mediaDevices.getUserMedia({audio: true})。这时候你应该想到:这个API需要HTTPS,而题目环境是HTTP。于是把URL改成https://再试,麦克风就工作了——而语音识别返回的结果里,就包含了flag。
4.2 从功能流程图里找到“逻辑断点”
很多2分题会提供一个“用户操作流程图”,比如:
注册 → 邮箱验证 → 设置安全问题 → 登录 → 查看个人资料但实际页面上,“设置安全问题”这一步是灰色不可点的。这时候不要急着找前端JS禁用逻辑,先检查Network:发现注册成功后,服务器返回的JSON里有"step": "email_verified",而邮箱验证接口返回的是"step": "security_question_set"。这说明后端用step字段控制流程。如果你手动发一个请求到邮箱验证接口,但把step参数改成security_question_set,就可能跳过验证直接进入下一步——而下一步的表单里,往往有<input type="hidden" name="flag" value="flag{...}">。
4.3 从错误提示语里提取“正则表达式”
比如登录失败时提示:“用户名只能包含字母、数字和下划线,长度6-16位”。这看起来是前端校验,但2分题里,后端必然有相同正则。你可以用这个正则去反推:
- 如果正则用的是
^[a-zA-Z0-9_]{6,16}$,那么admin_是合法的,而admin.不合法; - 如果正则用的是
^[a-zA-Z0-9_]+$(没限制长度),那你可以尝试超长用户名触发缓冲区溢出(虽然Web应用少见,但2分题里可能出现); - 如果正则用的是
^[a-zA-Z][a-zA-Z0-9_]{5,15}$(首字母必须是字母),那123abc就不合法,但a123bc合法——这可能影响你爆破用户名的字典选择。
更关键的是:错误提示里说“只能包含”,意味着其他字符会被截断或过滤。比如输入admin'--(带空格),提示变成“用户名包含非法字符”,但输入admin'--(无空格),提示变成“密码错误”。这说明空格被当作了SQL注入的终止符,而--被当作了注释符——后端SQL语句可能是SELECT * FROM users WHERE username = '$username' AND password = '$password',而admin'--让语句变成SELECT * FROM users WHERE username = 'admin'--' AND password = 'xxx',从而绕过密码校验。
经验总结:把每一条UI提示、每一个按钮文案、每一段帮助文字,都当成需求文档来读。我在某次模拟项目X中,就是靠页面底部一行小字“技术支持邮箱:help@internal.local”找到了内部DNS服务器,最终通过DNS重绑定(DNS Rebinding)攻击获取了flag。
5. 第四关:用“最小必要知识”组合解题,而不是堆砌工具
很多新手解不出2分题,不是因为不会用工具,而是太依赖工具。他们装了Burp Suite、SQLMap、Nmap,却忘了Chrome按F12就能解决80%的问题。真正的高手,是用最基础的工具,打出最精准的组合拳。
5.1 Burp Suite的“极简用法”:只开Proxy和Repeater
我不建议新手一上来就开Intruder、Scanner、Sequencer。对2分题,Burp只要两个功能:
- Proxy:把浏览器流量导入Burp,用来观察原始请求/响应;
- Repeater:对单个请求反复修改、重发,验证猜想。
比如发现一个参数?id=1,在Proxy里截获后,右键→Send to Repeater。在Repeater里把id=1改成id=1',点Go,看响应是否报错;再改成id=1 and 1=1,看响应是否和id=1一样;再改成id=1 and 1=2,看响应是否不同。三步就能判断是否存在布尔型SQL注入。整个过程不到30秒,比跑SQLMap快十倍。
5.2 curl命令的“三板斧”:-I、-v、-X
终端里敲curl,比开图形化工具更高效:
curl -I URL:只看响应头,检查CSP、X-Frame-Options等;curl -v URL:显示完整请求/响应过程,包括重定向链;curl -X POST -d "user=admin&pass=123" URL:发POST请求,测试表单逻辑。
比如一道题要求“上传ZIP文件”,但页面没给上传入口。用curl -v http://target.com/upload,发现服务器返回302重定向到/upload/form,而/upload/form返回403。这时候再用curl -I -H "Referer: http://target.com/admin",发现返回200——说明Referer校验被绕过。这就是用-v和-I组合发现的逻辑漏洞。
5.3 浏览器Console的“万能调试法”
当页面JS逻辑复杂,看不懂时,别急着上调试器。在Console里执行三行代码:
// 1. 查看所有已定义的函数 Object.keys(window).filter(k => typeof window[k] === 'function') // 2. 查看所有事件监听器(针对某个元素) getEventListeners(document.querySelector('#login-btn')) // 3. 拦截fetch请求,看它到底发了什么 const originalFetch = window.fetch; window.fetch = function(...args) { console.log('FETCH:', args); return originalFetch.apply(this, args); };这三行代码,能让你在1分钟内掌握页面的全部JS行为。我用这个方法,在一道“点击按钮无反应”的题里,发现按钮绑定了onclick="checkPermission(); submitForm();",而checkPermission()函数里有一行if (location.hostname !== 'admin.example.com') return false;——原来它强制要求域名是admin.example.com。把本地hosts文件加上127.0.0.1 admin.example.com,问题就解决了。
最后提醒:所有工具都是辅助,核心永远是你的思考链路。我见过太多人,Burp里开了10个tab,Intruder跑了2000次,却没花10秒钟去看一眼Application页签里的Cookie。记住,2分题的设计者,是在考你“作为Web工程师的本能反应”,而不是“作为渗透测试员的工具熟练度”。