提到SQL注入,就绕不开经典的sqli-labs靶场。而Less 20到22这三关,核心玩法全在cookie注入上——注入点不在URL参数里,也不在表单字段里,而是藏在浏览器每次请求都会自动携带的Cookie头中。我第一次按GET注入的思路去测Less 21时,折腾了半小时都没反应,最后才发现服务端先把Cookie值做了一次base64解码,然后才拼进SQL语句。这三关算是我在靶场里印象最深的一组:知识点本身不算难,但顺序一旦搞错,结果就完全不对。这篇内容就围绕cookie注入展开,从环境准备、手工测试、盲注判断到防御修复,把Less 20到22的完整链路拆开讲透,适合正在刷靶场的人,也适合想把Cookie输入面搞清楚的前后端开发者。
1. 为什么盯上 Less 20-22:Cookie 注入的核心价值
1.1 Cookie 注入到底和 GET/POST 注入有什么不同
常规注入测试,大家习惯性地扫URL参数、扫POST body,却很少去改Cookie。原因也很简单:Cookie在浏览器里像是“服务器自动维护的东西”,很多开发者也默认它不可控。可实际上,Cookie的每一个键值对都来自请求头,客户端完全可以手工修改。只要服务端代码里存在类似$_COOKIE['uname']这样的取值,又把它直接拼进SQL,那么Cookie就成了和GET参数一样的注入入口。
Cookie注入和普通注入在漏洞本质上没有任何区别,都是“用户可控输入进入SQL语句拼接”,区别在于攻击面和隐蔽性。URL注入的payload会直接出现在访问日志里,安全设备盯着union、select这样的关键词就能拦截。而Cookie注入的payload在HTTP请求头里,很多日志系统默认不会记录完整的Cookie内容,或者只记录部分字段,导致它更容易蒙混过关。从防御者的角度说,只要代码里有任意一个位置读了Cookie并交给SQL,就存在一个平时容易被忽略的隐藏输入点。
1.2 这三关在靶场里的定位和难度梯度
Less 20是接单的Cookie联合查询注入。服务端直接读取Cookie中的uname字段,不做任何编码和过滤,回显还特别明显,报错信息、查询结果全都露在外面,适合用来建立Cookie注入的基本感觉。
Less 21开始上难度:服务端先对Cookie值做一次base64解码,再把解码结果拼入SQL。页面不再直接显示SQL错误,需要自己想办法判断条件真假,本质上是从“明牌注入”切到“盲注”。
Less 22是在Less 21的基础上把SQL里的单引号闭合换成了双引号闭合,其他机制几乎一致。多加一道闭合判断,很多人在这一步会卡住,因为习惯了单引号的payload,遇到双引号时容易忽略闭合位置的调整。
从Less 20到Less 22,正好串起了“简单拼接 -> 编码伪装 -> 盲注判断”的三级递进。刷完这三关,你对Cookie输入面的理解会明显比只看教程深很多。
2. 动手之前的环境准备与基础认知
2.1 本地靶场快速搭建
玩这套靶场,最稳的方案是在本地用PHP集成环境跑起来,不要直接放到公网服务器。版本搭配上,我用了PHP 7.4 + MySQL 5.7 + Apache,整体非常稳定。如果你是PHP 8,部分旧代码里的mysql_*系列函数已经被移除,可能会出现白屏或连接报错,建议换成PHP 7.4试一下。
部署步骤不复杂:把源码放到Web根目录,修改数据库连接配置,访问安装页面初始化数据库。第一次跑起来后,记得检查一下数据库账号是否用了高权限。靶场本身是故意的脆弱代码,如果映射到公网,等于把一台有漏洞的机器暴露出去,完全没必要。只在本地回环地址访问,并且不要用真实环境中常见的端口号,减少被扫描器发现的概率。
2.2 Cookie 和 HTTP 请求的基础复习
Cookie本质上就是HTTP请求头里的一行键值对,浏览器会在请求同一个域名时自动携带。服务端拿到后,通过类似$_COOKIE['uname']的方式读取,再决定后续业务逻辑。
Less 20背后的代码逻辑可以简化成这个模型:
$uname = $_COOKIE['uname']; $sql = "SELECT * FROM users WHERE username='$uname'"; $result = mysqli_query($conn, $sql);这段代码本身就是漏洞的根源:$uname完全来自客户端,没做任何过滤,直接进入SQL语句。浏览器开发者工具里的Application面板可以方便地查看和修改Cookie值,但我更推荐用curl或者抓包工具的重发功能来测试,避免浏览器自作主张地做编码转换。
3. Less 20 实操:Cookie 联合查询注入,从抓包到爆数据
3.1 通过抓包定位注入点
先用靶场给的测试账号登录。登录成功后,页面上会显示当前用户名,同时浏览器里多了一个Cookie,名称为uname,值看起来像admin之类的普通文本。
定位注入点的方法很简单:把Cookie里的uname值改成admin',刷新页面。如果页面出现SQL语法错误,说明服务端把这个值拼进了SQL语句,并且没有做任何过滤。我当时第一次测的时候看到页面直接吐出一行数据库报错,其实心里是有底的,因为这意味着Less 20的回显条件非常宽松,后面的注入逻辑可以被完整地观察。
如果页面没有报错,也不代表一定不存在注入,有可能是PHP关闭了错误显示,或者是用了mysqli的or die()把错误吞掉了。Less 20通常能看到错误信息,所以适合从报错切入。
3.2 判断查询列数和回显位置
确定注入存在之后,第一步不是急着爆库,而是先搞清楚查询结果有几列。因为后面要用的联合查询,要求前后两个查询的列数完全一致。
用order by来探测列数:把uname改成admin' order by 3--+,如果能正常回显,就继续试order by 4;如果第4列报错,说明查询返回3列。
这里有一个容易踩坑的地方:URL里的--+是注释符后面加一个空格的意思,但在Cookie值里,加号不会被当成空格处理。为了稳妥,我习惯直接用--后面跟一个空格,或者用%23表示#注释符。具体怎么选,要看服务端对Cookie值的解码行为,最好的办法是用curl直接控制整个请求头,避免浏览器或者表单编码的干扰。
列数确定后,把uname改成-admin' union select 1,2,3--。这里为什么用-admin?因为-admin这个用户名在数据库里大概率不存在,左边查询查不到数据,右边union select返回的数据就会占据回显位置。接着看页面哪个数字被显示出来,那个位置就是可控回显位。Less 20通常会把第2列或者第3列显示在用户名、密码的位置上。
3.3 爆库、爆表、爆字段的完整过程
确定回显位后,后面就是标准的联合查询流程。
查看当前数据库名:
uname=-admin' union select 1,database(),3--查看当前库里所有的表:
uname=-admin' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database()--查看目标表里的字段:
uname=-admin' union select 1,group_concat(column_name),3 from information_schema.columns where table_schema=database() and table_name='users'--最后一步,直接读出users表里的用户名和密码:
uname=-admin' union select 1,group_concat(username,0x3a,password),3 from users--整个过程在Less 20里非常顺畅,因为每个查询结果都会直接渲染到页面。练习时你会发现,这个过程和GET注入几乎没有区别,唯一的差异是payload出现的位置从URL搬到了Cookie头。这里的0x3a是冒号的十六进制表示,用来分隔两个字段,避免查询结果混在一起看不出边界。
3.4 从 Less 20 反推修复重点
Less 20最危险的地方,在于它给了攻击者一个清晰的回显通道。攻击者不需要猜,不需要盲注,只需要按顺序把information_schema里的元数据翻出来,就能一步步拿到全部数据。这种“所见即所得”的注入,危害最大,也最容易修复。
修复方法就一句话:不要拼接SQL语句。用参数化查询或者预编译语句,把Cookie值当作参数传进去,而不是当作SQL语法的一部分。后面我会专门展开讲防御方案。
4. Less 21:Base64 编码与盲注组合实战
4.1 先看清服务端对 Cookie 做了什么
Less 21登录成功后,Cookie里的uname值就不再是明文了,看起来像一串乱码。把这个值拿去base64解码,得到的内容才是真实用户名。也就是说,服务端代码里大概是这样的:
$uname = base64_decode($_COOKIE['uname']); $sql = "SELECT * FROM users WHERE username='$uname'";很多人一上来直接改Cookie,发现怎么改都无效,就是因为漏掉了base64这一层。测试的时候,你需要先想清楚:最终进入SQL语句的字符串是什么,然后对那个字符串做base64编码,再把编码后的结果放进Cookie。
这里还有一个细节:浏览器在发送Cookie时,可能会对特殊字符做URL编码。比如等号=在URL编码里是%3D,所以你看到的Cookie值可能是YWRtaW4%3D,而不是YWRtaW4=。服务端会先把URL编码解掉,拿到YWRtaW4=,再base64解码成admin。
4.2 页面不回显 SQL 错误时,怎么判断注入存在
Less 21和Less 20最大的不同,就是SQL报错不会直接显示在页面上。尝试修改Cookie为admin'后,页面可能只是显示没有数据,或者干脆回到登录态,不会给你任何数据库错误信息。
这时候要建立“真假条件”的判断标准。提交两个请求:
- 第一次,Cookie值是把
admin' and 1=1--做base64编码后的结果。如果页面正常显示用户信息,说明条件为真。 - 第二次,Cookie值是把
admin' and 1=2--做base64编码后的结果。如果页面显示内容发生变化,或者不再显示用户信息,说明条件为假。
两次请求的页面差异,就是后面所有盲注判断的基础。
4.3 手工布尔盲注流程
以判断当前数据库名的第一个字符为例。思路是逐个字符猜解,每猜一个字符用二分法缩小范围。
判断第一个字符是否大于某个ASCII值:
admin' and ascii(substr(database(),1,1))>100--如果想精确确定字符,可以逐步缩小范围,比如先用二分法确定区间,再用>和<逼近。确定第一个字符后,把substr里的第二个参数改成2,继续猜下一个字符。
手工提交时,每个payload都要先经过base64编码。比如上面这条语句,base64编码后可能是:
YWRtaW4nIGFuZCBhc2NpaShzdWJzdHIoZGF0YWJhc2UoKSwxLDEpKT4xMDAtLSA=然后把这段字符串放进Cookie的uname字段里。需要注意,如果编码结果中带有加号+,在Cookie传输时可能被服务端当成空格解码,导致base64解码失败。稳妥的做法是对+做URL编码,改成%2B,或者直接用抓包工具发送,禁止浏览器自行处理Cookie。
手工刷盲注很费时间,但非常值得。你在一次次调整payload的过程中,会真正理解布尔盲注的底层逻辑。
4.4 Base64 不是安全方案
Less 21本质上说明了一个很常见却很危险的误解:做了编码,就算做了防护。很多开发者觉得,Cookie值不是明文,攻击者看不懂,就不用过滤了。但base64只是编码,不是加密,更不是过滤。服务端把base64解码后得到的字符串,还是原封不动地进入了SQL语句。编码这层壳,对注入没有任何阻断作用。
打个比方:把攻击payload写进一封信,套上信封,收件人打开信封后还是按照信里的内容执行。信封只负责运输,不负责审查。
5. Less 22:双引号闭合与变形注入
5.1 怎么判断双引号闭合
Less 21和Less 22的表面对比非常小,但只要仔细看SQL报错或者测试闭合方式,就会发现问题。
Less 22在服务端可能是这样的代码:
$uname = base64_decode($_COOKIE['uname']); $sql = "SELECT * FROM users WHERE username=\"$uname\"";字符串边界从单引号变成了双引号。如果你还用单引号去闭合,payload里的单引号只是普通字符,根本不会改变SQL语句的结构。
判断方法很直接:用双引号试一下。把admin" and 1=1--做base64编码后提交,页面正常;再把admin" and 1=2--做base64编码后提交,页面异常。如果两次结果有明显差异,就说明双引号闭合成立。
5.2 联合查询和盲注的调整
一旦确认双引号闭合,后续的操作和Less 21几乎一致,只需要把处层闭合符从'换成"。
比如联合查询探测回显位:
-admin" union select 1,2,3--再比如盲注判断数据库名第一个字符:
admin" and ascii(substr(database(),1,1))>100--注意,payload里的双引号用来闭合,不代表SQL语句里的字符串都可以用双引号。MySQL在没有开启ANSI_QUOTES模式时,双引号和单引号都能表示字符串,但为了避免和闭合符混淆,我习惯在字符串值里继续用单引号,只在闭合位置用双引号。
Less 22真正的难点不是构造payload,而是意识到“这个靶场可能不是单引号闭合”。很多人被前几关养成了思维定式,遇到所有注入点都先加单引号,结果在Less 22上绕圈子。
5.3 自动化工具与手工操作怎么配合
刷到Less 21和Less 22,很多人会直接掏出自动化工具。工具确实快,但必须会用。
使用自动化工具扫描Cookie注入时,需要把Cookie参数明确指定,并且把测试等级调高,否则默认扫描不会覆盖Cookie头。对于base64编码的Cookie,有些工具支持直接指定解码规则,这样工具可以自动先把提交的payload编码再发送。用工具拿到的结果,也要自己判断一下数据库名和表名是否符合靶场预期,不要盲目执行脱库命令。
我个人的建议是:先用Less 20把联合查询手工跑一遍,再用Less 21把布尔盲注手工跑一遍,最后用Less 22去验证你对闭合符的判断。三步走完,你对工具的信任感会完全不同。
6. 防御视角:从 Cookie 注入反推修复方案
6.1 服务端要平等对待所有输入来源
Cookie、Header、JSON Body、文件上传参数,凡是能被客户端修改的,都算用户输入。安全工作中最容易犯的错误,就是只给URL参数和POST参数加过滤,忽略了Cookie。
在做代码审计时,我会重点搜索这些关键的系统函数:$_COOKIE、$_REQUEST、getHeader,然后沿着调用链往下看,有没有进入SQL查询、命令执行、文件读取等危险函数。只要有一条路径把Cookie数据直接拼接进了敏感操作,就是一个需要修复的点。
更彻底的做法是统一封装输入获取函数。业务代码不再直接读取$_COOKIE,而是通过一个统一的入口取参数,入口内部做类型校验、白名单校验、危险字符检查。这样即使后面新增业务,也不太容易绕过审计。
6.2 参数化查询是底线
所有修复方案里,参数化查询是最可靠、成本也最低的。
以PHP的PDO为例:
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?"); $stmt->execute([$uname]);这段代码里,$uname只是作为数据传给数据库引擎,数据库不会把它解释成SQL语法。Cookie值再奇怪,最多也只是一个字符串,不可能改变查询结构。
如果是历史遗留代码,短期内无法全部改成预编译,也要至少做三层兜底:第一,对Cookie值做严格过滤,去掉引号、分号、注释符等危险字符;第二,对数值型字段做类型转换,比如intval,从根上消除语法注入的可能性;第三,数据库账号使用最小权限,不要用root连业务库。
6.3 日志监控要覆盖 Cookie 和真实 SQL
Cookie注入的流量特征和URL注入不太一样,看访问日志的URL可能完全正常,但Cookie字段里藏着payload。很多安全设备默认只解码URL参数,对Cookie的检测能力很弱。
监控方向上,可以关注两类信号。一是请求特征:Cookie字段出现超长字符串、连续base64字符、UNION、SELECT、SUBSTR、ORDER BY等SQL关键字。二是数据库日志:开启通用查询日志或者慢查询日志后,如果SQL语句里出现了本来是Cookie值的用户名,并且带有异常语法,就说明输入点已经被人动了手脚。
企业环境里,我建议把Cookie完整写入专门的WAF日志,并配置单独的告警规则,不要把Cookie排除在检测范围外。
7. 踩坑记录与常见排查技巧
7.1 Cookie 值里的空格和引号容易丢
浏览器开发者工具直接改Cookie,很方便,但也容易被它的自动编码坑到。Cookie值里如果有空格,有些浏览器会帮你转成加号或者%20,到了服务端反而解析出错。手工测试时,我更喜欢用curl直接指定Cookie,例如:
curl -b "uname=-admin%27%20union%20select%201%2C2%2C3--%20" "http://127.0.0.1/Less-20/"这样整个Cookie头都在自己控制之下,不会再被UI层二次处理。如果你用抓包工具,记得开启原始请求编辑功能,手动修改Cookie,不要依赖可视化编辑器的智能转义。
7.2 Base64 编码顺序搞反
Less 21和Less 22的错误,大部分出在编码顺序上。正确顺序是:先构造最终进入SQL的payload,再做base64编码,最后把特殊字符URL编码后放进Cookie。一些人习惯先对整体字符串做URL编码,再base64,结果服务端解码后拿到的不是预期语句,怎么调都调不出来。
验证方法很简单:本地终端里把构造好的payload做base64,再解码一遍,确认和原始payload一致,再提交。
7.3 盲注时页面差异不明显
盲注最怕的就是真假条件页面表现一样。出现这种情况,先看整个响应体长度,而不仅仅是状态码。用curl或者抓包工具对比两个请求的响应长度,哪怕只差几个字节都可以作为条件判断依据。
如果差异实在不明显,可以换一个更“凶狠”的判定条件,比如用延时函数做时间盲注。Less 21和Less 22在部分环境下也能用sleep()判断,但比较慢,还是优先用页面内容差异。
7.4 边界提醒
这些注入技术只能用于本地靶场、CTF比赛、或者拿到明确授权的测试环境。用同样的payload去访问真实站点,属于未授权测试,法律和道德上都不被允许。练习过程中也不要拿真实网站的Cookie做实验,不仅可能违法,还会给别人带来实际损失。
我自己刷完这三关,最大的体会是:Less 20是热身,Less 21考验细节,Less 22考验耐心。Cookie注入本身不难,难的是你能不能看清服务端到底对输入做了什么处理,以及是否愿意一遍遍核对编码结果。建议把Less 20的联合查询和Less 21的盲注流程各手写三遍,再来谈自动化工具,那时候你的基本功就很扎实了。