SQL 注入

产生原理
用户输入的参数打破了SQL语句的原有逻辑,导致 SQL 语句可控。
本质上就是使用 “闭合符号” 闭合掉原来的 SQL 语句,然后执行另外的 SQL 语句。( 打破原本传递数据区域的边界,插入逻辑代码。)
比如,后端的 SQL 语句如下:
1 | |
ID 为可控参数,使用闭合符号闭合以执行另外的 SQL 语句:
1 | |
闭合掉原有的 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 | |
判断注入
常见的注入判断方式:
- 逻辑判断
- 报错判断
- 延时判断
逻辑判断:
1 | |
报错判断:
- 报错判断通常尝试使用 单引号、反斜杠 等字符,破坏原有的 SQL 逻辑,导致报错的产生 ( 后端输出报错才会产生 )
延时判断:
- 通过延时函数让页面返回时间延长,以判断释放存在注入
联合注入
联合注入的关键就是 UNION 关键字,拼接 2 个 SELECT 语句。所以后端是 SELECT 语句的时候才能使用 UNION 注入。
联合查询
使用 UNION 关键字联合两个 SELECT 语句,一次查出两个 SELECT 的查询结果。
1 | |
注意:
UNION内部的每个SELECT语句必须拥有相同数量的列。- 列也必须拥有相似的数据类型。
- 每个 SELECT 语句中的列的顺序必须相同。
注入流程
- 判断字段数( 列数 )
- 确定回显点 ( 数据显示在页面的位置 )
- 数据查询
Payload
判断字段数
1 | |
原理:order by 可以将查询的结果按照字段排序后再返回数据。当 order by 一个超出索引范围的值时,由于没有索引对应的那个字段,order by 也就无法按照其字段进行排序,自然就会报错。
确定回显点
1 | |
?id=-1 可以让原因的 SELECT 查询结果为空,那么原有的查询结果就为空,UNION 拼接的 SELECT 语句的查询结果也就顶替了之前结果回显的位置。
union select 除过 1,2,3,4 之外还可以使用如 'a','b','c' 或者 null,null,null 来进行填充。
查询数据
数据查询的一般流程:
- 查询数据库名
- 查询数据表名
- 查询数据表中的列名
- 查询具体数据
Payload:
1 | |
1 | |
报错注入

主键重复报错
主键重复错误,报错时会将查询结果显示出来
Payload:
1 | |
Payload 分析:
1 | |
1 | |
遇到这样按 column_name 分组并记录行数时,它的执行过程为:
- 创建一个虚拟表,一个字段为 key(主键),一个为 count(计数),然后看分组依据 column_name,如果 key 有,就 count++,没有就插入key值再++
- 创建一个虚拟表,一个字段为 key(主键),一个为 count(计数),然后看分组依据 column_name,如果 key 有,就 count++,没有就插入key值再++
那么报错语句的流程是:
- 看x,x=0,key中没有,插入x,这里x是floor(rand(0)2)的别名,所以插入过程中又调用了floor(rand(0) 2)一次,所以插入的key应该是第二次的值1
- 看x,x=1,key中存在,count++
- 看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 | |

xpath语法报错
xpath 参数应该是 /xxx/xxx/xxx/… 这种格式,当我们写入其他格式时,xpath语法错误,报错同时返回查询结果。
1.extractvalue
1 | |
payload分析:
extractvalue(xml_frag, xpath_expr)函数使用XPath表示法从XML字符串中提取值xml_frag目标xml文档xpath_expr利用Xpath路径法表示的查找路径
注意事项:
~可以换成#、$等不满足xpath格式的字符extractvalue()能查询字符串的最大长度为32,如果我们想要的结果超过32,就要用substring()函数截取或limit分页,一次查看最多32位
靶场演示:
1 | |

2.updatexml
1 | |
updatexml(XML_document, XPath_string, new_value)改变文档中符合条件的节点的值XML_document:String格式,为XML文档对象的名称XPath_string:Xpath格式的字符串new_value:String格式,替换查找到的符合条件的数据
靶场演示:
1 | |
数值溢出报错
1 | |
Payload 分析:
exp()是以e为底的指数函数,但是如果传递的数太大了,当大于709时,exp()就会因为溢出而报错(DOUBLE 值超出)。
利用溢出特性和双层嵌套查询,使数据库将错误信息返回,(数据库优先执行括号语句)这时,双层语句内部就会执行,但是它不会回退,所以就带着第一层语句+信息返回,达到我们目的。
靶场演示:
1 | |

非法几何报错
几何函数的参数不满足,无法构成几何对象,产生报错(非法几何),报错信息中返回查询结果。
1.geometrycollection
1 | |
GeometryCollection 是由1个或多个任意类几何对象构成的几何对象。
GeometryCollection 中的所有元素必须具有相同的空间参考系(即相同的坐标系)
GEOMETRYCOLLECTION(POINT(10 10), POINT(30 30), LINESTRING(15 15, 20 20))
靶场演示:
1 | |

