☰
sqli-labs Less-22实战:Cookie注入与Base64编码下的双引号闭合绕过
2026/10/17 8:23:38 网站建设 项目流程

sqli-labs这个系列,玩过Web安全的应该都不陌生。从Less-1一路打到Less-21,你会发现这个靶场的设计思路非常清晰:先让你用手工注入把SQL语句的拼接原理摸透,再通过各种过滤和编码方式逼着你去思考“绕过”的本质。Less-22这关,我当初打的时候差点被它骗过去——表面上看跟Less-21长得几乎一模一样,都是Cookie注入,都带Base64编码,但闭合方式换了。就这一个变化,就让很多习惯性复制粘贴Payload的人翻车了。

如果你已经卡在Less-22有一阵子了,或者刚打通Less-21但还没搞清楚为什么21和22要设计成两关,那这篇文章就是写给你的。我会从源码逻辑讲起,把双引号闭合、Base64解码、各种注入姿势全部拆开揉碎,再带你从头到尾跑一遍实操流程。文章会尽量贴近实战,有些坑是文档里不会写、只有自己踩过才知道的。

1. 关卡概览与环境准备

1.1 Less-22在整个学习路径中的定位

sqli-labs一共65关,Less-22属于第二阶段的“高级注入”部分。前面的关卡教你单引号闭合、双引号闭合、数字型注入、括号闭合,到了Less-21和Less-22,开始引入“二次传递”的概念——注入点不再直接存在于参数里,而是藏在Cookie里,而且经过了Base64编码。

换句话说,这关训练的不是“你会不会拼SQL语句”,而是“你能不能识别出真正的输入点在哪里”。很多人在Less-21能做到注入成功,是因为知道了套路:登录后把Cookie里的值拿去Base64解码,解码出来就是用户名,然后构造payload编码回去。到了Less-22,套路其实一样,唯一的差异是后端SQL语句里闭合用户名的方式从单引号变成了双引号。

但恰恰是这一“小”改动,如果没搞清楚原理,你会发现在Less-21能跑通的Payload,在Less-22里要么不报错、要么数据不对。这是整个关卡最有价值的地方:它逼着你去看SQL语句本身,而不是只会背Payload。

1.2 本地靶场环境搭建的关键点

sqli-labs的搭建方式,网上教程一大堆,这里我只说容易出问题的几个地方。

环境方面,我推荐直接用PHPStudy或者Docker。PHPStudy的优点是傻瓜式,解压即用;Docker的话更干净,用完即弃。数据库是MySQL,需要确保phpMyAdmin能正常访问,因为后面你要用SQL语句验证数据,至少要知道数据库里有哪些库、哪些表。

如果你用PHPStudy,注意PHP版本最好选择PHP5.x或者PHP7.x,不要用PHP8。sqli-labs是老项目,数据库连接用的还是mysql_connect这类老函数,在PHP7里已经废弃,PHP8直接移除,不换版本会直接白屏。这个问题我在测试环境中踩过,后来换成了PHP 7.4才正常跑起来。

数据库导入这块,sqli-labs的根目录有个sql文件夹,里面是db.sql之类的文件,直接在phpMyAdmin里导入就行。导入后确认一下数据库名是security,用户表里至少有users表,里面包含id、username、password三个字段。这个表结构后面注入要用的。

1.3 Less-22与Less-21的“双胞胎”设定

Less-21和Less-22的界面完全一致,登录框一样,登录成功后显示的页面也一样。很多人打完了21,直接进22,第一反应是“我是不是没刷新?怎么还是21?”。

两者真正的区别在后面这几行PHP代码里:

  • Less-21的SQL语句是:SELECT users.username, users.password FROM users WHERE users.username='$cuname' LIMIT 0,1
  • Less-22的SQL语句是:SELECT users.username, users.password FROM users WHERE users.username="$cuname" LIMIT 0,1

就一个单双引号的差别。但同时,Less-22的过滤函数比Less-21做得更严格一点,尤其是对单引号的过滤——因为这关查询语句用的是双引号包裹,单引号在语句里没有“闭合”的作用,而双引号才是关键。所以你会看到,在Less-21里能用的报错payload,在Less-22里经常失效,问题就出在这里。

我把两个关卡放进一个流程来说,控制在第2、3章里一起处理,这样你对比着看,印象会深得多。

2. 源码级拆解:双引号闭合与Base64的联动机制

2.1 登录逻辑与Cookie回显的完整流程

要先理解Less-22为什么注入点在Cookie,得把它的登录流程走一遍。这关的界面是一个登录表单,输入用户名和密码后会执行一次数据库查询来判断登录状态。用Dumb和Dumb这个默认账号登录成功后,页面会跳转到一个欢迎界面,显示“Your User name is: Dumb”之类的字样。

