封神台“为了小芳”靶场实战:Cookie注入原理与完整注入流程
2026/9/15 15:27:16 网站建设 项目流程

第一次听说封神台靶场有个关卡叫“为了小芳”,我还以为是谁挂了个武侠同人页在上面。真正点进去之后才发现,这其实是一道很典型的Cookie注入题,剧情设置得挺有意思,但技术内核相当扎实。如果你最近也在练SQL注入,想找个和平时不一样的注入点练手,那这道题很值得花时间啃一遍。

我花了一个下午外加一个晚上,把这道题从头到尾走通了。整个过程里踩了不少坑,比如把注入点认错、order by语句被注释符坑了、还有一次把Cookie值改得太长直接给浏览器整懵了。这篇文章就照着我的实操路线,把Cookie注入的原理、判断方法、完整注入流程和排查技巧一次性讲清楚,希望能帮你少走几段弯路。

1. 封神台靶场到底是个什么环境

1.1 为什么选择靶场练手

封神台是安全圈里比较常见的Web漏洞练习平台,它提供了一系列故意设计好的有漏洞的Web应用,覆盖SQL注入、XSS、文件上传、越权等常见问题。你在这上面可以放心大胆地测,因为目标就是给你练手用的,弄坏了也不会影响任何真实业务。

我记得自己刚接触安全测试时,也动过“找个真实站点练练”的念头,幸好当时被前辈按住,说了一句我现在还记着的话:安全测试的前提是授权,没授权的测试叫破坏。靶场的价值就在这里,它把你在书上看过的漏洞类型,装进一个完全可控的站点里,让你随便折腾,等到你把原理和手法都摸透了,再去面对真实系统时才能有分寸。

“为了小芳”这个关卡就属于典型的剧情向注入题,名字取得挺有代入感,但本质上考察的,是你对Cookie注入这个冷门入口的敏感度。很多新手做注入时,眼睛只盯着URL参数和POST表单,很少会有人主动去改Cookie里的值试试。这恰恰是这道题的设计意图:转换思路,把注意力从常规参数挪到请求头里。

1.2 Cookie注入在整个注入体系里的位置

如果你把SQL注入按注入点来分类,常见的无非是GET注入、POST注入、Cookie注入,还有HTTP头注入。GET和POST是大家练得最多的,资料也多,而Cookie注入相对冷门。但它真实存在于不少老旧系统里,尤其是那些把用户信息存在Cookie里、再拿去拼SQL的写法。

Cookie注入的原理和普通SQL注入完全一样,都是因为程序把用户可控的输入未经处理直接拼接进了SQL语句。区别只在于数据从哪个位置进来。普通注入里你改的是?id=1,Cookie注入里你改的是Cookie: id=1。但是很多开发者对参数做了过滤,却忘了Cookie这道门,所以Cookie注入也经常被用来绕过基于参数的防护。

练完这道题你会发现,所谓的“新漏洞类型”其实并不新,它只是换了个入口,攻击思路和判断方法跟普通注入是相通的。这种“换个入口看问题”的能力,才是这道题真正想教给你的东西。

2. Cookie注入的原理,讲透它为什么能成

2.1 Cookie和Session的关系

聊Cookie注入之前,先把Cookie和Session这俩概念捋清楚。Session存在服务器端,用来标识一次会话的上下文,而Session ID一般会下发到浏览器,存在Cookie里。浏览器每次请求都会带上这个Cookie,服务器通过它找到对应的Session数据。

问题往往出在程序员图省事上。有人觉得Session太占服务器内存,就把一些“不那么敏感”的用户信息直接存在Cookie里,比如用户名、用户ID、角色编号。反正每次请求都会带过来,直接从Cookie里取出来用就行,省事。代码如下所示:

$id = $_COOKIE['id']; $sql = "SELECT nickname, email FROM users WHERE id = $id"; $result = mysqli_query($conn, $sql);

这段代码看起来很自然,但问题在于$id来自用户可控制的Cookie,并且没有做任何过滤和类型校验。攻击者把Cookie里的id改成1 or 1=1,SQL语句就变成了查全表。这就是Cookie注入的基本触发场景。

2.2 注入点为什么选在Cookie上

那为什么开发者会漏掉Cookie呢?这里面有一层心理因素。很多人在写过滤代码时,注意力都放在URL参数和表单提交上,觉得这两个才是用户输入的“正门”,却把Cookie当成系统自己维护的数据。这种想法在早期Web开发里很普遍,于是Cookie就成了一个无人看守的后门。