2.multipoint
1 | |
MultiPoint是一种由Point元素构成的几何对象集合。这些点未以任何方式连接或排序。
3.polygon
1 | |
4.multipolygon
1 | |
5.linestring
1 | |
6.multilinestring
1 | |
盲注
盲注就是页面没有回显,也没有报错回显的情况。
盲注流程
由于页面不能回显字符,也就无法直接获取到具体的字符。
需要通过 “布尔 / 延时” 的方法来判断具体的字符是什么?
一般的方法是:
- 判断要查询数据的 “个数,字符长度”
- 通过”布尔/延时”的方式判断具体的字符 ( 具体字母,ASCII 码 )
布尔盲注
Payload:
1 | |
延时盲注
1 | |
堆叠注入
堆叠查询
堆叠查询是指一次执行多条 SQL 语句,直接使用 ; 隔开。
堆叠注入
在原有的 SQL 语句后面执行一条新的 SQL 语句。
堆叠注入主要是后端执行 SQL 语句时,使用了不恰当的函数,这些函数可以执行多条 SQL 语句,从而导致堆叠注入的发送。
比如 PHP 中的函数:
1 | |
堆叠注入中,可以执行各种动词的SQL命令,比如 show, alert 这种,危害更大。
转义对抗
对于 SQL 注入的防御方法,核心思想是转义。将边界限定为单引号,参数中的内容统一进行一次转义,使之成为真正的数据。
只要数据无法逃逸出边界,便永远无法逃逸出边界,无法改变逻辑。
攻防是相对的。有转义机制就有对抗转义的方法。其中,最为典型的两个思想是 宽字节注入 和 二次注入。
宽字节注入
宽字节注入的思想是提交宽字节编码的半个字符,利用这半个字符和转义后的转义符 (\) 结合,”吃掉” 转义符,留下单独的单引号去闭合SQL语句。
宽字节注入有一个前提条件,就是服务器脚本连接数据库时使用的是”宽字节”编码,且该编码中含有低字节位,如 0x5C 的字符,即转义符 \。
比如在 GBK, Big5 这些字符集都存在宽字节注入的问题。
以 GBK 为例,%df 和转义符 %5C 合并后 %df%5C 是一个 GBK 编码中的 “運” ( 运的繁体 ) ,导致转义符被 “吃掉”,可正常闭合 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.16 后 secure_file_priv 的值默认为 NULL ,也就只能读取指定的安全文件路径。不过在 secure_file_priv 为空,也就是没有任何设置的时候,load_file 是可以读取任意路径的,再加上 windows 下的 UNC 语法就可以实现域名下的文件读取。
UNC 是一种命名惯例,主要用于再 windows 系统上指定和映射网络驱动器。其可以使用特定的标记法来识别网络资源,命名语法由服务器名、共享名和一个可选的文件路径组成。语法如下:
1 | |
Payload:
1 | |
因为域名对字符的限制,所以使用 hex() 对查询的数据进行了一下编码。
本地测试一下:
1 | |

SQL防御
采用 sql 语句预编译和绑定变量,是防御 sql 注入的最佳方法。
预编译
在 MySQL 中,可以使用预编译语句(Prepared Statements)来执行参数化查询,从而提高性能和安全性。预编译语句允许我们预先准备查询模板,然后将参数传递给该模板进行执行,避免了每次执行查询都要解析和编译 SQL 的开销,同时也可以有效防止 SQL 注入攻击。
MySql 预编译的一般步骤 ( 其他脚本语言类似,因为可以利用 MySql 的预编译达到绕过的效果这里写一下 ):
- 准备预编译语句
- 绑定参数
- 执行预编译语句
- 获取结果
- 清理资源
1 | |
使用预编译后,用户提交的参数会绑定到查询的变量位置,无法再将 SQL 语句闭合,所以预编译是防止 SQL 注入的一种方法。
但在堆叠注入的情况下,如果用户可以执行 SQL 语句,那么也可以利用 SQL 预编译来造成危害。
比如:站点存在 堆叠注入,但 WAF 过滤了 SELECT 等查询语句。这时我们可以通过 CONCAT 拼接字段构成 SQL 预编译模板,再执行 SQL 语句。
1 | |
严格的数据类型
对于数字型参数的查询,直接将参数进行强制类型转换,可以直接达到预防效果。
过滤转义
转义处理:对进入数据库的特殊字符进行转义处理,或编码转换,使其无法闭合。
过滤字符:添加过滤黑名单,匹配到就不往下执行 SQL 语句,直接返回
- 联合注入中的:
order|union|select|...... - 报错注入中的:
floor|update...... - 布尔盲注中的:
substr|length|ascii..... - 延时盲注中的:
sleep...
避免报错信息
报错信息的回显就是报错注入的产生原因。