关键点来了:登录成功后,服务器会执行一段类似下面的逻辑:

$uname = base64_encode($username); setcookie('uname', $uname, time()+3600);

也就是说,你登录成功后,浏览器里会存一个名为uname的Cookie,值是你用户名的Base64编码。比如用户名是Dumb,那Cookie值就是RHVtYg==。

当你再次访问这个页面时,服务器会把Cookie里的值取出来,先做Base64解码,再拼进SQL语句。大致逻辑是这样的:

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

这里已经能看出问题了:解码后的字符串直接被拼进了SQL语句的双引号里,完全没有二次转义。所以只要我们能让Base64解码后的内容变成一段“恶意SQL片段”,就能实现注入。

你可能会想:那直接把正常的SQL注入payload用Base64编码一下,填到Cookie里不就完了?思路是这个思路,但实际执行时有个很大的坑:Base64编码的payload在服务端解码后,如果含有多字节字符或者特殊符号,编码方式不一样,结果也会不一样。这个留在第4章详细讲。

2.2 过滤函数到底挡了什么东西

Less-22的代码里有一个自定义的过滤函数,主要做了两件事:

  • 把用户输入中的单引号转义掉
  • 把一些容易出问题的关键词做了简单过滤

但关键在于:这个函数是在“解码后”执行的,而执行的位置是SQL语句的拼接点。

我直接把Less-22核心过滤部分的思路写出来:

function check_input($value) { if(!empty($value)) { $value = str_replace("'", "\\'", $value); $value = str_replace('"', '', $value); // 注意这里 } return $value; }

等一下,这关不是双引号闭合吗?如果过滤函数把双引号都删掉了,那我们怎么闭合?

这个问题我当时也卡了很久。实际情况是:过滤函数处理的时机和拼接SQL的时机有先后顺序。有些版本的sqli-labs-Less-22并没有把双引号过滤掉,而是只处理了单引号和反斜杠;如果你看的源码里确实有“双引号删除”的逻辑,那它删除的是$_POST['submit']里的双引号,而不是Cookie里解码后的双引号。要确认你的环境是什么样的,最直接的办法是打开less-22/index.php源码看一眼。

至少在我用的版本里,双引号是没有被过滤的,所以我们可以直接构造payload。这也是为什么我强调:做sqli-labs不能只看Payload,要结合源码来理解。

2.3 为什么这关比Less-21更容易翻车

Less-21的注入点是'$cuname'单引号闭合,你构造payload的时候,脑子里想的是“用单引号闭合前面的引号,然后注入,然后用注释符把后面的内容干掉”。这套思维模式一旦建立,到了Less-22就特别容易惯性翻车。

举一个我实际看到的例子:有人在Less-21用的是这样的payload:

admin' ORDER BY 1--

这段在Less-21单引号闭合下可以正常执行,因为拼进去后SQL变成了:

SELECT ... WHERE username='admin' ORDER BY 1-- '

但到了Less-22,直接把这个payload Base64编码后填进Cookie里,拼出来的SQL是这样的:

SELECT ... WHERE username="admin' ORDER BY 1-- "

注意引号不匹配。"开头,admin'里的单引号在双引号内只是一个普通字符,反而导致后面多了一个单引号没有匹配,SQL语法直接报错。而且你用的注释符是--,后面的双引号虽然被注释掉了,但前面的单引号已经造成了语法混乱。

所以正确的做法是:闭合符用",payload里的引号全部要用双引号来闭合,单引号只作为字符串内容出现。理解了这一点,Less-22就变成一道“照猫画虎”的题了。

3. 渗透实操:四种注入姿势全记录

3.1 联合查询注入:最直接的一条路

联合查询(UNION SELECT)适合在页面有回显的情况下使用。Less-22的页面会直接显示用户名,所以联合注入是最快的。

第一步:确认注入点

正常登录拿到Cookie后,你会看到一个类似uname=RHVtYg==的值。先用浏览器开发者工具或者Burp Suite把Cookie改掉,改成某个已知payload的Base64编码值,看页面反应。

先测一个最简单的闭合验证。我把原始Cookie改成"的Base64编码Ig==,提交后页面报错,数据库提示SQL语法错误。这就说明单双引号的闭合逻辑被触发了,注入点是存在的。

第二步:判断列数

构造payload:

" ORDER BY 3 --

用Base64编码后得到IiBPUkRFUiBCWSAzIC0tIA==,提交到Cookie。页面正常显示,说明列数至少是3。再把3改成4,页面报错,说明SELECT出来的列表只有3列。

第三步:注入数据

构造:

" UNION SELECT 1,2,3 --

编码后提交,页面会显示某些字段的位次。通常是第2、3列可以被回显。这时候把2的位置替换成database(),3的位置替换成version(),就能拿到数据库名和版本号。

再进一步,把database()替换成子查询,可以拖出表名:

" UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schema=database() --

整个流程走下来,比较顺利的情况下两三分钟就能拿到数据。

注意:Less-22的源码里查询语句带LIMIT 0,1,所以即使UNION查询出来了多行,页面也只显示第一行。不过这不影响我们拿到数据,因为UNION前的查询结果为空的话,显示的就是UNION部分的结果。

3.2 报错注入:ExtractValue与UpdateXML的取舍

有些情况下联合查询没有回显,这时候报错注入就派上用场了。Less-22的配置支持报错信息的回显,MySQL的报错会直接显示在页面上。

我最常用的是extractvalue:

" AND extractvalue(1,concat(0x7e,database(),0x7e)) --

这段的Base64编码是IiBBTkQgZXh0cmFjdHZhbHVlKDEsY29uY2F0KDB4N2UsZGF0YWJhc2UoKSwweDdlKSkgLS0g。提交后会看到类似这样的报错信息:

XPATH syntax error: '~security~'

0x7e是波浪号~的十六进制表示,用来在报错信息里做分隔,方便看清楚数据边界。这个技巧极其常见,用了就不容易把数据结果和其他报错信息混淆。

updatexml的写法也类似:

" AND updatexml(1,concat(0x7e,user(),0x7e),1) --

两者功能几乎一样,区别在于extractvalue最多能返回32个字符,updatexml返回的报错长度也有限制。如果要拖出来的数据超过32个字符,比如表名太多、字段太多,就要用substr配合截断,一次只取一段。

举个例子,拖数据库里所有表名:

" AND extractvalue(1,concat(0x7e,substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,30),0x7e)) --

第一次先取前30个字符,然后手工改偏移量substr(...,31,30)取下一段。这样一段段拼出来的是完整结果。虽然麻烦,但这是在没有联合查询回显的情况下最稳的做法。

3.3 布尔盲注与时间盲注:无回显的兜底方案

Less-22本身是有回显的,但实际上手的时候,你可能会遇到各种“环境意外”,比如页面压根不显示具体数据、报错被关闭、联合查询的列数判断不对等等。这时候就要用盲注。

布尔盲注的思路是:根据页面返回内容的差异,判断条件真伪。比如:

" AND (SELECT ascii(substr(database(),1,1)))=115 --

如果数据库名的第一个字符是s(ascii 115),页面显示正常,否则不显示。判断完第1个字符后,把substr(database(),1,1)改成substr(database(),2,1)判断第二个字符,以此类推。

这个过程非常枯燥,但技术上绝对可行。我一般用Burp Suite的Intruder模块做自动化,把payload里的ascii码参数作为变量,跑一遍就能得出结果。如果你不想开Burp,自己手写个Python脚本也可以,核心逻辑就是发请求、读响应、比对关键字。

时间盲注的写法是:

" AND IF(ascii(substr(database(),1,1))=115,sleep(3),1) --

如果条件成立,服务器响应时间会明显延迟到3秒以上,否则立即返回。时间盲注不需要页面有无回显、是否显示报错,只要数据库执行了SQL语句就有时间差。

但时间盲注的效率最低,一次只能判断一个布尔条件,拖几十个字符的数据可能要等很久。所以我的建议是:优先级排序为联合查询 > 报错注入 > 布尔盲注 > 时间盲注。只有在万不得已时才用时间盲注。

4. 踩坑实录与排查技巧

4.1 Base64编码的隐蔽坑:换行与加号

Base64编码看起来是个很简单的东西,但在实际注入时经常在这里出问题。

先说一下最常见的坑:编码后的字符串里可能出现+号。比如payload里含有空格、特殊符号时,编码结果有时候会带+。而Cookie值里如果出现+,在传输过程中会被解析成空格,服务端解码时就得不到你想要的原始payload。

解决办法有两个:一是把+替换成%2B进行URL编码后再放进Cookie;二是用Burp Suite直接构造请求,绕开浏览器的自动解码。我更推荐后者,因为Burp里可以精确控制Cookie值,不会出现浏览器自动转换的问题。

另一个坑是换行符。当你用命令行工具或者在线工具生成Base64字符串时,有些工具默认会在每64个字符后加一个换行符。比如:

echo "payload" | base64

这个命令在某些系统上输出的Base64会带换行符(因为echo默认加了换行)。虽然大部分时候不影响解码,但如果你把这段带换行的值直接塞进Cookie里,可能引起解析异常。最稳妥的方法是用echo -n "payload" | base64去掉尾随换行,或者用Python的base64.b64encode()来生成。

4.2 引号闭合的选择与常见误区

这一关最大的误区,就是很多人在payload里用错引号。我在本地测试的时候,老实说也在这方面浪费过时间。

记住这条铁律:Less-22的SQL语句用的是双引号包裹,所以你注入开始时的闭合符必须是双引号。比如:

  • 正确:" UNION SELECT 1,2,3 --
  • 错误:' UNION SELECT 1,2,3 --

为什么错误?因为拼进SQL后变成:

WHERE username="' UNION SELECT 1,2,3 -- "

单引号在双引号里只是普通字符,完全没有闭合效果。SQL引擎把它当成用户名的一部分,整个语句虽然能执行,但查的数据是空,页面不会回显任何注入结果。

还有一点,如果你用的Payload里面本身就需要单引号,比如database()返回的是字符串,extractvalue里需要字符串参数,那么单引号在双引号内是可以正常使用的。也就是说,闭合符用双引号,字符串内容用单引号,两者不冲突。

4.3 注释符的选择:--、#与%23

SQL注入里注释符的选择是个很容易被忽略但影响巨大的细节。

Less-22的注入点在SQL语句末尾自带LIMIT 0,1,所以我们的payload必须以注释符结尾,把后面这段LIMIT和多余的引号全部注释掉。

MySQL支持的注释符有三种:

注释符使用时注意点
--后面必须跟一个空格或控制字符,否则不生效
#直接注释到行尾,不需要额外空格
/* */块注释,Payload里尽量别用,容易把多段内容误注释掉

这里面最阴险的是--(减号减号空格)。如果你在Base64编码前用Python或者某些在线工具处理,末尾的空格可能被吃掉。没有空格的--在MySQL里根本不会触发注释,于是后面的"和LIMIT 0,1都会变成SQL语法的一部分,直接报错。

我在实测中更推荐用#,因为它不依赖末尾空格,编码成%23提交也更保险。比如:

" UNION SELECT 1,2,3 #

编码后提交,就算末尾没有空格也能正确注释。如果你对--情有独钟,那记得在写Payload时用--(末尾带一个空格),编码后再观察一下Base64字符串末尾是否有IA==这样的填充——IA==就是空格的Base64编码特征。

4.4 常见问题速查表

问题现象可能原因解决思路
修改Cookie后页面直接报SQL语法错误闭合符用错,没用双引号换成"开头
页面正常显示但数据没变化注释符没生效,LIMIT影响了结果换成#注释,重新编码
Payload在Less-21能用但22不行单双引号闭合差异检查payload开头的引号
编码后的Cookie值含+号,提交后解不出正确数据浏览器把+解析为空格用Burp直接构造请求,或URL编码
编码后末尾有换行echo/工具自动加了换行用echo -n或Python生成
报错注入返回数据超过32字符被截断extractvalue长度限制用substr分段截取
判断列数时ORDER BY不报错列数超过了实际数量但被容错逐步加数字测试边界

这张表是我在实际操作中遇到的典型问题,基本上覆盖了90%以上打不通的原因。如果你还是卡住了,建议先看一下页面的完整源码,确认过滤函数到底过滤了双引号还是只过滤了单引号,以及base64_decode执行的时机。很多时候思路没问题,就是环境不同导致的。

5. 通关后的几点心得

Less-22打完之后,我觉得最有价值的不是“会注入了”,而是把注入的思维模式升级了一层。

首先,注入的本质是拼接未过滤的输入到SQL语句里。Less-21到Less-22的变化,核心就一个字:引号。但引号的差异完全改变了payload的写法。这让我意识到,以后再遇到一个新环境,第一件事不是试之前的Payload,而是先想清楚“后端SQL语句到底长什么样”。

其次,编码只是包装,不是过滤。Base64解码并不能让输入变得安全,只是把危险延迟到了解码之后。很多人在Cookie里看到一串看不懂的Base64,就以为“这地方应该没问题”,恰恰是这种惯性思维让注入有机可乘。

最后,手工注入的能力永远不会过时。虽然自动化工具能扫出一堆漏洞,但真正面对一个需要绕过复杂逻辑的目标时,能手动判断闭合、构造payload、排除故障,才是基本功。sqli-labs的价值就在这儿——它逼着你把工具替你做的那部分事情亲手做一遍。

我个人打到这个阶段最大的体会是:每道关卡花的时间越久,越要庆幸是在靶场里踩的坑。因为生产环境里,你不会有第二次机会去试一个错误的Payload,数据库的报错日志也不会告诉你“你刚才闭合引号用错了”。现在多踩坑,实际项目中才能少踩坑。

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

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

立即咨询