从攻击者的角度来看,Cookie注入还有额外的好处。部分安全设备或者应用层的输入过滤只检查GET和POST的请求体,未必会仔细检查Cookie,所以用Cookie做注入点,有一定概率绕开基于参数的黑名单检测。这就是为什么很多老漏洞报告里,Cookie注入被归类为“绕过类”手法的原因。

需要注意一点,我说“一定概率”而不是“必然”,是因为现在稍微正规点的防护体系已经会把请求头全量检测了。但靶场存在的意义就是带你熟悉这一类思维,等你以后在代码审计里看到类似的写法,能一眼识别出来。

2.3 Cookie注入和普通注入的对比

我用一个表格把这几种注入方式的差异列出来,方便你对照着理解:

注入类型数据位置典型语句过滤方式检出难度
GET注入URL参数/user.php?id=1相对严格
POST注入请求体表单提交字段依站点而定
Cookie注入Cookie头Cookie: id=1容易遗漏
User-Agent注入请求头User-Agent: xxx'容易遗漏
Referer注入请求头Referer: xxx容易遗漏

从检测难度上就能看出,Cookie注入之所以在靶场里单独立题,就是要训练你在看一个请求时,不要只盯着主菜,要连配菜一起看。一个完整的HTTP请求里,任何一个可以被服务端取用的部分,都可能是注入点。

3. 准备工作:工具选型与测试环境搭建

3.1 干活前需要准备哪些东西

做Cookie注入测试,需要的工具不多,但每一样都有它的用途。我用到的工具如下:

  • 浏览器:推荐Chrome或Firefox,主要用来访问靶场页面和观察现象。
  • Burp Suite:抓包改包的核心工具,Cookie注入的关键操作都要在这里完成。
  • Cookie工具:浏览器插件如EditThisCookie或Cookie-Editor,也可以代替Burp直接改Cookie。
  • HackBar:浏览器插件,方便在Firefox里直接构造和发送请求。
  • 一个记事本:用来记录payload和报错信息,排查问题时有很大帮助。

这里面最核心的就是Burp Suite。如果你只用浏览器插件改Cookie,虽然也能做,但看不到完整请求,排查问题时视角受限。Burp能让你看到浏览器发出去的每一个请求头,Cookie长什么样、值变了之后服务器给了什么响应,全部一目了然。想练Web安全,Burp这套基本功早晚要过。

3.2 用Burp Suite拦截并修改Cookie的完整流程

第一次用Burp的同学经常卡在代理配置上,这里把步骤写细一点:

  1. 打开Burp Suite,默认工作在Proxy标签页。
  2. 确认Proxy -> Options里的监听地址是127.0.0.1:8080
  3. 打开浏览器的代理设置,把HTTP和HTTPS代理都指向127.0.0.1:8080
  4. 浏览器访问http://burp,下载并安装Burp的CA证书,否则HTTPS流量无法解密查看。
  5. 回到Burp的Proxy -> Intercept,把Intercept开关打开。
  6. 在浏览器里访问靶场页面,这时候Burp会拦截住请求。
  7. 在拦截到的请求里找到Cookie这一行,直接修改里面的值,然后点击Forward放行请求。

整个流程里最容易出问题的就是证书安装那一步。有些浏览器会拦着不让装CA证书,或者装完证书后流量还是显示加密状态,这时候需要去浏览器设置里手动信任一下证书。证书这块搞不定,后面的HTTPS流量全是一堆乱码,没法看。

3.3 了解你面对的目标页面结构

靶场环境因为是故意构造的,页面结构通常比较简单,没有大型网站的繁杂逻辑,这反而方便了测试。打开“为了小芳”这个关卡后,先正常操作一遍,查看页面能展示哪些信息。正常情况下,你会看到一个用户信息展示页面,它根据某个ID值去数据库里查询用户信息并显示在页面上。

这时候千万别急着改包,先把正常请求长什么样搞清楚。在Burp里把当前页面的请求从头到尾看一遍,记下URL路径、请求方法、Cookie值。只有先知道“正常”是什么样的,后面改包后出现异常时,你才能一眼分辨出是语法错误还是闭合方式不对。这一步看起来很基础,但很多人在注入半天没结果后回头排查,发现问题出在根本没看清请求结构上。

4. 实操全过程:一步一步注入“为了小芳”

4.1 判断注入点:到底是不是Cookie

先别急着上工具,手工访问一下目标页面,观察URL。很多新手拿到注入题的第一反应是往URL后面加?id=1,但如果注入点在Cookie里,你加了参数也没反应。

打开Burp的拦截,刷新页面,查看请求内容。我当时看到的请求长这样:

