
封神台靶场里最让我惦记的关卡就是“为了小芳”。注册完账号首页弹出来一条公告大意是小芳被不明身份的人带走了留言板里有人起哄说管理员手里有线索但普通会员的权限根本点不进去。我顺着页面把个人中心、留言板、修改资料都翻了一圈URL干干净净没有任何?id之类的参数于是老老实实开了 Burp Suite 抓包。请求发出去一看Cookie 里明晃晃躺着一个id2——到这儿我就明白了这一关要玩的是 Cookie 注入。后面我会把这关的完整思路写下来怎么确认注入点、怎么判断字段数和回显位、怎么把管理员账号密码拖出来以及登录后台之后怎样完成“救小芳”这个目标。如果你是刚接触注入的新手这会是一份能直接照着操作的路线图如果你已经会常规 SQL 注入那重点可以放在 Cookie 这个入口带来的各种细节差异上。毕竟把注入点从 URL 挪到 Cookie 里踩坑的点还真不太一样。1. 这关的剧情有点上头小芳的线索藏在后台1.1 封神台里最有人情味的一个关卡封神台靶场把每个关卡做成了封神榜题材的小剧场这个“为了小芳”的关卡尤其会撩人。一进去先弹页面公告大意是小芳失踪了管理员手里有目击者留言但只有管理员能看希望大家提供线索。下面还有几条“路人”留言有人问“小芳是谁”有人喊“管理员出来走两步”气氛一下子就起来了。靶场练习很多时候是冷冰冰的“能不能拿下”但这个关卡给了一个非常明确的剧情出口你得拿到管理员权限进后台找到那条关于小芳的记录才算过关。它不是让你把数据库删了、网站拖垮而是让你沿着“普通用户 → 管理员后台”这条路径走一遍本质上是一道很典型的 Web 权限提升题。我当时的操作路径是先注册一个普通账号然后翻功能点。个人中心能看到自己的用户名和注册邮箱还有一个“我的留言”页面。问题来了这几个页面的地址栏都没有参数比如/personal.php、/messages.php干干净净完全不像有 SQL 注入的地方。经验告诉我参数不一定都在 URL 里可能在 POST 表单里也可能被塞进了 Cookie。于是我把请求包翻出来果然在 Cookie 里看到Cookie: PHPSESSIDxxxxxx; id2这个id2一下就扎眼了。普通功能点上的“我的留言”应该就是靠这个 id 去查当前用户的数据如果后端直接拿它拼 SQL那就等于把一个注入点从 URL 悄悄转移到了 Cookie 里面。1.2 第一眼看上去不像注入点的思维误区很多人在做注入题时有个习惯先在 URL 后面加?id1去试没反应就换参数、扫目录甚至直接开 sqlmap。这个习惯本身没问题但容易漏掉一类场景——参数根本不在 URL。这就是 Cookie 注入的隐蔽性所在页面表现完全正常地址栏干净WAF 日志里也只有正常的访问记录如果不懂抓包可能永远找不到注入点。所以我把这个关卡的心得放在开头遇到“没有参数”的动态页面先想三件事——请求里有没有 Cookie 携带自定义参数有没有 POST 表单没注意到的隐藏字段响应页面里有没有调用其他接口这三样都没问题再考虑扫目录、撞后台。2. Cookie注入到底是什么别把入口当突破口2.1 参数在哪里注入点就在哪里SQL 注入的本质是程序把用户输入直接拼进了 SQL 语句导致输入中的特殊字符改变了原本的查询逻辑。很多人只记住了“用户输入”却默认“用户输入 URL 参数”这是一个很大的误区。实际上 HTTP 请求里能带数据的位置很多URL 查询串、POST 请求体、文件上传内容、Referer、User-Agent以及最容易被忽略的 Cookie。Cookie 是浏览器自动携带的键值对集合服务端拿它的方式通常和拿 GET/POST 参数一样方便。PHP 里有$_COOKIEJava 里有request.getCookies()如果开发人员偷懒就会直接把 Cookie 里的值丢进 SQL。Cookie 注入和普通注入没有本质区别SQL 语句还是那个 SQL 语句数据库还是那个数据库唯一的区别是“入口”从 URL 换成了 Cookie。入口不同导致两个连锁变化一是检测工具和绕过思路要跟着变二是很多只过滤 GET/POST 参数的安全设备会在这个入口上放行。2.2 一段典型的缺陷代码长什么样为了把原理说透我模拟一下这类关卡常见的后端逻辑。假设“我的留言”页面的 PHP 代码长这样$id $_COOKIE[id]; $con mysqli_connect(localhost, root, pass, zkaq); $sql select username, email from users where id $id; $result mysqli_query($con, $sql);注意$id是从$_COOKIE[id]取出来的没有经过任何过滤直接拼进字符串。此时如果我让 Cookie 变成Cookie: id2 and 11那么拼出来的 SQL 就是select username, email from users where id 2 and 11因为11恒真查询正常返回。如果我改成id2 and 12就变成了恒假条件什么也查不出来。页面是否显示数据就成了判断注入是否成立的标准。这就是字符型注入的常见闭合姿势为了让多出来的单引号不破坏整个语句要么用and 11补一个引号配对要么用注释符把后面的内容注释掉。后面的所有操作其实都是在处理“单引号闭合”这件事。2.3 为什么不少安全设备会漏掉 Cookie 注入我在实际测试和防御两侧都待过对 Cookie 注入的“漏报体质”印象很深。很多 WAF 的规则库重点检查 URL 参数、POST 表单内容对 Cookie 的检查要么直接跳过要么只做基础的关键词匹配比如检测union、select这种字符串但没有结合 Cookie 的整体结构去解析。攻击者稍微改一下大小写、加注释拆分、URL 编码就能让规则失效。更现实的一点是Cookie 是分号分隔的多个键值对有些 WAF 解析 Cookie 的时候没有按分号做严格切割可能把空格、编码字符都混淆进去。所以 Cookie 注入在实战中经常是“突破防御的最后一根稻草”。当然靶场环境没有 WAF 拦在中间它给我上的这堂课是永远不要只盯着 URL 参数。3. 抓包改Cookie从第一次试探到锁定注入点3.1 让 Burp Suite 先接管流量前面说到我在 URL 里找不到参数但心里已经盯上了 Cookie。接下来的活儿就顺理成章了打开 Burp Suite配置好浏览器代理访问封神台靶场里那个“我的留言”页面。因为页面本身有登录态访问时会自动带上会话 Cookie再加上那个id2。在 Burp 的 Proxy → HTTP History 里我能清楚地看到整个请求。右键选择 Send to Repeater把请求固定下来后面所有的注入测试都在 Repeater 里做。为什么要用 Repeater因为我要反复修改 Cookie 的值如果用浏览器直接改 Cookie 再刷新速度慢而且容易被浏览器自带的功能干扰。Repeater 里每改一次点一下 Go响应立刻回来效率高很多。这里有个小提醒Burp 里修改 Cookie 时手要稳不要把后面的PHPSESSID一起删了否则会话失效测试结果会变成“未登录”而不是我们想要的注入结果。我在打关卡时犯过这个低级错误页面突然变成登录跳转我愣了几秒才发现是改包时多删了一个分号。3.2 单引号那一哆嗦确认请求没问题后我开始做第一轮试探。原始 Cookie 是Cookie: PHPSESSIDxxxxxx; id2第一步只改 id加一个英文单引号Cookie: PHPSESSIDxxxxxx; id2点击 Go响应直接扔回来一个数据库报错。报错信息里出现了 SQL 语句的片段能明显看到where id 2这种结构。到这一步我心里已经有底了后端确实把 Cookie 里的 id 拼进了 SQL 查询而且闭合方式就是单引号。单引号报错不一定代表一定能注入还要看报错后的语句和业务状态。但至少说明输入确实进入了 SQL 执行环境接下来需要验证“能不能用逻辑条件控制查询结果”。这一步非常关键也是判断注入类型数字型还是字符型的依据。3.3 用逻辑真假代替瞎猜有了报错信息接下来验证注入是否成立。我把 Cookie 改成Cookie: PHPSESSIDxxxxxx; id2 and 11发送后页面正常显示当前用户“小张”的留言列表。紧接着改成Cookie: PHPSESSIDxxxxxx; id2 and 12这次页面返回空白或者提示“没有数据”。一真一假结果完全不同这就说明注入条件可控。这里解释一下为什么要写成and 11而不是直接写and 11。因为原 SQL 本身带了一个前单引号where id 2我注入 and 11后完整语句变成where id 2 and 11前引号和 my id 的单引号配对and 11是布尔条件最后和原语句末尾的引号配对。整条语句语法完美闭合。如果我不小心在原语句末尾留了一个游离的单引号后面所有的 union 查询都会因为语法错误而失败。这就是字符型注入最需要关注的问题闭合。到了这里注入点正式锁定。接下来要做的就是把数据库里的信息一点一点“问”出来。4. order by探字段、union查回显一步步把数据库“问”出来4.1 字段数从 order by 1 试到 order by 5确认注入后第一步是判断查询结果有几列。最常用的方法是order by它会按指定列排序如果指定的列号超出了实际查询列数数据库就会报错。这个报错信息非常直观。我把 Cookie 依次改成Cookie: PHPSESSIDxxxxxx; id2 order by 1%23 Cookie: PHPSESSIDxxxxxx; id2 order by 2%23 Cookie: PHPSESSIDxxxxxx; id2 order by 3%23 Cookie: PHPSESSIDxxxxxx; id2 order by 4%23前三次返回都正常到order by 4时页面报错提示Unknown column 4 in order clause。这说明原本的 SELECT 语句只查询了 3 列。这个 3 很关键因为后面构造 union select 时必须保证列数一致。这里插一句我在 Cookie 里用的是%23也就是#的 URL 编码。原因在于字符型注入闭合后我要把后面多余的引号注释掉否则 SQL 语法不完整。#在 MySQL 里是注释符URL 编码成%23是为了避免服务器或代理把它当特殊字符处理。用--在某些环境下也行但加号有时候会被解析成空格不如%23稳。4.2 回显位负ID加 union select 的固定套路知道了查询列数是 3 列接下来要找到页面上到底回显了哪些位置。这一步的标准姿势是把原来的 id 改成不存在的负数同时用 union select 自己造一行数据Cookie: PHPSESSIDxxxxxx; id-1 union select 1,2,3%23为什么是-1因为正常情况下数据库里没有 id 为 -1 的用户原始查询返回空结果union 后面的查询结果就能直接显示出来。如果我还用id2 union select ...原始查询已经有一条数据了页面可能优先显示原数据union 的数据要么被覆盖要么混在里面观察起来会很吃力。发送后页面上出现了“2”和“3”位置分别在用户名和邮箱那一栏“1”没有显示。这说明模板只把查询结果的第 2 列和第 3 列渲染到了页面上。回显位就是 2 和 3接下来所有拖库的数据都可以放在这两个位置上。有经验的人看到这里应该心里有数了这个关卡的显示逻辑就是把查询结果的几个字段挨个填进页面的模板位置。我的目标是拿到管理员账号密码所以接下来的 payload 基本都是union select 1,目标信息,3的格式把目标信息放在第 2 或者第 3 的位置上就行。4.3 拖库三步走查库、查表、查字段回显位确认后拖库是水到渠成的事。第一步先看当前数据库名和版本把第 2 个位置换成database()Cookie: PHPSESSIDxxxxxx; id-1 union select 1,database(),3%23页面回显一个数据库名我记得这一版环境里是zkaq。换到第 3 个位置看version()回显了 MySQL 的版本号。知道版本后后面查 information_schema 的语法就有底了。第二步查当前库里有哪些表。用group_concat把表名一次性拼成一串Cookie: PHPSESSIDxxxxxx; id-1 union select 1,group_concat(table_name),3 from information_schema.tables where table_schemadatabase()%23页面返回一串表名里面通常有admin、news、message之类。我这里一眼就看到了admin目标基本锁定。第三步查 admin 表里的字段Cookie: PHPSESSIDxxxxxx; id-1 union select 1,group_concat(column_name),3 from information_schema.columns where table_nameadmin%23回显字段名类似id,username,password这样的组合。看到 password 字段就知道离管理员密码不远了。第四步直接把数据带出来。为了避免用户名和密码连在一起分不清我用concat加一个分隔符Cookie: PHPSESSIDxxxxxx; id-1 union select 1,concat(username,0x7c,password),3 from admin%230x7c是竖线|的十六进制写法这样页面上显示的就是admin|e10adc3949ba59abbe56e057f20f883e这种格式。前面是管理员账号后面是 32 位的 MD5 密文。我把数据记下来第一阶段的注入到此完成。5. 拿到管理员密码之后md5撞库和后台那些事5.1 32位字符串不是终点很多新手拿到 32 位密文后就开始兴奋以为马上要通关了结果发现这串字母数字根本没法直接拿来登录。32 位 hex 字符串有很大概率是 MD5但也可能是其他算法的输出。判断方法很简单长度 32 位、字符范围是0-9a-f优先按 MD5 处理长度是 40 位则要考虑 SHA1如果是 64 位考虑 SHA256。这一关的密码长度是 32 位我直接按 MD5 处理。MD5 虽然理论上不可逆但大量弱口令的 MD5 早就被预计算好了。我的做法是拿到密文后先去公共的 MD5 查询站点撞库输入密文秒出明文。当时解出来的明文是一个很典型的弱口令比如admin888或者123456这类。这里吐个槽只要密码是弱口令不管 MD5 还是 SHA256撞库就是分分钟的事。如果撞库撞不出来怎么办有几个备选思路一是换更大的字典在本地用 hashcat 跑跑 MD5 的速度非常快弱口令秒出二是回数据库继续拖其他用户的数据或者看有没有其他表的密码更弱三是检查后台有没有“记住密码”或者“找回密码”功能看能不能通过重置逻辑配合注入绕过去。不过在这类新手关卡里管理员密码基本不会太狠。5.2 找后台入口拿到账号密码下一步是找到后台登录页面。这个往往是新手容易卡住的地方因为前台页面上不会明晃晃放一个“管理后台”的链接。我当时的做法是先看页面源码搜索admin这个词有的模板会在注释里留下后台路径。另外常见后台路径就那几个admin.php、admin/login.php、manage.php、system.php直接访问试一遍成本很低。封神台这个关卡的后台入口我记得是admin.php打开后是一个简洁的登录框输入刚才解出来的用户名和密码一次就进去了。这里没有验证码也没有登录失败次数限制放到真实环境里非常危险。靶场这么设计也是在暗示“后台登录防护薄弱”是多么常见的漏洞点。5.3 后台里的小芳登录后台后我看到的不是想象中的“网站管理大屏”而是一个很简单的管理页面导航里有“留言管理”“会员管理”“公告管理”几个入口。我点开留言管理里面有几条被标记为“管理员可见”的留言其中一条恰好提到了小芳的下落线索。到这里关卡的剧情目标就算达成了找到这条线索页面给出了过关提示。不过我还额外看了一眼会员管理里面能看到所有注册用户的账号和邮箱甚至还有密码字段虽然显示的是脱敏后的星号。这个信息在真实渗透里非常值钱因为很多用户会复用密码拿到会员列表后可以横向尝试其他系统。但靶场关卡到此结束我也就没有继续深入。整个过程中我可以明显感受到这个关卡的教学意图Cookie 注入只是入口后面能不能找到后台、能不能解出密码、能不能在正确的位置找到剧情线索考察的是完整的攻击链思维。只会在 URL 里拼接union select是不够的还得知道下一步做什么。6. 复盘Cookie注入比普通注入多踩的坑6.1 那些让我卡住的细节这个关卡打下来不算难但有几个细节确实让我反复试了好几次这里一并写出来希望大家能少走弯路。第一个是 Cookie 里的分号问题。HTTP 协议用分号分隔不同的 Cookie 键值对所以如果注入语句里出现分号比如某些多语句注入写; select ...就可能被客户端或服务器错误拆分。我测试时所有的 union 查询都不用分号结尾MySQL 本来也不要求单条语句写分号所以没有踩这个坑但如果你习惯在 SQL 语句末尾加一个分号这个习惯在 Cookie 注入场景下最好收一收。第二个是 Cookie 里的注释符。我一开始用的是--这是 GET 型注入的常见写法但在 Cookie 里加号可能会被某些后端解析成空格导致注释没有生效SQL 语句结尾残留了一个多余的引号报错报得莫名其妙。换用%23之后全部畅通。所以我的建议是只要是站在 Cookie 这个入口做注入注释符统一用%23省心。第三个是手滑误改 Cookie 结构。有些同学在 Burp 里用界面化的“Cookie 编辑器”改值改完以后把id2 union select 1,2,3%23整个塞进去结果把后面的PHPSESSID给挤掉了发送以后跳转登录页还以为是注入失败。这种情况我在教学群里见得太多了。我的习惯是直接在原始报文里手动改 Cookie 那一行改完立即看一眼分号有没有保留。6.2 工具很好用但脑子更重要打这个关卡的时候我没有全程用 sqlmap。手动注入做完之后我也拿 sqlmap 测了一遍主要想验证一下工具在这个场景下的表现。实测发现sqlmap 默认的 Level 1 只测 GET 和 POST 参数不会主动测 Cookie所以直接跑会“无疾而终”。必须加上--level 2或者手动指定注入点比如sqlmap -u http://target/messages.php --cookie id2 --level 2 --batch或者直接在 Cookie 里用*标记注入点sqlmap -u http://target/messages.php --cookie id2* --batch这样工具才会去测 Cookie 这个位置。不过我想强调一句工具能帮你省时间但不能替代理解。手动走一遍你才能明白什么是闭合、为什么注释要编码、为什么负数 id 加 union 能稳定回显。只依赖工具的话一旦碰到 sqlmap 识别不了的场景比如自定义的闭合方式或复杂的 WAF就会非常被动。手动过程积累的直觉才是能带到真实环境里的东西。6.3 防御侧的一点反思打完这个关卡站在防守方角度重新看这个问题Cookie 注入完全是可以避免的。最核心的一点就是 Cookie 里的用户标识不能直接参与 SQL 拼接。开发时应该先对 Cookie 中的值做严格的类型校验比如判断id必须是纯数字然后使用参数化查询或预处理语句这样即使输入里带引号、注释符也只会被当成普通字符串数据永远不会改变 SQL 语句结构。另外后台登录页面的防护也要到位。弱口令、无验证码、无频率限制这三个问题叠加在一起本身就等于给攻击者开后门。如果管理员密码再是“admin888”“123456”这种字典常见项前面所有注入工作都可以直接简化成“暴力破解”四个字。对真实业务来说后台登录至少要上验证码和防爆破策略密码策略也要强制复杂化。安全设备层面同样需要把 Cookie 纳入检测范围。很多 WAF 对 URL 和 POST 参数查得很严对 Cookie 却睁一只眼闭一只眼。攻击者只要把注入语句挪进 Cookie就能轻松绕过一票规则。正确做法是像解析 GET 参数一样解析 Cookie 的键值对再对每个值做同样的危险函数匹配和语义分析。最后再分享一个我自己的习惯每次打完靶场我都会把“如果我是开发人员这段代码要怎么修”写在笔记里。攻击思路是矛防御修复是盾两边都摸过一遍才算真正吃透一个漏洞。封神台这个“为了小芳”的关卡虽然技术上只是基础的 Cookie 注入但它把完整的攻击链、剧情和解密环节串了起来整个过程更像一次真实的业务渗透演练这比单纯刷一道注入题有意思得多。