最近把Sqllibs靶场Less20到Less22连着刷完了,这三关在整本靶场里算是一个小专题:Cookie注入。说它特别,是因为注入点从常见的GET/POST参数转移到了请求头里的Cookie字段,而且Less21和Less22还多包了一层Base64编码,很多人第一次碰到会卡在原地。这篇把我这三关的完整理解和实操记录整理出来,内容包括底层原理、逐关打法、报错回显的处理方式、以及我在改包时踩过的各种坑,给正在刷靶场或者想补Web安全基本功的朋友一个参考。
先说清楚边界:以下所有操作都只建议在本地靶场、CTF或获得授权的前提下进行。Sqllibs这类开源练习平台本身就是用来练手的,把流程走通、把原理吃透,才是这篇的目标。
1. 先弄清楚Cookie注入的底层逻辑
1.1 为什么Cookie也会成为SQL注入点
很多人一开始会有个错觉:Cookie是浏览器自动保存的东西,服务端读取它好像挺可信。实际上Cookie完全可控,浏览器开发者工具、抓包工具都能随意修改它的值。所谓Cookie注入,本质就是服务端代码把Cookie里的某个字段拿出来,直接拼进了SQL语句。
用一个生活的例子来说:Cookie就像你去健身房前台时拿到的会员卡,前台看到卡上写的“年卡会员”就直接放行,但他不会去想这张卡是不是有人拿笔改过。服务端也一样,如果代码里写着类似这样的逻辑:
$uname = $_COOKIE['uname']; $sql = "select * from users where username='$uname'";那Cookie里的uname就成了一条完全开放的SQL拼接入口。Less20到Less22这三关,核心都是这个场景,只不过后面两关在拼接之前多了一道Base64解码的工序。
1.2 Cookie注入和GET/POST注入的关系
从注入手法上看,Cookie注入和传统的GET/POST注入没有本质区别,单引号闭合、报错注入、布尔盲注、时间盲注,这些套路全部通用。差异主要体现在“传参位置”和“编码处理”上。
| 传参位置 | 数据放在哪 | 提交方式 | 常见利用难度 |
|---|---|---|---|
| GET | URL问号后面 | 链接或表单 | 入门级,WAF最常检查 |
| POST | 请求正文 | 表单提交 | 常见,WAF检查也频繁 |
| Cookie | 请求头Cookie字段 | 自动随请求发送 | 需要抓包改包,隐蔽性更高 |
Cookie注入在真实环境里经常被忽略,一个很重要的原因是安全测试人员的注意力通常放在URL和请求体上,对请求头里的Cookie关注不够。而开发者也很少会把Cookie当成“用户可控输入”去过滤,两边的盲区凑在一起,就形成了一个容易出问题的点。
Cookie注入的实际场景其实不少,比如“记住我”自动登录功能,服务端把用户名存进Cookie,下次访问直接读取并查询,这就是Less20背后最常见的模型。
2. Less20实战:从抓包到报错注入
2.1 登录后抓包,锁定Cookie里的注入参数
先按正常流程走一遍。用靶场里已有的账号密码登录,登录成功后打开Burp Suite的Repeater,把刚才的请求整个拖进去,重点看请求头里的Cookie字段。正常情况会看到类似Cookie: uname=admin这样的键值对。
这里的uname就是代码读取后直接拼SQL的注入点。在Burp里手动把Cookie改成:
Cookie: uname=admin'发送过去,如果响应里出现SQL语法报错,比如You have an error in your SQL syntax附近带出'admin'',说明单引号被带入了SQL语句,闭合方式也基本清楚了:username='$uname'这种字符型结构。
注意一个细节:浏览器开发者工具里的Application面板也能改Cookie,但Repeater更适合反复尝试不同payload,改完直接点发送,查看响应,效率高得多。
2.2 单引号探测闭合与报错取数据
闭合判断完了,下一步就是想办法把数据带出来。Less20这类MySQL环境,报错注入永远是最优先尝试的方案,因为Sqllibs靶场默认基于MySQL 5.x,updatexml和extractvalue这两个函数都能用。
最简单的验证payload:
Cookie: uname=admin' and updatexml(1,concat(0x7e,version(),0x7e),1) --+这里0x7e是波浪号“~”的十六进制表示,用两个波浪号把目标内容包起来,是为了在报错回显里看得更清楚,避免输出内容被截断。发送之后,响应里会出现类似XPATH syntax error: '~5.7.26~'的信息,版本号就被带出来了。
拿到版本号之后,爆库、爆表就是同一个套路:
Cookie: uname=admin' and updatexml(1,concat(0x7e,database(),0x7e),1) --+ Cookie: uname=admin' and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e),1) --+需要注意的是,updatexml函数接受的参数是(xpath_document, xpath_expression, replacement),第二个参数最多能回显32个字符,所以用concat把多个字段拼一起时,输出可能会被截断。解决办法是分段取,比如用substr或者一次只查一个表名。
2.3 报错不可用时的盲注兜底
报错注入不是所有场景都灵。如果目标环境过滤了报错函数,或者页面根本没有报错回显,就需要换盲注思路。Less20这个关卡,虽然报错能出东西,但作为基本功练习,盲注也必须会。
布尔盲注看的是页面差异。构造请求:
Cookie: uname=admin' and length(database())>3 --+如果页面正常显示,说明条件为真;如果页面显示异常或者没有任何查询结果输出,说明条件为假。接下来就是逐字符猜解,工作量比较大,但非常锻炼对SQL语句和页面响应的敏感度。
时间盲注适合页面完全无差异的情况:
Cookie: uname=admin' and if(ascii(substr(database(),1,1))=115,sleep(2),0) --+如果确实存在注入,条件为真时请求会明显延迟两秒。这里我不想把时间盲注作为首选推荐,因为它效率最低、对网络稳定性也有要求,但它是报错和布尔都失效时的最终兜底方案。实测下来Less20用报错注入的效率最高,布尔盲注用来验证思路,时间盲注更多是练手。
3. Less21实战:被Base64夹了一层的Cookie注入
3.1 先理解“Base64编码”这一道坎
Less21和Less20最大的区别,就是Cookie里的uname值不再是明文。服务端代码会先把Cookie值做base64_decode,解码之后再把结果拼进SQL语句。所以直接提交admin' and updatexml(...)这类原始注入语句,服务端会把它当成base64字符串去解码,得到的基本是乱码或空值,注入自然失效。
这个设计并不是凭空加难度。真实系统里确实存在把用户身份信息做简单编码后放进Cookie的做法,比如“记住我”功能把一个base64格式的用户名存到Cookie里,后端读取时直接解码再查询。Less21就是在模拟这种“先解码再拼SQL”的坏习惯。
做题之前先想清楚一件事:编码只是给传参过程加了一层壳,SQL注入的本质没有变。我们需要做的只是把“最终要执行的SQL片段”整体做一次base64编码,替换到Cookie值里,服务端解码后就等于收到了原始的注入语句。
3.2 标准流程:编好码再放进Cookie
实操流程并不复杂,分三步:
第一步,准备好原始注入payload。比如要探测数据库版本:
admin' and updatexml(1,concat(0x7e,version(),0x7e),1) --+第二步,把这个payload整体做base64编码。本地有Python最方便:
python3 -c "import base64; print(base64.b64encode(b\"admin' and updatexml(1,concat(0x7e,version(),0x7e),1) --+\").decode())"命令的输出是一串以“=”结尾的base64字符串,这一串才是要放进Cookie的内容。
第三步,用Burp把Cookie改成:
Cookie: uname=编码后的字符串发送之后,服务端解码得到原始payload,后面的注入流程和Less20完全一致,报错注入、盲注都能用。
需要提醒的是,如果base64后的字符串里出现了“+”号,在HTTP传输里有概率被解析成空格,导致解码失败。稳妥的做法是对“+”号和“=”号做URL编码,也就是+换成%2B,=可以保留也可以换成%3D。实际操作中,我习惯把编码结果里所有非字母数字的字符都做一次URL编码,省得来回试。
3.3 手工判断编码型注入的识别方法
很多时候我们并不知道某个参数是不是做了Base64编码,尤其是实战环境里,可能是Base64、Hex、URL编码、甚至自定义编码混着来。见到一个奇怪的Cookie值,先别急着注入,花十秒钟做一个判断:
第一,观察Cookie值形态。如果值的末尾有“=”,且内容由大小写字母、数字组成,通常就是Base64;如果整串全是十六进制字符(0-9和a-f),可能是Hex编码。
第二,直接把值丢进解码器验证。比如YWRtaW4=解出来是admin,那就说明是明文用户名的Base64形式,后面构造注入payload时也必须先编码再放进去。
第三,如果解码后看不出规律,可以尝试提交一个可以预测的输入,再观察编码结果。比如登录时用户名填admin,登出后看Cookie值是多少,反推服务端用的编码方式。
Less21关卡其实把这些都摊开了,Cookie里就是明文的base64用户信息,一点都不难。难的是养成“看到非明文参数先解码”的习惯,这个习惯练好了,后面遇到加密型传参点才不会慌。
4. Less22实战:换了个姿势的Base64 Cookie注入
4.1 别急着打,先看报错里的SQL语句
很多人刷到Less22时会有一种“这不是和Less21一样的吗”的感觉,于是直接把Less21的payload复制过来,改一下关卡号就发,结果要么无效要么没反应。Less22表面看起来也是Base64编码的Cookie注入,但注入点的SQL上下文可能已经变了。
不同版本的靶场代码有差异,有的版本是双引号闭合,有的版本是把解码后的Cookie值拼进UPDATE语句而不是SELECT语句。所以第一件事不是盲打,而是发送一个解码后带单引号的payload,观察报错信息里的SQL语句结构。
比如发送admin'的base64编码进Cookie,如果响应里能看到类似update ... where username='admin''的回显,就说明这是UPDATE型注入;如果报错显示的是select语句,那就还在Less21的套路里。这一步判断非常关键,能省下后面大量试错时间。
4.2 UPDATE型注入的报错与堆叠尝试
如果是UPDATE型注入,最常用的还是报错注入,但SQL结构上有个细节要注意:报错函数里如果要嵌套select子查询,必须用括号包起来,否则UPDATE上下文里解析会出错。
示例:
admin' and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e),1) --+这个payload同样需要先做base64编码再放进Cookie。如果报错函数被限制,盲注思路也不变,只是因为服务端可能执行的是UPDATE操作,页面回显的判断逻辑要更仔细一些。
另外Less22在部分版本里支持堆叠注入,也就是用分号把多条语句拼在一起执行,比如在注入点后面追加一句更新密码的语句。但堆叠注入能不能成功,取决于PHP连接MySQL用的驱动和配置,并不是所有环境都支持。我在测试时先尝试了报错注入,再顺手试了一下堆叠:
admin';update users set password=md5('test') --+这个payload也要先base64编码。需要特别提醒:在Cooke值里直接用分号很危险,因为Cookie字段本身就是用分号分隔多个键值对的,必须把分号做URL编码成%3B再放进去,否则请求头的Cookie解析会乱掉。就算编码了,底层驱动不支持多语句执行,这条也跑不通,所以不必在这上面死磕,重点是理解思路。
4.3 三关横向对比:差异点与通用套路
刷完这三关,我习惯做一个横向对比,把每关的注入载体、编码方式、SQL类型和推荐的第一步整理清楚:
| 关卡 | 注入载体 | 编码方式 | 常见SQL类型 | 推荐第一步 |
|---|---|---|---|---|
| Less20 | Cookie的uname | 无 | SELECT | 单引号探测闭合 |
| Less21 | Cookie的uname | Base64 | SELECT | 先解码再注入 |
| Less22 | Cookie的uname | Base64 | UPDATE或闭合变化 | 观察报错SQL语句 |
对比完会发现,核心链路始终是“找参数点、判断闭合、选择注入手法”,编码只是中间插入的一道障碍。Less22真正要练的能力是:不要套用上一关的惯性思维,而是根据报错信息动态调整payload。这个思维在实战里特别重要,因为目标的SQL语句不可能永远是你想象中的那种。
5. 实操中躲不开的坑与排查方法
5.1 用Burp修改Cookie的完整操作
Cookie注入这种场景,Burp基本是标配。具体操作分几步:
打开Burp后,可以先让浏览器走代理正常登录一次,在HTTP History里找到登录请求,右键发送到Repeater。Repeater的Raw区域能看到完整的请求头,找到Cookie那一行,直接改uname的值,点击Go发送。
如果不想让浏览器走代理,也可以直接用Burp抓一次请求,或者手动复制Header。靶场练习里最快的路径是:先用浏览器访问一次登录页,随便登录,Burp里就能看到完整请求,再送进Repeater操作。
有个小技巧:把payload提前准备好几个版本放在同一个Repeater标签页里,比如单引号探测版、报错注入版、盲注版,来回切换改Cookie值就行。实战中我经常把常用payload存成一段文本模板,需要用的时候改一下表名和字段名就能直接发,效率高很多。
5.2 我从翻车现场总结的六个高频问题
这几关刷下来,我整理了一个高频坑清单,每一个都是自己踩过或者看别人踩过的:
第一,base64串里的“+”号被吃成空格。这是最经典的坑。PHP对Cookie值会做URL解码,+在URL编码语义里就是空格,所以base64串里的+会被当成空格,解码立刻失败。解决方案是把+编码成%2B。
第二,注释符“#”在URL里被当成锚点。如果用的是GET方式或者在Burp的URL里直接传参,#后面的内容会被浏览器或者服务端忽略。Cookie里虽然没有URL锚点问题,但为了统一,我建议全部使用--+或--%20做注释,少用#。
第三,Cookie值里出现了分号。分号是Cookie的分隔符,一个没URL编码的分号会把一个完整字段拆成两个,服务端要么报错要么只读前半段。需要分号执行堆叠语句时,务必先编码成%3B。
第四,对编码顺序搞反了。Less21和Less22里,正确顺序是“先构造原始SQL注入语句,再整体base64编码”,不是“把编码后的内容拿去当payload”。我见过有人把admin编码成YWRtaW4=,再把updatexml(...)这一长串直接塞进Cookie,结果是服务端解码出admin,后面的SQL部分完全没进去。
第五,没注意MySQL版本导致的函数差异。updatexml函数在MySQL 5.1.5以上才可用,如果靶场环境是更低版本,报错函数换extractvalue或者floor(rand(0)*2)这类方式会更稳。
第六,直接复制网上payload不验证。很多网上的payload里带了表名、字段名,或者用的闭合方式跟当前环境不一致,直接复制过来只会浪费时间。每次都应该先做单引号探测,再决定后面怎么办。
5.3 为什么Cookie注入经常被安全人员漏掉
这个问题值得单独说,因为它不仅影响测试,也会影响防御。Cookie注入容易被漏,主要有两个原因:
一是测试习惯。很多安全测试人员盯着URL参数和POST表单做检查,却把Cookie当成“环境配置”而不是“用户输入”。实际上Cookie完全由客户端控制,和URL参数没有任何区别。
二是服务端信任。开发者在写代码时,往往对Cookie有“这是浏览器帮我管的东西”的错觉,少了一层过滤和校验。如果登录逻辑直接从Cookie里取用户名拼SQL,这个漏洞在代码审计里也容易因为位置偏门而被漏掉。
防御层面其实不复杂,所有Cookie字段都应该按用户输入处理,SQL查询一律参数化,不在Cookie里存可识别的用户名或身份信息。但这是另一个话题了,靶场练习阶段先把攻击链路吃透,后面做防御才能更有针对性。
6. 从靶场到实战,这套能力怎么迁移
6.1 真实系统里Cookie注入常见于哪些功能
Cookie注入在真实系统里最典型的功能就是“记住我”。用户登录时勾选“记住我”,服务端把用户名或用户名加时间戳存进Cookie,下次访问时读取Cookie去数据库查用户,如果这个查询是字符串拼接,Less20的模型就出现了。
购物车和用户偏好设置也可能出现这类问题。有些系统会把购物车ID、主题色或者某个配置项存进Cookie,后端又需要根据这些值查询数据,处理不当就有注入空间。和表单参数比,这些位置通常没那么显眼,测试覆盖率也更低,所以一旦存在就是深水区。
写到这里必须重复一遍:真实环境里测试Cookie注入,必须拿到授权。未经授权去探测任何系统,性质就变了。Sqllibs这类靶场的价值,就是让你在完全合法的环境里把技能练扎实。
6.2 刷完三关你应该固化的检查清单
这几关练下来,最重要的是形成一套肌肉记忆。我给自己整理了一个固定流程,每次遇到一个可疑参数时按顺序过一遍:
第一步,确定这个参数是否会被服务端读取,参与了哪些后端逻辑。Cookie字段别跳过。
第二步,判断参数值有没有编码或加密。遇到base64、hex、URL编码等,先解码成原始内容再看。
第三步,判断闭合方式。单引号、双引号、无引号,每种闭合对应不同的payload构造,这一步错了后面全错。
第四步,优先尝试报错注入,回显快、效率高;报错不行再走布尔盲注,时间盲注留最后。
第五步,观察SQL语句类型。是select还是update,决定了子查询有没有括号、能不能尝试堆叠、影响范围有多大。
第六步,梳理影响面。如果是UPDATE型注入,理论上可以直接改库里的数据;如果是SELECT型,主要威胁是数据泄露。影响面不同,后面上报给防守方的风险等级也不同。
这个清单不复杂,但它把散状的经验串成了一条线。我每次刷靶场都会被提醒:注入的本质从来不是某个神奇的payload,而是你能不能在一个陌生代码逻辑里快速找到用户输入到SQL语句的通路。
最后分享一点个人体会
这三关刷下来,最大的感受是Cookie注入一点不比GET/POST注入深奥,它只是把输入点挪到了请求头里,再套了一层编码壳子。真正难的不是构造payload,而是识别出“这个参数可以进SQL”这件事本身。多做几次Less21、Less22这种先解码再注入的关,往后看到任何编码后的参数都会下意识先解码看看,这个习惯能帮你在其他类型漏洞里少走很多弯路。如果还想继续往下练,我建议把Less23到Less25连着刷完,那几关的报错注入和堆叠注入会让对SQL上下文的理解再深一层。