说实话此前对 SQL 注入的理解仅仅只是皮毛。当然,目前也是,只是有了一定程度的理解。

最近好像工具用得有些过头了,需要停下来整理下工具的实现原理。

更好地理解了工具实现,才能更加心安理得地使用工具。毕竟等别人怼的时候,还能够比较安心地回道: “我用不用现成的工具只是取决于我想不想自己再写一套”

当然,毕竟成熟的工具有更多的优化,这就不是短时间内我想不想自己写的问题了,哈哈。

概要

SQL 注入漏洞究竟会产生怎么样的危害性,仅仅只是绕过某些登录账号密码的验证? 只是绕过某些访问限制实现特权内容的访问?

此前我的认识也仅仅只是停留在这里。可谁曾想,SQL SELECT 语句真的是功能强大啊。通过各种各样的字符串拼接,最终能达到的目的,可以说把整个数据库的内容拖下来也不为过。当然,这也是限定在没有实现分库分表,数据库查询账号的权限过大的前提下。

不过,这也就够了。只有真正认识到问题的严重性,最终才会想着去做出一些改善。

SQL 注入技术

基于布尔的注入

最早接触到 SQL 注入问题,还是两个月前。操作的内容也相当简单,猜解某账号的密码。

某接口提供了查询账号的功能,后台的拼接 SQL 可以简单理解成 SELECT * FROM users WHERE username = '${}' 。其中 ${} 就是直接使用的接口请求参数。

而接口的请求结果会根据 SQL 查询的结果,有或没有记录呈现两个不同的页面。

这算是最简单容易的 SQL 注入了,也是凭借个人能力直接能够想到注入点的问题了。

最先做的就是猜解存储密码字段的字段名究竟是什么? 很好,符合常理的设计,直接就是 passwd

下面就是苦力工作,利用 SQL LIKE 操作符,逐一去猜每一位。${} 的注入内容就类似 admin' AND passwd LIKE '?% 这里的问号就是逐一猜解的内容(1-9a-zA-Z + 特殊符号)

至于为什么利用 LIKE 操作符,很明确,减少猜解的次数。否则,在密码未知的情况下,即使六位密码也有猜解百万次。而 LIKE 操作符能将猜解次数降为线性,密码长度 * 字符集数

这里仅仅用了 AND,但熟悉了一个,其它就基本类似了。

如果登录也能够注入,认证 SQL 类似 SELECT * FROM users WHERE username = '${}' AND passwd = '${}',那么直接在第一个 ${} 处注入 admin' OR 1=1; --

基于时间的注入

绝大多数时候,注入绝对没有这么简单。也许注入点背后的拼接 SQL 并不对返回值产生影响。例如仅仅只是为了从 DB 查询用户信息,并打一条日志,返回的永远是静态日志(当然,我知道这个例子不恰当,无奈目前只能想到这个)。

既然拼接 SQL 总是存在,但没法给我们一个直观的注入成功 OR 失败的提示。那么,时间就成了一个最好的判断。毕竟是阻塞式的执行。

还是以 SELECT * FROM users WHERE username = '${}' 为例,使用类似 admin' AND IF(passwd LIKE '5%', SLEEP(5), 1);-- 的 PAYLOAD ,当满足 passwd 以 5 开始时,则 IF 判断进入 SLEEP(5) ,根据网页的响应时长就可以进行相应的判断。

基于报错的注入

这应该也算一种比较容易自动化的注入方式了。其实在猜解存储密码字段的字段名时,前面也是这样用的。

> SELECT * FROM users WHERE password ='1';
ERROR 1054 (42S22): Unknown column 'password' in 'where clause'

如果程序不做任何处理,对 SQL 错误直接抛出,那么,通过这个就能获得相当多的信息。

由于这部分接触不深,仅仅给出 sqlmap 提的 PAYLOAD

AND (SELECT [RANDNUM] FROM(SELECT COUNT(*),CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]',FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a)

很标准的 PAYLOAD,而且完全可以。INFORMATION_SCHEMA 库的表对所有用户都可查,因此不存在授权的问题。而且表中数据至少大于等于 3 条。

这个 PAYLOAD 一定导致报错的主因,就是对 RAND()GROUP BY 的配合应用。

Use of a column with RAND() values in an ORDER BY or GROUP BY clause may yield unexpected results because for either clause a RAND() expression can be evaluated multiple times for the same row, each time returning a different result. If the goal is to retrieve rows in random order, you can use a statement like this:

而真正想要得到的内容,通过 CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]',FLOOR(RAND(0)*2))x 得到,[QUERY] 就是真正想要注入的完整SQL串。

而这里的 DELIMITER_START DELIMITER_STOP 作为界定符,帮助程序提取 [QUERY] 得到的结果。否则,直接对请求返回的结果进行过滤可真是太困难了。

联合查询注入

联合查询,应该算是最顾名思义的注入方式。使用 UNION 在原 SQL 已经限定了查询表的前提下,获得数据库其它库表的信息。

LIKE: 1' UNION SELECT * FROM users;-- 这样的 PAYLOAD。

堆查询注入

我想这应该是最让人摸不着头脑的命名方式了。

形象化的,我们利用 PAYLOAD 来进行说明。1'; INSERT INTO users (user, passwd) VALUES ('aaa', 'aaa');--

看起来和 UNION 挺像的哈,都是补充上一个 OR 多个 SQL 。当然,也是有区别的,否则为什么会出现这样一种技术呢。

最大的区别,就是堆查询注入能够完成 UPDATE, INSERT, DELETE 等操作,毕竟,UNION 联合的只能是作为查询了(可能说法不太恰当,但姑且这样吧)

