注入攻击是一种比较常见的攻击方式。比如之前提到的XSS攻击就是HTML注入攻击,针对数据库的注入攻击则是有SQL注入攻击。
注入攻击的本质其实就是将用户输入的数据当作代码去执行了。其包含两点:用户控制自己的输入,用户的输入被拼接到代码当中去了。
所以说在进行代码设计的时候,我们应该注意将数据与代码分离。
同时对于SQL注入而言,数据库在设计时也要注意错误回显的内容,攻击者可以通过错误回显获得自己想要知道的信息。
那如果没办法通过回显获得有效信息咋办?
这时候就需要使用盲注。
比如说:
http://127.0.0.1/sql/Less-1/?id=1 and 1=2
and 1=2 永远是一个假命题,所以查询不到id=1的记录(这儿sql语句的拼接并没有使用引号),攻击者就看不到正常的页面;
但是这并不能说明这儿存在注入点,因为一些处理逻辑和安全功能可能会使得页面返回不正常,所以需要继续构造请求:
http://127.0.0.1/sql/Less-1/?id=1 and 1=1
如果这时候页面返回正常,就说明and被执行了,自然就代表存在注入漏洞了。
这就是通过最为简单的盲注去判断是否存在注入漏洞的方法。
同时还有一种基于事件的盲注Timing Attack,一般会用到一个测试函数性能的函数:BENCHMARK(count,expr)
如果当前数据库用户(current_user)具有写权限,那么攻击者还可以将信息写入本地磁盘中,比如写入Web目录中,攻击者就有可能下载这些文件:
1 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:
1 union select "<? system($_REQUEST['cmd']);?>",2,3,4 into outfile "/var/www/html/temp/c.php" --
在针对MYSQL的注入攻击中,可以通过LOAD_FILE读取系统文件并通过INTO DUMPFILE写入本地文件。当然,这要求当前数据库用户有读写系统相应文件或目录的权限。
比如:
1 union select 1,1,LOAD_FILE('/etc/passwd'),1,1;
当然,我们也可以将文件读出后再将结果返回给攻击者(补:BLOB (binary large object)----二进制大对象,是一个可以存储二进制文件的容器。):
CREATE TABLE potatoes(line BLOB);
UNION SELECT 1,1,HEX(LOAD_FILE('/etc/passwd')),1,1 INTO DUMPFILE '/tmp/potatoes';
LOAD DATA INFILE '/tmp/potatoes' INTO TABLE potatoes;
大致流程就是用户先创建potatoes表(这要求用户有创建表的权限),然后通过LOAD_FILE将passwd文件读出,并通过INTO DUMPFILE写入系统中,然后再通过LOAD DATA INFILE将文件导入常见的表中,最后再通过注入等技巧直接去操作表数据即可。
在这儿可以想一想,为什么不能直接对'/etc/passwd'文件直接进行LOAD DATA INFILE操作呢?
除了INTO DUMPFILE,还有一个INTO OUTFILE可以使用前者适用于二进制文件,他会将目标文件写入同一行;而OUTFILE适用于文本文件。
这种写入文件的技巧经常可以用于导出一个Webshell!
下面来说说命令执行:
我们除了可以使用上述到处Webshell的方式执行命令以外,还可以使用用户自定义函数UDF:User-Defined Functions来执行命令:
即,我们可以通过从本地文件系统中导入一个共享库文件作为自定义函数:
CREATE FUNCTION f_name RETURNS INTEGER SONAME shared_library
当然,前提是我们在shared_library中已经有了这个函数的定义了,这条语句就类似于导包的作用。
需要注意的是:
- 当 MySQL< 5.1 版本时,将 .dll 文件导入到 c:\windows 或者 c:\windows\system32 目录下。
- 当 MySQL> 5.1 版本时,将 .dll 文件导入到 MySQL Server 5.xx\lib\plugin 目录下 (lib\plugin目录默认不存在,需自行创建)。
.dll文件就是shared_library。
其实UDF就和普通的database()或者是user()一样,不过可以使我们通过mysql语句去执行系统命令。具体UDF提权的步骤先留个坑。
而在MS SQL Server中,则可以直接使用存储过程'xp_cmdshell'执行系统命令。
所以在建立数据库账户时应该遵循“最小权限原则”,尽量避免给Web应用使用数据库的管理员权限。
编码问题:
字符编码的问题会导致基于字符集的注入攻击,我们直接举一个例子:
对于用户的输入,开发者往往会把输入中的敏感字符进行转义,比如在单引号双引号之前加反斜杠\;
而若数据库使用了宽字符集但是web语言中没有考虑到这些,就会出现安全问题。
当MYSQL使用了GBK编码时:
0x5c=\
0x27='
在输入进入数据库之前会进行一些安全处理,比如php中的addslashes()函数、或者当magic_quotes_gpc开启时,会在特殊字符前面增加一个转义字符‘\’
那么如果攻击者输入:
0xbf27 or 1=1
经过转义后就变成了:
0xbf5c27
而对于MYSQL而言,0xbf5c是一个字符,那么转义号\直接被吃掉了。
要解决这种问题,需要统一数据库、操作系统、Web应用所使用的字符集,以避免各层对字符的理解存在差异。统一设置为UTF-8是一个很好的方法。
其实在XSS攻击时也存在基于字符集的攻击,比如浏览器和服务器返回的字符编码不同时就会出现这种问题。
解决方法就是在HTML页面的<meta>标签中指定当前页面的charset。
SQL Column Truncation
当MYSQL的sql-mode设置为default时,即没有开启STRICT_ALL_TABLES选项时,MYSQL对于用户插入的超长值只会提示warning而不是error(error即插入不成功),这可能会导致发生一些截断问题。
比如在已有username=admin password=passwd的情况下,插入数据username=admin x password=passwd就会造成插入的username被截断,两条记录的username都是admin。这可能会造成一些处理错误甚至越权。
如何正确地防御SQL注入:
一种最容易想到的方式就是找到所有的SQL注入漏洞并修复。比如使用黑名单的方法:
mysql_real_escape_string($_GET['id'])
这个函数只会转义
'
"
\r
\n
NULL
Control-Z
但是其实是不够的,比如还有空格、括号、逗号等常用的特殊字符;
而且逗号也可以用join来绕过,空格可以用/**/来绕过,以及一些敏感字符可以使用十六进制编码;
在SQL的保留字中,有许多字符可能是正常语言中会用到的,比如order by ,就有可能造成误判。所以这种黑名单的过滤方法并不合适。
一般来说,防御SQL注入的最佳方式,就是使用预编译语句,绑定变量,这样保证了SQL语句的语义不会发生改变。在SQL语句中,变量用?表示。
如在PHP中绑定变量如下所示:
$query="INSERT INTO myCity (Name,CountryCode,District) VALUES (?,?,?)"; $stmt=$mysqli->prepare($query); $stmt->bind_param("sss",$val1,$val2,$val3); $val1='Stuttgart'; $val2='DEU'; $val3='Baden-Wuerttemberg'; /* Execute the statement */ $stmt->execute();
bind_param()中的操作就是对不同类型的参数做不同的处理并替换原始sql语句中的问号,以保证输入的变量不会对sql语句的语义产生更改。
除了使用预编译语句外,我们还可以使用安全的存储过程对抗SQL注入。使用存储过程的效果于使用预编译语句的效果类似,其区别就是存储过程需要先将SQL语句定义在数据库中。但需要注意的是,存储过程中也可能会存在注入问题,因此应该尽量避免在存储过程内使用动态的SQL语句。如果无法避免,则应该使用严格的过滤或者是编码函数来处理用户的输入数据。
但是有时候可能无法使用预编译语句或存储过程,该怎么办?这时候只能回到输入过滤和编码等方法上来。
对于输入的数据的数据类型进行检查,在很大程度上可以对抗SQL注入。
但是如果需要用户提交的是一段字符串,单纯的数据类型检查就不够用了。
一般来说,各种Web语言都实现了一些编码函数,比如OWASP ESAPI中的实现:
ESAPI.encoder().encodeForSQL(new OracleCodec(),queryparam);
当然,数据库厂商也会对自己数据库的编码函数做一些指导。
从数据库自身的就角度来说呢,应该使用最小权限原则,避免Web应用直接使用root,dbowner等高权限账户直接连接数据库。如果有多个不同的应用在使用同一数据库,则也应该为每个应用分配不同的账户。Web应用使用的数据库账户,不应该有创建自定义函数、操作本地文件的权限。
除了SQL注入以外,还有其他的注入攻击,比如XML注入:
XML与HTML都是SGML:Standard Generalized Markup Language 标准通用标记语言
XML注入与HTML注入也很相似,主要都是通过闭合标签或者其他符号来完成注入的。
同样的,XML诸如也需要满足注入攻击的两大条件:用户能控制数据的输入、程序拼凑了数据。在修补方案上,与HTML注入的修补方案也是类似的,对用户输入的数据中包含的“语言本身的保留字符”进行转义即可。
相对来说,代码注入就比较特表一点。代码注入和命令注入往往都是由一些不安全的函数或者方法引起的,其中的典型代表就是eval():
$myvar="varname"; $x=$_GET['arg']; eval("\$myvar=$x;");
注入使用的payload如下:
/index.php?arg=1;phpinfo()
存在代码注入漏洞的地方,与“后门”没什么区别。
此外,JSP的动态include也能导致代码注入。严格来说,PHP、JSP的动态include(文件包含漏洞)导致的代码执行,都可以算是一种代码注入。
所以,对抗代码注入、命令注入时需要禁用eval()、system()等可以执行命令的函数。如果一定要使用这些函数、则需要对用户的输入数据进行处理。此外,在PHP/JSP中避免动态include远程文件,或者安全地处理它。
CRLF注入:
CR是指回车符Carriage Return \r ASCII 13 0x0d
LF是指换行符Lined Feed \n ASCII 10 0x0a
CRLF常被用作不同语义之间的分隔符,因此通过“注入CRLF字符”就有可能改变原有的语义。
比如在如下的写日志的程序下:
def log_failed_login(username) log=open("access.log",'a') log.write("User login failed for: %s\n" % username) log.close()
正常情况下的记录如下:
Username login failed for: guest Username login failed for: admin
使用如下payload也可以形成这样的记录:
guest\nUsername login failed for: admin
CRLF还可以用于http头部的注入:
在HTTP协议中,HTTP头是通过“\r\n”来分隔的。因此如果服务器端没有过滤“\r\n”,而又把用户输入的数据放在HTTP头中,则有可能导致安全隐患。这种在HTTP头中的CRLF注入,又可以称为“Http Response Splitting”。这种注入最常见的情况就是把用户的输入拼接到http response的头部中了。
理论上,通过设计和实施合理的安全解决方案,注入攻击是可以彻底杜绝的。
参考书籍:《白帽子讲Web安全》