4.1 SQL Injection
简介
这篇文章主要探讨SQL 注入原理、利用面、如何绕过代码过滤,而对于具体的代码暂不做过多探究,若感兴趣可以参阅 不同数据库的操作笔记 和 全面的技术细节
比赛中,通常没有 WAF,而在实际渗透中,目标通常都会安装 WAF 进行保护,而关于如何绕过 WAF 进行 SQL 注入,这就留到后面的 WAF 绕过章节。
什么是 SQL 注入?
sql 注入,直白点讲就是更改原本的 sql 语句含义。所以条件就显而易见:
- 程序的 SQL 语句中,有用户可控输入。即用户通过什么方式影响程序中的 SQL 语句。
- 对用户可控输入未严谨过滤。即为什么用户能更改 SQL 语句原意。
成功的 SQL 注入能干什么?
- 修改程序逻辑。例如:改变验证逻辑,在登录过程中,可以跳过对密码检查。
- 获取数据,修改数据。例如:可以获取网站存储的用户信息。修改表中金额等敏感数据。
- 读取系统文件,写入系统文件。一般数据库管理系统都具有读写文件的能力。据此,有可能读取系统敏感例如 /etc/passwd ,或写入 shell 到系统上。
- 执行系统命令。某些数据库具有执行命令的功能。
- 与其他漏洞结合。例如利用 数据库管理系统发送请求功能来 ssrf。
等等
SQL 是什么?实际中采用它做什么?它的原理是什么?
-
sql 是 Structured Query Language 的缩写,是数据库管理系统用来操作数据的一种语言。
-
像网站的注册、登录功能就会涉及向数据库中插入、查询等操作。
-
程序通过编程语言提供的数据库 API + SQL 语句与数据库进行交互,进行数据的存取。
简单点说,就是程序利用编程语言封装好的数据库 api 与数据库管理系统进行交互。
在一开始,程序通过 tcp 连接到数据库,然后当执行 sql 命令时,会将命令通过建立起的 tcp 连接传给数据库管理系统,然后系统就执行接收到的命令。
实际渗透中存在的限制
首先是 WAF 防御。
其次是代码防御。
即使注入成功,也有可能当前数据库用户权限很低。
即使是获取了数据库 root 权限。也通常因为运行数据库管理软件的是普通用户,不一定能完全访问系统文件。
如何攻击?
三步走。检测是否存在注入点,绕过防御,然后进行利用。
经典工具 sqlmap 。
如何防御?
预处理语句 + WAF
通用思路
1. 判断是否存在注入点?
根据应用场景需要执行的功能猜想 SQL 语句是怎样的?
例如,在用户登录过程中,一般使用 select 语句进行检索。而用户注册功能中,使用 insert 向表中添加用户数据。
这里必须懂得 SQL 基础语句的含义,才能猜测功能。
常见有 Select、Updata、Insert、Delete。
2. 验证注入点是否存在?
- 当页面直接显示信息,union select 和 error 报错注入。
- 当页面不直接显示信息,那么就为盲注,此时也是有判断方法的。
- 利用 out-of-band 技术,最常见的是利用 dnslog ,但听说也有利用 icmp。但根本在于,数据库管理系统要具有能发送请求的功能(例如mysql 的load_file 函数)详见 Mysql oob 技术
- 利用推断,例如 bool 注入、与 time 注入
这块的方法其实也是获取数据的方式。
3. 绕过代码验证
见后文。再次强调一下,此处绕过的只是代码中常见有缺陷的防御手段,而针对 WAF 暂不作讨论。
4. 利用手法
另外,其实也可以通过大概研究一下 SQLMAP 的利用 payload 来学习利用手法。
绕过代码验证
针对代码的验证规则
如果代码只是进行简单的替换,则可以根据规则尝试,大小写绕过、双写关键字。
利用数据库管理系统特殊语法
例如,在需要字符串时,以下几种方法是几乎等效的。
select concat(char(0x67),char(0x75))
select 'users'=0x7573657273
SELECT char(114,111,111,116)
以上都可以表示字符串
在猜测表的列数时,也有多种方法
order by 4
select into @a,@b,@c
union select NULL,NULL
当空格被过滤
注释
select/**/*/**/from/**/users;
/*!select*//*!**//*!from*//*!users*/
/*!50110 KEY_BLOCK_SIZE=1024 */
url编码
%09 TAB 键(水平)%0a 新建一行 %0c 新的一页 %0d return 功能 %0b TAB 键(垂直)
括号
select(a)from(yz)where(a=1)
更多数据库语法笔记,详见 4
利用字符集转换特性
宽字节注入通常是用来绕过转义符反斜杠 \
,其原理是通过字符集转化,将多个字符视为一个字,从而 “吃掉” 转义符。
从原理上来讲,宽字节注入条件有两个:
- 可变长编码。这才可能发生吃掉一个字符的现象。
- 反斜杠 0x5c 要是变长编码中多字节编码中的非第一个的有效字符。
因为 Mysql 有多种编码格式,所以实际转化在哪步发生原理较为复杂,但实际攻击中只需要修改下 payload 尝试下即可。
其它
其它注入手法
二次注入
像上面 验证注入点是否存在 中说的注入手法都是属于直接注入,也就是说一次试探就可以知道结果,而二次注入需要注入恶意数据,和引用恶意数据两步。当注入恶意数据被程序进行正确过滤,而引用恶意数据时没有正确过滤时就会产生二次注入。其产生的原因是由于程序信任从数据库取出的数据。
例如 插入数据的sql
表中的内容
所以,当代码中引用这个数据时,没有进行过滤的话,就会产生漏洞。
解决办法也有两个,1. 在一次过滤前,双重转义。但这样等于会多引进几个字符。有可能发生长度截断。2. 在取出数据时进行过滤。
堆叠注入
在一次 sql 函数调用中执行多条 sql 语句。
需要调用特殊的函数或进行特殊设置。这个条件现实中一般较难满足。
例如 php 中使用 $mysqli->multi_query
或 mysqli_multi_query
函数。
例如 java 中必须在连接数据库时指定允许堆叠查询 String dbUrl = "jdbc:mysql:///test?allowMultiQueries=true";
截断
比赛中较为常见,常出现于具有注册和登录功能,可以通过注册特殊用户名来改变登录验证逻辑。
假设程序检验是否以 admin
用户登录,但是只是通过检查 sql 语句 是否有 返回结果。那么利用以下数据库特性就能达到绕过的方式。
对于 mysql 来说
'admin'='admin '
'AdMIn '='admin'
例如,注册以下用户名
'AdMIn' //大小写敏感
'admin ' //后加多余空格
'admin a'
//为预防编程语言去除末尾的空格在尾部添加a,而中间大量的空格为了利用用户名长度进行截断。
登录时,用户名 admin
而密码为新账户的密码。
预处理语句
预处理语句对关键符号进行转义的过程其实是由数据库系统实现的。详情可以抓一下 mysql 的数据包。
一般来讲,除非数据库编码存在缺陷可以使用宽字节注入,否则没有什么好的绕过方法。
注意某些语言具有模拟预处理功能,也就是说不是真正的预处理,例如 php 开关如下
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
这个特性主要跟在开发中 sql 查询效率方面有关。
Mybatis
MyBatis框架底层已经实现了对SQL注入的防御,但存在使用不当的情况下,仍然存在SQL注入的风险。
Mybatis中 ${ }
是字符串替换,在处理时会将 sql 中的 ${ }
替换为变量的值,传入的数据不会加两边加上单引号
#{ }
是预编译处理,在处理时会将 sql 中的 #{ }
替换为?,然后调用 PreparedStatement 的 set 方法来赋值,传入字符串后,会在值两边加上单引号
order by 注入
此漏洞出现频率较高,由于直接使用 #{}
会将对象转成字符串,形成 order by "user" desc
造成错误,因此很多研发会为了方便采用 ${}
来解决,而 order by的语句也会默认使用 ${q}
,从而造成SQL注入。
@RequestMapping("/mybatis/vul/order")
public List<User> orderBy(String field, String sort) {
return userMapper.orderBy(field, sort);
}
// mapper.xml语句
<select id="orderBy" resultType="com.best.hello.entity.User">
select * from users order by ${field} ${sort}
</select>
安全代码:排序映射,缺失是代码量会增加。如果想减少代码量,需要在Java层面做映射,设置一个字段/表名数组,仅允许用户传入索引值,这样保证传入的字段或者表名都在白名单里面。
in语句
和上面的order by大概相同,直接使用 #{}
会爆错,直接使用 ${}
又存在 SQL 注入漏洞。
漏洞代码
Select * from news where id in (${ids})
Select * from news where id in (#{ids})
安全代码:使用foreach 进行遍历
id in
<foreach collection="ids" item="item" open="("separatosr="," close=")">
#{ids}
</foreach>
like 模糊搜索
Mybatis 也可以把SQL语句用注解方式写到Mapper接口文件中。在模糊搜索时,直接使用 %#{q}%
会报错,部分开发为了方便直接改成 %${q}%
从而造成注入。
@Select("select * from users where user like '%${q}%'")
List<User> search(String q);
安全代码
@Select("select * from users where user like concat('%',#{q},'%')")
List<User> search(String q)
SQLMAP
关于 sqlmap ,初学阶段基础使用会就行,帮助文档很详细,后期的tamper 脚本或者二次开发就等到基础了解差不多再进行。
以下是我目前在使用时遇到的几个坑,sqlmap 版本为 1.5.1.44#dev:
-
注意无法使用 --sql-query 执行 show grants 等非 select 语句,官方的解释是因为 这个东西不能嵌入存在注入的语句
其实可以替代一下,show grants 数据其实来自下面这个表
select * from information_schema.user_privileges;
-
当要指定 https 时,要主动指定 --force-ssl ,否则即使 url 为 https 也没用。
-
当 level > 3 ,才会识别头部中参数,否则 -p 指定了也用
WAF 绕过
预留,等链接到其它章节
ctf 中的 sql 注入
-
考点: information_schema 被代码过滤,无列名注入。
由于题目可以用 sqlmap 跑出三种类型注入点 error、bool、sleep。所以解法有两种:
-
一种是通过 join 报错爆出列名,然后通过报错获取数据
(SELECT * FROM (SELECT * FROM SOME_EXISTING_TABLE JOIN SOME_EXISTING_TABLE b) a) (SELECT * FROM (SELECT * FROM SOME_EXISTING_TABLE JOIN SOME_EXISTING_TABLE b USING (SOME_EXISTING_COLUMN)) a)
-
另一种是通过 bool ,跳过列名爆破,直接逐步判断获取数据
# 爆出列数,并且除数据位,其它几位不影响结果 (select 1,2,3) > (select * from table)
疑问:在无法访问 information_schema 的情况下,能否使用 select * from table ;
可以,因为 information_schema 是一个投影,并不是真正存在的数据库。无法限制(revoke)正常 mysql 用户对此数据表的访问,见此 。
-
-
[强网杯 2019] 随便注
堆叠注入中的 sql 语句。sqlmap 探测必须要是用 select 等关键词。而当程序过滤了时,其就无法探测。
当存在堆叠注入。可以通过堆叠得到表、列等信息。此时过滤 select 等,无法获取数据。
-
利用程序已写死的 sql 语句。
selsect id,data from words where id =
利用堆叠将目标表中的 flag 列列名修改为 id 或 data,再将目标表表名修改为 words 。从而利用已有的 sql 语句获取数据。
-
利用预处理过程。其可以从字符串中生成命令,而字符串可以替换。
1';PREPARE hacker from concat(char(115,101,108,101,99,116), ' * from `1919810931114514` ');EXECUTE hacker;#
PREPARE hacker from concat('``s``','``elect``', '` `* ``from` ``1919810931114514` ');EXECUTE hacker;#
-