“为了小芳,冲之”——这句话我在封神台靶场某个题目里看到的时候,第一反应是笑了一下,第二反应是这题肯定没那么简单。果不其然,题目页面干干净净,URL参数里连个 id 的影子都找不到,折腾了半天才反应过来:这一次注入点根本不在 URL 上,而是在 Cookie 里。
cookie注入,是很多新手从“只会 URL 拼 SQL”迈向下一个台阶时最容易卡住的点。封神台这个靶场题把它包装成了“解救小芳”的剧情任务,实际上考的就是:你能不能跳出常规思维,去服务端信任的 Cookie 里找漏洞。这篇文章我会从原理、检测、手工注入实操到踩坑排查,完整走一遍这道题的解题链路。想练习 Web 安全、刚学完 SQL 注入基础、或者正在刷靶场卡在 Cookie 注入上的朋友,都可以照着这份思路自己复现一遍。
1. 项目由来与目标分析:一道藏在剧情里的 Cookie 注入题
1.1 封神台靶场:为什么适合拿来练手
封神台是很多安全爱好者早期刷题会用的在线靶场,它的特点在于“任务制+剧情化”。和那些冷冰冰只给你一段代码的练习平台比,封神台的题目更像是在玩一个解谜游戏,每道题都有一个背景故事。比如这道题里的“小芳”,就承担了两个作用:一是给枯燥的注入过程加点代入感,二是提示你这题的数据大概和“人员信息”“用户档案”有关。
从学习路径来看,这类靶场的难度分级做得比较清晰。刚开始的题目通常会把注入点放在 URL 参数里,你用 sqlmap 或者手工 union 注入三板斧就能搞定。但越往后,出题人就越喜欢“藏”,把参数藏进 Cookie、藏进 User-Agent、藏进 Referer,甚至藏进 POST 请求体。这道 cookie 注入题恰好就处在“URL 注入已经顺手、但还没接触过 HTTP 其他位置注入”的过渡阶段。
还有一点值得说,靶场环境和真实目标不一样,它是有明确授权的,你可以放心大胆地测试、改包、反复试错。我在刷题时一直建议新手多在靶场里折腾,因为你只有见过了足够多的“异常响应”,以后在真实授权测试里看到类似现象才能快速反应。
1.2 Cookie 注入和普通 SQL 注入到底差在哪
先来捋一捋概念。Cookie 本身是服务端下发、浏览器保存的一小段文本数据,用来维持会话状态。当你登录一个网站后,服务端经常会往 Cookie 里写入一个标识,比如 uid=3 或者 username=admin,下次你访问页面时,服务端就靠这个 Cookie 认出你是谁。
cookie 注入的意思,就是服务端在拼接 SQL 语句时,直接使用了 Cookie 里的值,而且没有做任何过滤。攻击者把 Cookie 里的值改写成 SQL 语句片段,比如把 uid=3 改成 uid=3',数据库就可能在执行 SQL 时报错,或者因为 union select 的注入而回显出敏感数据。
和普通 URL 参数注入相比,它们的本质一模一样,都是“外部输入未经过滤就进入了 SQL 语句”。区别只在于数据的位置变了——一个在请求行里,一个在请求头里。很多新手在 URL 上找得到注入点,一到 Cookie 就懵,是因为思维惯性太强,眼睛只盯着地址栏和表单,忽略了 HTTP 请求的其他组成部分。
从服务端开发的角度想,这个漏洞的成因其实很有意思。很多开发者在写代码时,会对 $_GET、$_POST 参数做严格的过滤验证,但到了 $_COOKIE 这里就觉得“这是服务端自己设置的,用户改不了”,于是放松了警惕。这种“信任错觉”在真实项目里非常常见,Cookie 注入也因此成了老牌 Web 漏洞里生命力很强的一种。
2. 原理深度拆解:为什么 Cookie 会成为 SQL 注入的入口
2.1 代码层面:服务端是怎么把 Cookie 拼进 SQL 的
要想彻底理解 cookie 注入,最好的方式就是看一段“罪魁祸首”代码。我用 PHP 写一个极简示例,很多老项目里你还能看到类似的写法:
<?php $uid = $_COOKIE['uid']; // 从 Cookie 里取出用户 ID $conn = mysqli_connect("localhost", "root", "password", "testdb"); // 直接把 uid 拼进 SQL,没有过滤、没有参数化 $sql = "SELECT id, username, email FROM users WHERE id = $uid"; $result = mysqli_query($conn, $sql); while ($row = mysqli_fetch_assoc($result)) { echo "用户名: " . $row['username'] . "<br>"; echo "邮箱: " . $row['email'] . "<br>"; } ?>注意,在第 2 行,$uid 直接从 Cookie 里取值,第 7 行又直接把它拼进字符串。这时候如果 Cookie 的值是 3,SQL 就是 SELECT id, username, email FROM users WHERE id = 3,一切正常。
但如果攻击者把 Cookie 改成 3',SQL 就变成了 SELECT id, username, email FROM users WHERE id = 3',语句多了一个单引号,数据库就会报语法错误。这种“多一个引号就报错”的现象,就是我们在检测阶段最想要的信号。
从开发角度来看,这种漏洞产生的根源有三个:
- 第一,没有区分“可信输入”和“不可信输入”。Cookie 本质上和 URL 参数一样,都是外部可控的数据,但它被开发者在潜意识里归为了“可信数据”。
- 第二,老项目的 SQL 语句习惯用字符串拼接,而不是预处理语句。PHP 里的 mysqli 预处理、PDO 绑定参数,Java 里的 PreparedStatement,都是为了解决这类问题而生的。
- 第三,缺少统一入口的全局过滤。很多项目只对 GET/POST 做了统一过滤,Cookie 不在过滤范围里,于是成了漏网之鱼。
2.2 Cookie 注入的请求过程与判断逻辑
下面我按实际操作顺序,把一次 cookie 注入从发请求到收到响应的完整链路拆开讲。
第一步,浏览器访问目标页面,服务端收到请求头,其中包含一行 Cookie: uid=3。第二步,服务端从请求头里解析出 uid 的值,拼进 SQL。第三步,SQL 执行,结果返回后被拼进 HTML 页面回显给浏览器。
攻击者要做的事情,就是打断第二步。他不再老老实实提交 uid=3,而是通过抓包工具修改请求头里的 Cookie 字段,把它改成 uid=3' 或者 uid=0 union select 1,2,3-- -,然后再把请求发出去。服务端并不会区分这个 Cookie 是浏览器自动带的还是攻击者改过的,它照样解析、照样拼接、照样执行。
这里有个关键点:正常用户很难通过浏览器地址栏直接修改 Cookie,所以攻击者通常需要借助开发者工具或者抓包工具。这也是为什么我一直强调,刷这道题之前,你至少得会一个抓包工具的基本用法,不然连注入语句都送不进去。
判断一个 Cookie 参数是否存在注入,逻辑和 URL 参数判断一模一样:
- 先用正常值访问一次,记录页面内容。
- 再把 Cookie 改成 1',看页面是否报错、内容是否变化。
- 如果报错,说明单引号进入了 SQL 且破坏了语句结构,存在注入。
- 如果页面无差别,再试试 1'-- - 看看注释掉后面的内容后页面是否恢复正常,恢复正常基本可以确定注入点。
这里面比较坑的是,有些服务端会把 SQL 报错信息吞掉,页面只显示空白或者“服务器错误”。遇到这种情况,别急着下结论,可以走盲注路线,用条件真假的页面差异来判断。
2.3 和 User-Agent 注入、Referer 注入放在一起看
Cookie 注入不是一个孤立问题。在 HTTP 请求里,能被服务端读取并用于业务逻辑的头字段,理论上都可能成为注入入口。最常见的是这三类:
| 注入位置 | 服务端获取方式 | 典型场景 | 检测要点 |
|---|---|---|---|
| URL 参数 | $_GET | 文章 id、商品 id | 修改地址栏参数即可 |
| Cookie 字段 | $_COOKIE | 用户身份、偏好设置 | 需改请求头,浏览器 F12 或抓包工具 |
| User-Agent | $_SERVER['HTTP_USER_AGENT'] | 访问日志、设备识别 | 需改请求头,纯手工较麻烦 |
这三类注入在原理上完全一致,都是“不可信数据进入 SQL”,但它们的排查难度不同。URL 参数是明摆着的,一眼就能看到;Cookie 和 User-Agent 则藏在请求头里,需要你主动去翻、去改。封神台这道题选 Cookie 作为注入点,其实就是在帮新手打破“参数只能出现在 URL 上”的思维定式。
3. 实操全记录:封神台 Cookie 注入题的完整复现
3.1 准备工具与环境
刷这类题目,我不建议一上来就上 sqlmap,虽然它很方便,但手工注入能让你真正理解每一步在干什么。我的建议是先把手工流程走通,再用工具提速、验证结果。
你需要准备三样东西:
- 一个现代浏览器,推荐 Chrome 或 Edge,主要用 F12 开发者工具。里面有三个地方会用到:Console 看报错、Network 看请求头、Application 面板改 Cookie。
- 一款抓包工具,我用的是 Burp Suite 的免费版。它的核心作用是拦截 HTTP 请求,修改请求头里的任意字段,再放行给服务端。不会用的同学,直接拿浏览器开发者工具也能完成大部分操作,只是可控性差一些。
- 一份 SQL 注入基础笔记,特别是 union select 的语法,后面会反复用到。
工具准备好后,打开封神台靶场对应题目。注意,每个靶场实例的地址可能不同,我这里只讲通用流程。另外,建议整道题都在无痕模式下操作,避免浏览器缓存干扰判断。
3.2 第一步:找到 Cookie 参数并确认它的作用
打开题目页面,第一眼看上去就是一个普通的个人信息页,可能显示了一个用户名或者“欢迎回来”之类的字样。这时候先别急着瞎改,打开 F12 开发者工具,切到 Application 面板,左侧找 Cookies,点开当前站点域名,你就能看到这个页面用到的所有 Cookie。
我在这道题里见到的典型 Cookie 长这样:
Cookie: uid=3很明显,uid 就是用户的 ID。页面当前展示的内容大概率就是 uid=3 这个用户的资料。此时你可以做个对照实验:在 Application 面板里把 uid 改成 1,刷新页面,看看显示的内容是否变成了另一个用户。如果变了,说明页面确实是通过 uid 这个 Cookie 去数据库查数据并展示的,这就是我们要找的注入点;如果没变,说明这个 Cookie 和页面数据没关系,得换个参数试。
这一步是整个题目的胜负手。很多新手卡住,是因为根本没想到去翻 Cookie,或者翻了但没改。记住一句话:页面数据是通过某个输入数据查出来的,那个输入数据就是注入点,不管它藏在 URL 里还是 Cookie 里。
3.3 第二步:用单引号探路,判断注入是否存在
找到 uid 参数后,下一步就是“探路”。把 Cookie 从 uid=3 改成 uid=3',刷新页面。
这里要特别注意修改方式。如果直接改浏览器开发者工具的 Cookie,改完要刷新页面,让请求带上新的 Cookie 发出去。如果用的是 Burp Suite,就需要开启拦截,在请求发出时修改请求头里的 Cookie 行,再放行。
- 如果页面出现 SQL 语法错误、数据库报错信息,比如 “You have an error in your SQL syntax”,恭喜你,注入点实锤了。
- 如果页面只是变成空白,或者显示 500 错误,也别灰心,这同样说明单引号影响了 SQL 执行,大概率存在注入。
- 如果页面内容和 uid=3 时完全一样,没有变化,那要么参数名不对,要么服务端做了过滤,需要进一步测试。
为了进一步确认,可以把 Cookie 改成 uid=3'-- -。这里的 -- - 是 SQL 的注释符,意思是把后面的语句片段全部注释掉。如果加了注释后页面恢复正常,恢复到 uid=3 的样子,那基本可以断定:SQL 语句原本是 WHERE id = $uid,当我提交 3' 时,语句变成 WHERE id = 3'-- -,多出来的单引号被注释符抵消,SQL 又能正常执行了。这个“报错-恢复”的循环,就是典型的注入特征。
3.4 第三步:order by 猜列数,为 union 注入做准备
确认注入存在后,下一步就是用 union select 来做联合查询。但 union 前后两个查询的列数必须一致,否则数据库会报错。所以要先猜出原查询查了几列。
方法是把 Cookie 改成 order by 的形式,从 1 开始逐步增大:
Cookie: uid=3 order by 1-- - Cookie: uid=3 order by 2-- - Cookie: uid=3 order by 3-- -逐个尝试,直到页面报错为止。比如我改到 order by 4 时页面报错,说明原查询只有 3 列,因为 ORDER BY 4 时数据库找不到第 4 列。
这里有个细节,不同数据库的报错方式不一样。MySQL 一般是 “Unknown column '4' in 'order clause'”,OpenOffice 或者其他数据库的报错文本可能不同。不过靶场里大多是 MySQL,按这个思路来基本不会错。
3.5 第四步:union select 找显示位,摸清页面回显
知道列数之后,就开始构造 union 注入。把 Cookie 改成:
Cookie: uid=3 union select 1,2,3-- -如果页面只回显了原来的 uid=3 数据,看不到 1 和 2,说明查询结果只取了第一条。这时候要把前面的值改成一个不存在的值,让 union 后面的查询结果排到前面来:
Cookie: uid=999 union select 1,2,3-- -因为 id=999 不存在,数据库就不会返回第一条记录,于是页面就会显示 union select 后面的数据。这时候你看页面上出现了哪些数字,对应的就是“显示位”。比如页面显示 2 和 3,说明第 2 列和第 3 列的数据会被拼进 HTML 输出,后续查数据就往这两个位置放。
3.6 第五步:爆库名、爆表名、爆字段名,拖出小芳的信息
显示位确定后,接下来的流程就非常标准了。我把关键语句按顺序列出来。
先查当前数据库名:
Cookie: uid=999 union select 1,database(),3-- -再查所有表名:
Cookie: uid=999 union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database()-- -看到表名后,查某个表的字段名。假设表名是 users:
Cookie: uid=999 union select 1,group_concat(column_name),3 from information_schema.columns where table_name='users'-- -最后查数据:
Cookie: uid=999 union select 1,group_concat(id,0x3a,username,0x3a,password),3 from users-- -这里 0x3a 是冒号的十六进制表示,用来把 id、username、password 分隔开,方便阅读。你也可以把 group_concat 换成 concat_ws(0x3a, id, username, password),效果类似。
整个过程看起来很长,实际上就是三板斧:查库名、查表名、查数据。第 3.5 节到第 3.6 节的语句,是把“手工注入标准套路”完整走了一遍。封神台这道题的“为了小芳”,通常就是让你在某个表里把包含小芳的那条记录查出来,找到对应的账号信息,提交答案就算过关。
3.7 关于手工注入和 sqlmap 的取舍
手工流程走完之后,你可以再用 sqlmap 验证一遍结果。命令很简单:
sqlmap -u "http://目标地址" --cookie "uid=3" -p uid --dbssqlmap 会用 --cookie 指定要测试的请求头 Cookie 参数,-p uid 指定测试参数名。它会自动检测注入类型、数据库类型,然后爆出库名表名和内容。
但我要强调,sqlmap 是工具,手工是基本功。你只有先手工明确了“为什么改 Cookie 能改变 SQL 语句”“为什么 order by 能测列数”“为什么 union select 能回显数据”,再用 sqlmap 才不会两眼一抹黑。工具报错了你也不知道怎么调,这毛病是最要命的。
4. 常见问题与排查技巧实录
4.1 改了 Cookie 但页面没反应,问题出在哪
这是我刷题时遇到最多的情况,也是新手最容易卡死的地方。修改 Cookie 后页面完全不变,原因通常有三个。
第一,浏览器的缓存。有些页面在请求时直接命中了本地缓存,请求根本没到达服务器,Cookie 自然就没生效。解决方法是开无痕模式,或者刷新时加 Ctrl+F5 强制刷新。第二,参数名不对。你以为服务端用的是 uid,实际上可能用的是 user_id 或者 mid,这就要在开发者工具的 Application 面板里老老实实翻一遍 Cookie 列表。第三,服务端可能在代码里加了过滤,把特殊字符直接去掉了。遇到这种情况,可以试试大小写混合、十六进制编码或者双写绕过,但靶场里一般不需要这么折腾。
4.2 单引号加进去后页面空白,没有报错信息
页面空白并不是坏事,它至少说明 SQL 的结构被破坏了,只是服务端没把报错信息吐出来。这种情况我会优先走“时间盲注”或者“布尔盲注”的思路。
布尔盲注的意思,是构造两个条件一个为真一个为假,看页面内容是否不同。比如把 Cookie 改成:
Cookie: uid=1 and 1=1-- - Cookie: uid=1 and 1=2-- -如果第一个页面有内容、第二个页面空白,说明 and 后的条件影响了查询结果,那就可以用这种“页面有无”来逐字符猜数据库名。虽然这样一个个猜很慢,但思路清晰,不容易翻车。
对于时间盲注,则依靠 sleep 函数来判断,比如:
Cookie: uid=1 and sleep(3)-- -如果请求明显延迟了 3 秒,说明条件被执行为真。不过靶场题一般不会把人逼到盲注这一步,更多是让你确认注入点类型。
4.3 union 注入时回显位置错乱或者类型不匹配
union select 报错的原因主要有两类。
一类是列数不对。这种情况回到第 3.4 节,重新用 order by 一步步测,别偷懒跳步。另一类是前后查询结果的数据类型不一致。MySQL 的 union 对列的数据类型比较敏感,如果第 2 列本来放的是字符串,你 select 的是数字,有时候能自动转换,有时候会报错。稳妥的做法是让 select 输出的都是字符串类型,比如:
Cookie: uid=999 union select 1,(select group_concat(table_name) from information_schema.tables where table_schema=database()),3-- -把敏感的查询结果包在括号里,可以减少类型干扰。
4.4 修改 Cookie 后页面只显示第一行数据,后面内容看不到
这是 union 注入最常见的一个坑。查询结果有多行,但页面只回显了原来的第一条记录,后面 union 追加的内容根本没显示。解决办法前面也提到了:把 Cookie 第一个值改成一个不存在的 id,比如 999 或者 -1,目的就是让原始查询返回空结果,这样 union 后面的数据才能“浮出水面”。
还有一种情况是页面本身就只取第一条数据,比如 PHP 代码里用了 mysqli_fetch_assoc 而不是循环输出所有行。这时候多用 group_concat 把多行数据拼到一行里输出,是最省事的方案。
4.5 问题排查速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 修改 Cookie 后页面无变化 | 浏览器缓存、参数名错误、服务端过滤 | 无痕模式刷新;查看全部 Cookie 名称;尝试编码绕过 |
| 单引号后页面空白 | SQL 报错被吞 | 尝试布尔盲注或时间盲注判断注入 |
| union select 报错 | 列数不对、类型不匹配 | 重新 order by 测列数;用 group_concat 拼字段 |
| 只显示第一行数据 | 页面只输出第一条记录、查询结果被覆盖 | 将 uid 改为不存在值;配合 group_concat 多行合一 |
| 报错里出现转义字符 | 服务端对单引号做了转义 | 尝试双写单引号转义,或用十六进制编码 |
5. 从 Cookie 注入到 Cookie 安全:刷完这题后的延伸思考
5.1 把剧情题变成知识沉淀
“为了小芳”这个梗,刷完题再看,其实就是出题人给注入过程套的一层故事包装。但我觉得,刷题不能只停留在“过了这道题”的层面。每刷完一道题,我都会问自己三个问题:这题考的是哪个知识点?如果是开发人员,我该怎么防御?如果是下一个攻击者,还有哪些绕过思路?
放在这道题上,知识点就是“外部输入不可信”,防御思路就是“Cookie 里的数据也要校验”,绕过思路就是“如果服务端对 GET/POST 做了过滤,可以试试 Cookie”。把这三点想通了,一道题的收获比单纯跑通 sqlmap 大得多。
5.2 防御端:Cookie 到底该怎么设置才安全
做安全的人不能只懂攻击,还得知道怎么修。Cookie 注入的防御,核心思路有两层。
第一层,从根上杜绝 SQL 注入。不管是 Cookie 里的值还是 URL 上的参数,凡是进入 SQL 语句的变量,一律使用预处理语句加参数绑定。PHP 里是 PDO 的 prepare + execute,Java 里是 PreparedStatement,这种方法可以让数据库自动处理输入的转义问题,拼字符串这种写法在代码审查时应该直接打回。
第二层,提高 Cookie 本身的安全性。服务端设置 Cookie 时,至少要考虑三个属性:
- HttpOnly:设置为 true 后,JavaScript 无法通过 document.cookie 读取 Cookie,能有效防止 XSS 攻击把 Cookie 偷走。
- Secure:设置为 true 后,Cookie 只在 HTTPS 连接中传输,避免在 HTTP 明文状态下被中间人截获。
- SameSite:可以控制 Cookie 在跨站请求中是否携带,对于缓解 CSRF 攻击很有帮助。
举个例子,用 PHP 设置一个相对安全的 Cookie:
setcookie("uid", $uid, [ 'expires' => time() + 3600, 'path' => '/', 'domain' => 'example.com', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax', ]);这段代码加了 Secure、HttpOnly、SameSite,虽然不能直接阻止 SQL 注入,但可以让 Cookie 不易被窃取、不易被跨站利用。配合后端的预处理语句,整个 Cookie 的使用场景才算安全。
5.3 后续练习建议:从手工注入到综合渗透
刷完这一题,如果还想继续提升,我建议按这个路线往下走:
- 继续刷封神台后面的题目,重点留意 User-Agent 注入、Referer 注入这类“非标准参数注入”,把 HTTP 请求头里的每个字段都变成你检查的对象。
- 学习 POST 型注入。很多真实场景下,注入点在登录表单、搜索框这类 POST 请求的 body 里,方法和 GET 型大同小异,但抓包工具的使用会更频繁。
- 掌握一种自动化工具的进阶用法,比如 sqlmap 的 --forms、--level、--risk 参数,理解工具背后的检测逻辑。
我个人在实际操作中的体会是,刷靶场最忌讳“跑通即可”。一道题跑通了,如果没办法反过来给别人讲清楚“为什么这样就能进数据库”,那下次换一个环境你还会卡住。封神台的“为了小芳”是一道很温柔的入门题,它只是把 Cookie 这个不被注意的注入入口指给你看,真正的功夫,在于你以后看到任何请求头字段时,都能本能问一句:这个值,服务端到底怎么用的?