9月24日,广泛存在于Linux的bash漏洞曝光。因为此漏洞可以执行远程命令,所以极为危。危害程度可能超过前段时间的心脏流血漏洞。
漏洞编号CVE-2014-6271,以及打了补丁之后被绕过的CVE-2014-7169,又称ShellShock漏洞。
漏洞起因:
要是用一句话概括这个漏洞,就是代码和数据没有正确区分。
此次漏洞很像SQL注入,通过特别设计的参数使得解析器错误地执行了参数中的命令。这其实是所有解析性语言都可能存在的问题。
1 |
env x= '() { :;}; echo vulnerable' bash -c "echo this is a test" |
2 |
#env [OPTION]... [NAME=VALUE]... [COMMAND [ARGS]...] |
这是网上最早流传出来验证漏洞的代码。如果漏洞存在,那个”echo vulnerable”会被执行,屏幕上会输出“vulnerable”。我们分析这句shell命令的语法。
1 |
+ env 'x=() { :;}; echo vulnerable' bash -c 'echo this is a test' |
2 |
#这是在bash -x下执行,执行命令时把命令和它们的参数显示出来。 |
可以看出,这个语句原本的意图是使用env命令创建一个临时环境,然后在里面执行一个bash命令。
从解析上看,bash解析并没有问题,语法是正常的。所以应该是env命令处理变量名时的漏洞。
bash可以将shell变量导出为环境变量,还可以将shell函数导出为环境变量!当前版本的bash通过以函数名作为环境变量名,以“(){”开头的字串作为环境变量的值来将函数定义导出为环境变量。此次爆出的漏洞在于bash处理这样的“函数环境变量”的时候,并没有以函数结尾“}”为结束,而是一直执行其后的shell命令。
所以,在某种环境,bash会在给导出的函数定义处理环境时执行用户代码。
漏洞原理:
黑客定义了这样的环境变量(注:() 和 { 间的空格不能少):
1 |
export X= '() { echo "inside X"; }; echo "outside X";' |
2 |
#可以用export看到这个函数变量。 |
3 |
declare -x X= "() { echo \"inside X\"; }; echo \"outside X\";" |
当我们开启一个子bash时。
1 |
[sean@localhost ~]$ export X= '() { echo "inside X"; }; echo "outside X";' |
2 |
[sean@localhost ~]$ bash |
3 |
outside X |
变量中的代码被执行了。(以上参考coolshell.cn)
漏洞就在于创建子bash时,注入代码被执行。所以,回忆一下那个漏洞验证代码,env后有一个bash,我试过多次,不直接跟bash都不会触发注入代码。上面的例子很好的解释了这一点,注入代码是在子bash载入用户环境变量时执行的,env后直接跟bash就是为了在env创建的临时环境中创建子bash以触发漏洞。
导出函数的代码:
01 |
/* |
02 |
Initialize the shell variables from the current environment. If PRIVMODE is nonzero, |
03 |
don't import functions from ENV or parse $SHELLOPTS. |
04 |
从当前环境安装shell变量,如果PRIVMODE非零,不要从ENV或者prase $SHELLOPTS导入函数。 |
05 |
*/ |
06 |
void initialize_shell_variables (env, privmode) char **env; int privmode; |
07 |
{ |
08 |
... |
09 |
create_variable_tables (); |
10 |
|
11 |
/* |
12 |
从ENV环境变量中获取参数 |
13 |
*/ |
14 |
for (string_index = 0; string = env[string_index++]; ) |
15 |
{ |
16 |
char_index = 0; |
17 |
name = string; |
18 |
while ((c = *string++) && c != '=' ) ; //查找等号'=' |
19 |
if (string[-1] == '=' ) |
20 |
char_index = string - name - 1; |
21 |
22 |
/* If there are weird things in the environment, like `=xxx' or a |
23 |
string without an `=', just skip them. |
24 |
如果这里环境中有诡异的事情,像'=xxx'或者一个没有'='的字符串,就忽略它们。 |
25 |
*/ |
26 |
if (char_index == 0) |
27 |
continue ; |
28 |
29 |
/* ASSERT(name[char_index] == '=') */ |
30 |
name[char_index] = '\0' ; |
31 |
/* |
32 |
Now, name = env variable name, string = env variable value, and char_index == strlen (name) |
33 |
name=env变量名,string=env变量值,char_index= name长度 |
34 |
*/ |
35 |
36 |
/* |
37 |
If exported function, define it now. Don't import functions from the environment in privi |
38 |
leged mode. |
39 |
如果导出函数,先定义。不要在特权模式下把函数导入到环境。 |
40 |
*/ |
41 |
if (privmode == 0 && read_but_dont_execute == 0 && STREQN ( "() {" , string, 4)) //匹配 |
42 |
string中的 "(){" 用于判断这是一个函数 |
43 |
{ |
44 |
string_length = strlen (string); |
45 |
temp_string = ( char *)xmalloc (3 + string_length + char_index); |
46 |
|
47 |
strcpy (temp_string, name); |
48 |
temp_string[char_index] = ' ' ; |
49 |
|
50 |
strcpy (temp_string + char_index + 1, string); //字符串拼接 |
51 |
|
52 |
/* |
53 |
这句是关键,initialize_shell_variables对环境变量中的代码进行了执行,由于它错误的信任的外部发送的 |
54 |
数据,形成了和SQL注入类似的场景,这句代码和PHP中的eval是类似的,黑客只要满足2个条件 |
55 |
1. 控制发送的参数,并在其中拼接代码 |
56 |
2. 黑客发送的包含用户代码的参数会被无条件的执行,而执行方不进行任何的边界检查 |
57 |
|
58 |
这就是典型的数据和代码没有进行正确区分导致的漏洞 |
59 |
*/ |
60 |
parse_and_execute (temp_string, name, SEVAL_NONINT|SEVAL_NOHIST); //执行函数 |
61 |
|
62 |
// Ancient backwards compatibility. Old versions of bash exported functions like name()=() {...} |
63 |
// 古老的向下兼容,老版本的bash导入函数类似name()=(){...} |
64 |
if (name[char_index - 1] == ')' && name[char_index - 2] == '(' ) |
65 |
name[char_index - 2] = '\0' ; |
66 |
|
67 |
if (temp_var = find_function (name)) |
68 |
{ |
69 |
VSETATTR (temp_var, (att_exported|att_imported)); |
70 |
array_needs_making = 1; |
71 |
} |
72 |
else |
73 |
report_error (_( "error importing function definition for `%s'" ), name); |
74 |
|
75 |
/* ( */ |
76 |
if (name[char_index - 1] == ')' && name[char_index - 2] == '\0' ) |
77 |
name[char_index - 2] = '(' ; /* ) */ |
78 |
} |
79 |
} |
80 |
} |
代码中,直接把变量值传入parse_and_execute(),网上不少分析都是说这个就是漏洞所在。但是表示略有疑问,为何定义要直接传进去执行呢?真正分离开数据和代码可能才是堵上漏洞的根本办法。
漏洞危害:
其实,一开始知道这个漏洞,还是觉得奇怪,语句本来就是在shell上执行的。后来发现,安全问题在于远程访问时,某些协议正好会组成类似测试代码的语句,触发此漏洞。
因为CGI会把post包中的变量导入成用户变量,并在里面启动子bash,这样就触发了漏洞。
此利用方式,需要以下条件:
- [远程]服务会调用bash。(创建bash子进程)
- [远程]服务允许用户定义环境变量。
- [远程]服务调用子bash时加载了用户定义的环境变量。
注入流程:
看上去,目前CGI已经基本不用了。但是只要符合上面所说的三个条件,还是会触发漏洞。所以,此漏洞危害巨大,又因为是源码级的漏洞,影响广泛。
关于那个被绕过的第一个补丁:
在爆出漏洞第二天,补丁就出来了。
第一个补丁对传入的变量做了一个判断。类似于防SQL注入的过滤方法。第一个补丁判断如果不是函数定义,或者命令(command)超过一个就判为不合法。自然,这种方法很可能就被绕过。
很快,绕过漏洞的测试代码也跟着出来。
1 |
[sean@localhost ~]$ env X='() { (a)=>\' sh -c "echo date" ; cat echo |
2 |
sh: X:行1: 未预期的符号 `=' 附近有语法错误 |
3 |
sh: X:行1: `' |
4 |
sh: `X' 函数定义导入错误 |
5 |
2014年 09月 28日 星期日 21:33:50 CST |
当时发现了代码执行后,所在目录莫名其妙出了个echo文件,echo文件存的就是那个日期。
coolshell的解释:
- X='() { (a)=>\’ 这个不用说了,定义一个X的环境变量。但是,这个函数不完整啊,是的,这是故意的。另外你一定要注意,\’不是为了单引号的转义,X这个变量的值就是 () { (a)=>\
- 其中的 (a)=这个东西目的就是为了让bash的解释器出错(语法错误)。
- 语法出错后,在缓冲区中就会只剩下了 “>\”这两个字符。
- 于是,这个神奇的bash会把后面的命令echo date换个行放到这个缓冲区中,然后执行。
相当于在shell 下执行了下面这个命令:
1 |
[sean@localhost ~]$ >\ |
2 |
> echo date |
bash中“\”用于命令上下换行,所以,实际执行的是:
1 |
[sean@localhost ~]$ > echo date |
bash中“>”符号是输出重定向,这里是把标准输出重定向到echo文件。date是我们费劲心思让它执行的命令。执行结果就是执行了date命令,输出重定向到echo文件。
怎么绕过补丁的:
补丁判断如果不是函数定义,或者命令(command)超过一个。
在代码中,”(){“表示了这是函数定义,命令只有一个,因为分号只有一个。这就绕过补丁的检测。
能想到这个攻击的真的是个变态,连语法出错都用上了。
转载请注明:旅途@KryptosX » ShellShock漏洞原理分析