GET /user.php HTTP/1.1 Host: target.com Cookie: id=1 User-Agent: Mozilla/5.0

注意观察,URL里没有任何参数,页面却能展示一条用户信息,说明程序读取的数据源只可能来自Cookie,最可疑的就是这个id=1。如果你想进一步确认,可以用浏览器插件把Cookie里的id改成2再刷新页面,看看页面内容是否变化。如果变了,那就基本确定了这个Cookie值是页面数据的查询条件。

这一步做下来,注入点就有眉目了。别小看这个“改值看变化”的动作,这就是手工测试里的“黑盒判断法”,后面所有注入逻辑都建立在这个基础上。

4.2 验证注入存在:经典的单引号试探

确认Cookie的id是查询条件后,接下来验证是否存在SQL注入。按照SQL注入的基本套路,先输入一个单引号,看看后端会不会报错。把Cookie改成id=1',放行请求,观察页面响应。

如果页面出现SQL语法错误,比如显示You have an error in your SQL syntax near ''1''',那就说明后端程序确实把Cookie值拼到了SQL语句里,且没有做过滤。这就是一个标准的注入点。如果页面返回正常或者直接跳转到错误页,那说明Cookie值可能被强转了类型,或者被引号包裹但闭合方式不对,需要继续尝试。

我当时看到报错信息时的第一反应是松了口气,因为报错直接暴露了数据库类型。从报错语句的写法可以判断后端用的是MySQL。知道数据库类型后,后面的payload构造就有了明确方向。

4.3 确认字段数量:order by的用途

进入正式注入环节,第一步是确定目标SQL语句一共有几个字段,这决定了联合查询里你最多能占几个位置。用order by来测:

id=1 order by 1 id=1 order by 2 id=1 order by 3

每一次把order by后面的数字加1,然后观察页面是否还正常。如果页面正常,说明排序字段不超出当前查询的字段数;如果页面报错或者返回异常,说明字段数就到这里为止。

我测试的时候比较顺利,从1开始一直加到3都正常,加到4的时候页面报错了,说明SQL语句的字段数是3。另一个容易踩的坑是注释符的选择。在MySQL里,--后面必须跟一个空格,SQL语句中的--后面不加空格不会生效。这也是我在实操中浪费了最多时间的步骤。如果order by测试没有生效,先检查注释符写法,把--换成#或者--+再试一把。

4.4 判断显示位:union select的联合查询

知道字段数是3之后,开始构造联合查询,目的是找出页面上哪个位置会把数据库查询结果直接显示出来。把Cookie的值改成:

id=-1 union select 1,2,3

这里把id改成-1,是因为要让前面的查询查不到任何记录,这样union后面的查询结果才有机会显示在页面上。这是一种很常见的技巧,一定要记住。

放行请求后,页面上如果出现数字123中的某一个或几个,那么这些数字对应的位置就是注入回显位。后面需要展示数据库信息时,把这些数字替换成对应的SQL函数就行了。我第一次跑的时候,页面上只显示了23,说明第一个字段被程序用来做逻辑判断或拼接,没直接显示出来,而第二、第三个字段是回显点。

如果页面上什么数字都没出现,不要急着怀疑语法。看看当前页面是不是有多个请求,你可能在处理一个带有分文件的页面,回显内容跑到另一个请求里去了。这时候去Burp的历史记录里找响应体包含数字的那个请求。

4.5 爆库:获取数据库名

找到回显位后,把对应位置的数字替换成database()函数,就能获取当前数据库名。我的payload长这样:

Cookie: id=-1 union select 1,database(),3

页面刷新后,第二个数字变成了一个字符串,这个字符串就是当前数据库名。靶场环境下,数据库名一般是sqlsitetest之类的名称,比较有标识性。拿到库名之后,整个注入就步入正轨了,后面的操作都是按部就班地查表名、查字段名、查数据。

4.6 查表:从information_schema里取数据

MySQL有一个自带的元数据库叫information_schema,里面记录了所有数据库、表、字段的信息。通过查询它的tables表就能拿到目标数据库下的所有表名。payload如下:

id=-1 union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database()

这里用了group_concat函数,把多个表名拼在一行显示,省得一行一行翻。我当时查出来一堆表名,有usersadminlogs这些,一眼就能看到重点。如果表名特别长也没关系,页面会换行,你从响应里能看到全部内容。

4.7 查字段:定位关键字段

拿到表名后,接着查这张表里有哪些字段。payload如下:

id=-1 union select 1,group_concat(column_name),3 from information_schema.columns where table_name='users'

注意表名两边的单引号。如果表名带了前缀,比如dx_users,也要完整写进去,错一个字母都查不出来。如果查出来为空,多半是表名写错了,回去核对一下刚才拿到的表名。

4.8 拖数据:读取用户名和密码

找到关键字段后,直接读取目标表的记录。payload如下:

id=-1 union select 1,group_concat(username,0x3a,password),3 from users

0x3a是冒号的十六进制表示,这样用户名和密码之间就会以冒号分隔,看起来一目了然。执行完之后,页面上就能看到目标用户的账号密码信息,有些密码是明文,有些是MD5哈希值。如果是哈希值,可以去一些在线破解平台碰碰运气,不过靶场里十有八九是明文或者简单哈希。

我做完这些步骤,把PageSource翻了一遍,没有看到其他花活,这一关就过了。整体流程从判断注入点到拖出数据,耗时不到二十分钟,但中间排障的时间比实际注入的时间还长。

5. 踩坑实录:我做这道题时遇到的典型问题

5.1 注入语法对但没结果,问题在注释符

做SQL注入最让人抓狂的就是payload看上去完全正确,但页面上没有任何变化。这个问题我自己遇到过,后来发现是注释符的问题。在MySQL中,--注释必须后跟一个空格或者控制字符。很多朋友在命令行里试--习惯了,到了HTTP包里还是只写两个减号,导致后面的单引号没有被注释掉,SQL语句直接报错,页面自然就异常了。

解决方案很简单:把--改成#,或者写成--+,也可以直接把后面的单引号配对好。我在靶场里习惯用#注释,因为它不受空格限制,写起来省事。以下三个写法在MySQL里都合法:

id=1' order by 3-- id=1' order by 3--+ id=1' order by 3#

如果你用的是MySQL 5以上的版本,建议用#,在URL编码环境下可以写为%23。用--时一定记得后面加空格,在HTTP协议里这个空格很容易被忽略,但在SQL语法里它是必须的。

5.2 Cookie值被URL编码干扰

Cookie里允许出现的字符和URL参数不太一样,有些特殊字符需要编码。比如空格、单引号、#这些,在Cookie值里直接发出去,服务器收到的可能是被转义或者被拒绝的版本。

遇到这种情况,可以在Burp里把payload做URL编码再放进Cookie值。比如说#写成%23,空格写成%20。但要注意,不要整个payload都编码,否则服务器会按你编码后的字符串整体作为Cookie值,注入逻辑就全变了。我习惯的做法是只编码特殊字符,保留unionselect这些关键字原样。

还有一个细节:有些服务器的Web中间件会限制Cookie头的总长度,比如Nginx默认的large_client_header_buffers是4K左右,如果你的注入payload特别长,比如group_concat带了一大串表名,可能会被中间件截断,这时候需要精简payload,把不需要的查询条件去掉。

5.3 误以为GET参数才是注入点,方向跑偏

这道题我一开始也犯了先入为主的错误。打开靶场页面看到URL是/user.php,习惯性地在后面补了个?id=1,结果页面显示正常,我还以为参数有效。后来在Burp里细看才发现,浏览器发送的请求里根本没有这个参数。真正的注入点在Cookie的id值上。

这个教训其实值得多说一句。你在做任何测试之前,先完整看一遍实际的HTTP请求,不要凭直觉猜。尤其是那种URL干干净净没有任何参数的页面,数据来源大概率就在Cookie或者别的请求头里。把Burp用起来,看到什么测什么,别自己脑补参数。

5.4 页面无报错时的盲注判断思路

有些场景下,后端会把SQL错误信息屏蔽掉,你扔一个单引号进去,页面既不报错也不异常,就像什么都没发生一样。这时候不要放弃,改用“真值对比法”来验证注入。

把Cookie的值改成id=1 and 1=1,如果页面正常,说明整个条件为真,注入点存在。再改成id=1 and 1=2,条件变为假,页面应该跟之前不一样,或者不返回任何记录。这种判断方式不依赖报错信息,哪怕页面把SQL错误全部吞掉,也能通过对比响应的差异确认注入是否存在。

如果再进一步,连响应内容的差异都看不出来,那就需要考虑时间盲注,用sleep()函数来判断。把id=1 and sleep(5)放进去,如果页面卡了5秒才响应,同样能说明注入点存在。靶场环境一般不太会用到这一步,但实战里是非常常用的保底手段。

5.5 测试结束后记得恢复环境

这是一个很容易被忽略的小节。测试过程中你会改掉Cookie的默认值,发了一堆乱七八糟的注入请求。测试完成后,把Cookie恢复成正常值,再刷新一下页面,确认页面恢复正常。如果后续有人接着用你的浏览器环境,看到Cookie里写着一堆注入payload,容易对靶场产生误判。

另外,把Burp里的拦截开关关掉,不然下一次打开浏览器访问百度都会被卡住。这类操作看似不起眼,但在多人共用一台机器时特别重要,养成好习惯才能让你在团队里显得专业。

6. 从攻击视角切回防御视角:怎么防住Cookie注入

6.1 最有效的防线:参数化查询

Cookie注入的本质还是没有规范的SQL拼接。防住它的首选方案就是参数化查询,把所有来自用户的输入都当作参数传递,而不是拼接进SQL字符串。用PHP的PDO写法举例:

$stmt = $pdo->prepare("SELECT nickname, email FROM users WHERE id = ?"); $stmt->execute([$_COOKIE['id']]);

参数化查询能彻底杜绝拼接型注入,因为用户输入不会参与SQL语句的语法解析,数据库只把传入的值当作字面量处理。很多还在用字符串拼接的老代码,是Cookie注入的重灾区,升级改造时优先改这类代码,收益最大。

6.2 辅助防护:输入验证与最小权限

除了参数化查询,还需要对输入做类型校验。Cookie里的id,在业务逻辑里如果被当作整数用,后端就应该强制转换成整数类型,注意不是过滤,而是强制类型转换。在PHP里就是(int)$_COOKIE['id'],转完类型,注入就没有发挥空间了。在Java里就是Integer.parseInt(),解析失败直接报错。

数据库账号的权限也要控制。很多站点的数据库账号是root,这等于给了攻击者一把万能钥匙。正确的做法是给应用分配一个最小权限账号,只能增删改查它自己的表,不能碰其他库。这样哪怕注入成功,攻击者能拿到的东西也极其有限。

6.3 Cookie本身的保护机制

另外,Cookie本身的安全属性也要补齐。给Session Cookie加上HttpOnly标记,让JavaScript无法通过document.cookie读取,可以有效防范XSS攻击带来的Cookie窃取。加上Secure标记,确保Cookie只在HTTPS连接中传输,防止在明文HTTP中被抓包截获。

还有一个常见问题是,不少老系统把用户ID直接明文放在Cookie里。这种做法无论如何都应避免。用户身份信息应该放在服务端Session里,Cookie里只存一个随机生成的Session ID。即便某个环节出了问题,攻击者也拿不到直接可用的用户标识。

7. 这道题之后,还能往哪些方向延伸

做完“为了小芳”这道题,Cookie注入的基本流程你已经完整走了一遍。但我觉得这道题只是起点,以下几个方向很值得继续往下钻。

第一个方向是把其他请求头作为注入点试试。Cookie之后,还有User-Agent、Referer、X-Forwarded-For等请求头都是潜在注入位置。老系统里经常出现把User-Agent直接存进数据库再显示出来的功能,那里同样可以注入。练法跟Cookie注入一模一样,换了个入口而已。

第二个方向是结合报错注入。如果页面把SQL错误信息显示得非常详细,可以用updatexml或者extractvalue函数直接通过报错内容把数据“震”出来,这种方式比联合查询更简洁,对没有回显位的场景更友好。

第三个方向是学习如何写自动化脚本。手工注入练明白原理之后,用Python写个脚本,发送HTTP请求、提取响应内容、自动遍历数据库里的表名和字段名,可以显著提高测试效率。早期博客时代很多工具就是这么起源的。

第四个方向是深入挖掘该关卡的后续变体。封神台里面还有其他SQL注入相关的关卡,难度层层递进,有的加上了WAF,有的做了整数型注入,有的是宽字节注入。把基础注入吃透之后再去挑战这些,你会发现万变不离其宗,很多思路是相通的。

最后分享一点个人体会

在封神台刷“为了小芳”这关的时候,我一直没急着看别人的WP,而是自己在Burp里一遍一遍地试。中途有几次卡到怀疑人生,尤其是order by一直不生效的那段时间,差点就想去查答案了。后来静下心把HTTP请求从头到尾看了一遍,才发现是注释符写法的问题。

这道题给我最大的收获不是Cookie注入本身,而是提醒我:做安全测试要有多角度审视一个请求的习惯,别只盯着URL和POST参数的“正门”看,Cookie、请求头这些容易被忽略的位置,往往藏着真正的入口。靶场里的“小芳”是个虚拟角色,但现实世界里每一条被拖走的数据库记录背后,都可能对应着真实的用户。练技术的同时,更要懂得敬畏和分寸,学会攻的最终目的,还是为了更好地守。

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

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

立即咨询