另类注入

之前的几种,我们都是利用了 SELECT 完成的注入,那么对于 INSERT, UPDATE 之类的语句是否有注入的可能呢。当然也是存在可能的。

不过,精力有限,而且目前看来也不需要做到这一步。暂时挖个坑吧。不填

SQLMAP

简单的了解了几种注入原理之后,还是要看看 SQL 注入神器的——SQLMAP

也算是尝试了好久才真正了解它的强大之处。此处强烈推荐 DVWA,Damn Vulnerable Web Application,一个用来合法攻击的工具。

部署方式也是开箱可用,只要有 docker,直接 docker run --rm -it -p 80:80 vulnerables/web-dvwa 即可完成部署。

对于将 DVWA 安全级别设置为 low 时,仅仅只需要执行下列命令就可以获取到 DB 里几乎完整的信息。

$ sqlmap -u https://127.0.0.1/vulnerabilities/sqli/\?id\=1\&Submit\=Submit\# --cookie="PHPSESSID=jhton35qahj78l3unjea9k2lf7;security=low" -v 3 --banner

当然,换一下相关获取的内容,例如把 --banner 换成 --dump ,我们借此来简单看看 SQL 注入漏洞的可怕之处

[10:41:39] [INFO] using default dictionary
do you want to use common password suffixes? (slow!) [y/N]
[10:41:40] [INFO] starting dictionary-based cracking (md5_generic_passwd)
[10:41:40] [INFO] starting 8 processes
[10:41:42] [INFO] cracked password 'abc123' for hash 'e99a18c428cb38d5f260853678922e03'
[10:41:44] [INFO] cracked password 'charley' for hash '8d3533d75ae2c3966d7e0d4fcc69216b'
[10:41:47] [INFO] cracked password 'letmein' for hash '0d107d09f5bbe40cade3de5c71e9e9b7'
[10:41:49] [INFO] cracked password 'password' for hash '5f4dcc3b5aa765d61d8327deb882cf99'
[10:41:53] [DEBUG] post-processing table dump
Database: dvwa
Table: users
[5 entries]
+---------+-----------------------------+---------+---------------------------------------------+-----------+------------+---------------------+--------------+
| user_id | avatar                      | user    | password                                    | last_name | first_name | last_login          | failed_login |
+---------+-----------------------------+---------+---------------------------------------------+-----------+------------+---------------------+--------------+
| 1       | /hackable/users/admin.jpg   | admin   | 5f4dcc3b5aa765d61d8327deb882cf99 (password) | admin     | admin      | 2018-12-15 00:42:31 | 0            |
| 2       | /hackable/users/gordonb.jpg | gordonb | e99a18c428cb38d5f260853678922e03 (abc123)   | Brown     | Gordon     | 2018-12-15 00:42:31 | 0            |
| 3       | /hackable/users/1337.jpg    | 1337    | 8d3533d75ae2c3966d7e0d4fcc69216b (charley)  | Me        | Hack       | 2018-12-15 00:42:31 | 0            |
| 4       | /hackable/users/pablo.jpg   | pablo   | 0d107d09f5bbe40cade3de5c71e9e9b7 (letmein)  | Picasso   | Pablo      | 2018-12-15 00:42:31 | 0            |
| 5       | /hackable/users/smithy.jpg  | smithy  | 5f4dcc3b5aa765d61d8327deb882cf99 (password) | Smith     | Bob        | 2018-12-15 00:42:31 | 0            |
+---------+-----------------------------+---------+---------------------------------------------+-----------+------------+---------------------+--------------+

这里就可以看到 dvwa.users 表的全部内容,甚至连简单密码都帮你完成了爆破。

更多内容可以自行尝试,说真的,这个工具相当强大。如果仅仅说我针对某型数据库(例如 MySQL)完成诸如基于报错的注入,在不考虑鲁棒性的情况下,完全可以做到。但是支持超10余种数据库,且自动完成注入的全过程…

预编译 SQL

提了这么多,究竟 SQL 注入就不能够预防吗? 当然可以,不然整个网络环境下的WEB应用不得都完蛋了。有釜底抽薪的处理方式吗,也有。

预编译 SQL 就是一个超级 OK 的解决方案。同时在使用上也能够明显提升 DB 查询效率

我相信预编译 SQL 很多人写过,毕竟借助例如 MyBatis 框架,能够快速实现 SQL 预编译语句的编写。同时在求学过程中估计也都用过 Java 原生 JDBC 的 PreparedStatement

那么,与 MySQL 交互时的预编译 SQL 是怎么样地填上了占位符呢? 先来看看再说

mysql> prepare {name} from 'SELECT * FROM users WHERE user=? AND passwd=?'; # 这里的 {name} 可以自定义命名,无需 {}

mysql> set @a='admin', @b='password';     # 声明变量,并赋值

mysql> execute {name} using @a, @b;     # 提供变量并执行预编译 SQL

我想看到这里应该就应该能够明白了吧,MySQL 对于预编译 SQL 的每一个占位符,都已经认定了是单一的元素,也就不可能存在允许打破已有预编译结果的内容了。

即使真的注入了 admin OR 1=1 之类的内容,也是会被认为这是一个完整的字符串,用来替代 user 字段或 passwd 字段,根本不可能重新拆解。

 __                    __                  
/ _| __ _ _ __   __ _ / _| ___ _ __   __ _ 
| |_ / _` | '_ \ / _` | |_ / _ \ '_ \ / _` |
|  _| (_| | | | | (_| |  _|  __/ | | | (_| |
|_|  \__,_|_| |_|\__, |_|  \___|_| |_|\__, |
                |___/                |___/