0%

基于时间的 SQL 盲注

本博客网站由 nginx 驱动,服务器运行在 AWS 上。与其它有固定公网 IP 的服务器一样,我的服务器也时常受到外界的攻击。今天讲解一种常见的攻击方式:基于时间的 SQL 盲注(time-based blind SQL injection)。

我在 nginx 的用户访问日志 access.log 里面,经常能观测到一些可疑的请求——在 URL 中嵌入了代码。

1
2
3
4
5
6
7
8
9
10
11
# 正常的请求
GET /2015/01/28/life-in-7th-semester/

# 嵌入了代码的请求
GET /2015/if(now()=sysdate(),sleep(15),0)/28/life-in-7th-semester/

# 正常的请求
GET /2018/04/07/deep-simplicity/

# 嵌入了代码的请求
GET /2018/1%20waitfor%20delay%20'0:0:15'%20--%20/07/deep-simplicity/

和正常的请求相比,这些可疑请求用代码替换掉了原 URL 的一部分:

  • 01 被替换成了 if(now()=sysdate(),sleep(15),0)
  • 04 被替换成了 1 waitfor delay '0:0:15' -- 。注意 URL 中的 %20 是空格的转义字符。

所以这两段代码分别有什么含义呢?

经过调查,第一段代码是一个 MySQL 语句。MySQL 支持 if 条件语句,它的格式是

1
if(condition, execute when true, execute when false)

now()sysdate() 都是 MySQL 的内置函数,用来获取系统当前时间。一般来说 now()sysdate() 的返回值是相同的,也就是这个 if 语句的条件为真。此时 MySQL 会执行 sleep(15),休眠 15 秒之后返回。

第二段代码是 Microsoft SQL Server 语句,其中 waitfor delay '0:0:15' 和 MySQL 的 sleep(15) 的作用完全相同。这里连 if 语句都省去了,直接休眠。

那么,为什么要构造这样的特殊请求,发送到我的服务器上呢?

很多 web 应用依赖于 URL 获取参数(比如用户名)。攻击者可以构造含有特定代码的 URL,如果未经过滤,这些代码可能会在 SQL 的执行阶段被激活,达成特定的目的。这类攻击方式叫做「SQL 注入」。前文描述的两个可疑请求,都是攻击者在尝试 SQL 注入的例子。

攻击者向我的服务器发送上述两种特殊构造的 URL,就是想知道他们的 SQL 代码是否会得到执行。如果服务器在 15 秒之后才做出响应,那么攻击者有相当大的把握认为,嵌入的 SQL 代码被执行了。如果服务器响应的时间短于 15 秒,那么攻击者就会知道,嵌入的 SQL 代码一定没有执行。由于探测 SQL 被执行与否取决于服务器的响应时间,因此说这是一种基于时间的 SQL 注入。一旦攻击者发现 SQL 代码被执行了,就相当于找到了一个 SQL 注入的漏洞。他们之后可以利用这个漏洞控制整个 SQL 服务,盗取数据,或者进一步入侵服务器。

另一方面,我们不难察觉出,这种攻击有相当大的盲目性。

首先,攻击者不确定 URL 的哪个部分可能是 SQL 的参数。在第一个例子中,他们选取了 /2015/01/28/ 中的 01,并认为这个字段可能会被 SQL 用到。这里的 01 指的是一月份,它是否真的会参与 SQL 查询,攻击者除了尝试之外没有别的办法知晓。

其次,攻击者不确定数据库的类型。上文中提及的两个例子,第一个是针对 MySQL 的,第二个是针对 SQL Server 的。事实上我还找到了针对 PostgreSQL 的 OR 438=(SELECT 438 FROM PG_SLEEP(15))。攻击者需要尝试每一种数据库,来确定服务器使用的数据库类型。

总结一下,这种攻击方式,需要在每一个可能成为 SQL 参数的位置,尝试每一种数据库类型,进行 SQL 注入。我们把盲目注入简称为盲注,所以这种攻击方式,可以称之为「基于时间的 SQL 盲注」。

以我对近期 nginx 用户访问日志的分析,在我的服务器上,此类攻击平均每个星期会发生 2000 次左右。让我感到十分安心的是,这个博客是一个静态站点,没有使用任何 SQL 服务,所以再多这样的攻击也注定是徒劳无益的。