对CVE-2014-6271 [破壳漏洞] 的一次不太深入的跟踪
@firtst:有些事,该你遇到的始终会遇到!2013年,Struts2远程代码执行漏洞闹的满城风雨时,当时还对此一无所知;2014年4月,HeartBleed掀起波涛汹涌时,较快对此予以关注,晚上跑脚本测试那些使用了HTTPS的网站(以一个旁观者的身份);2014年9月25早上8点,一改平时早上不看微博的习惯,很碰巧地发现了GNU Bash <= 4.3环境变量远程代码执行的漏洞的信息,然后该关注的关注,该传播的传播,这一天虽然没有找出有效的解决方案,也算是打了一次鸡血!鸡血打完后,还有点余热,26号早早起来,打开电脑查阅新闻及相关资讯时,发现前一天下午的各种关注已经降温了。甲方的同学也许一晚彻夜未眠,忙着修复隐患和打补丁,尽管后来证明这一晚算是白忙活了,一个官方的patch足以让你的一切努力变得不再有效。阿里在此次危机中率先分析漏洞、提出临时的补丁方案,虽然依旧是incompletement, 可以bypass,但表现出了对安全问题的足够重视、高效的响应速度和背后强大的安全团队。最终,只有等到官方的补丁出来,厂商也好,用户也罢才好把心放下来。在漏洞曝光后一天多的时间,终于出了可靠的补丁,该打补丁的赶紧动手,黑产牛们早已在这段时间不知道圈了多少“鸡场”了。当然,在实际生产环境下应用补丁之前,还得对其可靠性和可用性做一个评估呢,littlehann同学good job!
网上关于此次CVE-2014-6271的分析文章在25号当天已经有好几篇了,基本上都是简要介绍该漏洞,给出验证方式之类的:freebuf上有两篇、知道创宇出了一篇(25号之后又有一篇)、乌云知识库、还有littlehann同学,可能还有其他的分析文章,但我所知道的的就这些。这几篇文章里面,littlehann同学的文章发布得比较早,后来下午又更新了一下,总的说来,内容更丰富。http://drops.wooyun.org/papers/3064一文发布时间最迟,总结了当前的利用方式,除了在bash中的验证,还提及了ssh和远程环境的测试。之所以说littlehann同学的文章更好,并不是看他文笔好,然后其实他长得也不帅,而是其他几篇文章谈到的都是验证漏洞和利用漏洞的方法,没有探究其根源问题。没错,bash解析环境变量没有对代码和数据进行边界识别,导致任意代码执行,它对会调用bash进行解析的cgi等程序造成影响,简言之:
在sshd配置中使用了ForceCommand用以限制远程用户执行命令,这个漏洞可以绕过限制去执行任何命令。一些Git和Subversion部署环境的限制Shell也会出现类似情况,OpenSSH通常用法没有问题。
Apache服务器使用mod_cgi或者mod_cgid,如果CGI脚本在BASH或者运行在子SHELL里都会受影响。子Shell中使用C的system/popen,Python中使用os.system/os.popen,PHP中使用system/exec(CGI模式)和Perl中使用open/system的情况都会受此漏洞影响。
PHP脚本执行在mod_php不会受影响。 DHCP客户端调用shell脚本接收远程恶意服务器的环境变量参数值的情况会被此漏洞利用。
守护进程和SUID程序在环境变量设置的环境下执行SHELL脚本也可能受到影响。
任何其他程序执行SHELL脚本时用BASH作为解释器都可能受影响。Shell脚本不导出的情况下不会受影响。
参考自:
http://drops.wooyun.org/papers/3064
https://securityblog.redhat.com/2014/09/24/bash-specially-crafted-environment-variables-code-injection-attack
但是,说了这么多,我们还是不知道漏洞到底是如何产生的,不知道bash如何处理环境变量的函数和代码,不明白补丁是如何生效的,不明白如果要防止此类问题和修复此类问题我们应该怎么做。既然问题发生在代码层面,要解决问题自然也是在这个层面了,这就是littlehann同学走的稍远的地方,他分析了之前官方给出的patch代码,并跟踪到了相关的文件和函数;先不论分析得怎样,至少研究更为深入,而且有攻也有防,懂技术的人不能老是把人们的目光牵引到攻击和利用上,你还得告诉人家当你的系统出了问题时应该怎么保护自己。毕竟,知道了存在漏洞,却不知道不合修补还是一件很让人难受的事!
结合官方的patch来看,当前阶段需要重点关心的有evalstring.c、variables.c,还有common.h和patchlevel.h,这几个文件是patch中进行了修改的地方。本人在看的时候,发现源码中还有一个eval.c、execute_cmd.c,如果要对此事继续跟踪下去,这两个文件应该也需要了解。
如littlehann所言,evalstring.c中如下代码未对传入的command做边界检查:
else if (command = global_command)
{
struct fd_bitmap *bitmap;
bitmap = new_fd_bitmap (FD_BITMAP_SIZE);
begin_unwind_frame ("pe_dispose");
add_unwind_protect (dispose_fd_bitmap, bitmap);
add_unwind_protect (dispose_command, command); /* XXX */
global_command = (COMMAND *)NULL;
但是应该对此负责的,我觉得variables.c更为妥当,该文件中有两个函数:initialize_shell_variables()和parse_and_excute(),一个进行初始化,一个用来解析和执行命令:
parse_and_execute (temp_string, name, SEVAL_NONINT|SEVAL_NOHIST);
/* Ancient backwards compatibility. Old versions of bash exported
functions like name()=() {...} */
if (name[char_index - 1] == ')' && name[char_index - 2] == '(')
name[char_index - 2] = '\0';
if (temp_var = find_function (name))
{
VSETATTR (temp_var, (att_exported|att_imported));
array_needs_making = 1;
}
else
report_error (_("error importing function definition for `%s'"), name);
/* ( */
if (name[char_index - 1] == ')' && name[char_index - 2] == '\0')
name[char_index - 2] = '('; /* ) */
}
其实应该是传入什么参数无关紧要,关键在于进行解析和执行之前要判断传入的参数是否合法。就像我在一个URL中输入一个'就提示“请不要进行非法攻击”之类的,用户想输入什么都是他的自由,提示输入非法的确有效,但总归不太友好。最好的做法本人觉得,不管输入参数是否合法,后台在进行解析和执行前先鉴定其合法性,然后闷不做声的干掉非法参数,返回一个正常的结果,让整个过程变得透明。比如XSS,过滤<>()"这些可以,但黑名单可以绕过,这样子是分批处理,还不如将输入输出放在一个可控制的范围,在解析和执行之前在一个集中的关卡进行转义,当然输出同样需要如是处理。
在最初的补丁中,variables.c中做出了点小改动:
--- 319,326 ---- bash-3.2-patches
strcpy (temp_string + char_index + 1, string);
! /* Don't import function names that are invalid identifiers from the
! environment. */
! if (legal_identifier (name))
! parse_and_execute (temp_string, name, SEVAL_NONINT|SEVAL_NOHIST|SEVAL_FUNCDEF|SEVAL_ONECMD);
if (temp_var = find_function (name))
*** 359,369 **** bash-4.3-patches
strcpy (temp_string + char_index + 1, string);
! if (posixly_correct == 0 || legal_identifier (name))
! parse_and_execute (temp_string, name, SEVAL_NONINT|SEVAL_NOHIST);
!
! /* Ancient backwards compatibility. Old versions of bash exported
! functions like name()=() {...} */
! if (name[char_index - 1] == ')' && name[char_index - 2] == '(')
! name[char_index - 2] = '\0';
if (temp_var = find_function (name))
在parse_and_excute()函数之前引入了legal_identifier(name):
if (privmode == 0 && read_but_dont_execute == 0 && STREQN ("() {", string, 4))
{
string_length = strlen (string);
temp_string = (char *)xmalloc (3 + string_length + char_index);
strcpy (temp_string, name);
temp_string[char_index] = ' ';
strcpy (temp_string + char_index + 1, string);
....
这里legal_identifer()用于判断name的内容是否合法,如果说打了补丁后,依旧可以用 env X='() { (a)=>\' sh -c "echo date"; cat echo 绕过,legal_identifer()是否需要对此负责呢?这个问题有待讨论
另外进行简单的跟踪后发现unwind_protect等函数或许也有关注的必要,总之是越扯越多了,但要想真正从根源上理解这个问题恐怕还需要了解更多
后来redhat上给出了一个简单粗暴的应对bypass的方案,将bash_ld_preload.c编译成so库文件,放到lib目录,源代码如下:
/*bash_ld_preload.c*/
static void __attribute__ ((constructor)) strip_env(void);
extern char **environ;
static void strip_env()
{
char *p,*c;
int i = 0;
for (p = environ[i]; p!=NULL;i++ ) {
c = strstr(p,"=() {");
if (c != NULL) {
*(c+2) = '\0';
}
p = environ[i];
}
}
看到源代码就知道了,一目了然c = strstr(p,"=() {");
简单而粗暴,因此在给出这个方案后,redhat也进行了说明,这个东西is potentilaly dangerous,作为关键的临时应急措施尚可,非长久之计(影响bash的正常使用)
再然后,9月25号当天大家都是没辙了,甲方的同学赶紧出方案,乙方的同学也得想对策,进行一系列测试和风险评估后只能"求上天降幅于我",再等官方出补丁吧...
到了26号互联网上关于此事就冷却下来了嘛,其实这时候甲方的同学们,尤其运维人员其实也还是蛮着急的,只是没有媒体的推波助澜,表面上看起来平平稳稳,其实黑产牛们已经是开发出了各种利用方式(绕过方式呢?),从最初的user_agent到dhcp、邮件服务器,如果有人说这是yy,我可以告诉你tombkeeper(tk)在微博上贴了图证实了dhcp的利用可行,而邮件服务器则更是有直接root的,不多说(我只是个打酱油的)
26号下午(北京时间),看到官方终于出补丁,redhat上找了好久,结果还是mirror.centos.org上下载的rpm补丁。rpm包的补丁很方便,但是看不到source code,这对于想要分析源代码的liitlehann同学来说就不喜欢了。不过,大牛就是大牛,很快就弄到了change diff的文件,以bash-4.2为例:
xxx-0.patch是这样的:parse.y
@@ -2959,6 +2959,8 @@
FREE (word_desc_to_read);
word_desc_to_read = (WORD_DESC *)NULL;
+ eol_ungetc_lookahead = 0;
+
last_read_token = '\n';
token_to_read = '\n';
}
xxx-1.patch是这样的:
1 --- ../bash-4.2-orig/variables.c 2014-09-25 13:07:59.313209541 +0200
2 +++ variables.c 2014-09-25 13:15:29.869420719 +0200
3 @@ -268,7 +268,7 @@
4 static void propagate_temp_var __P((PTR_T));
5 static void dispose_temporary_env __P((sh_free_func_t *));
6
7 -static inline char *mk_env_string __P((const char *, const char *));
8 +static inline char *mk_env_string __P((const char *, const char *, int));
9 static char **make_env_array_from_var_list __P((SHELL_VAR **));
10 static char **make_var_export_array __P((VAR_CONTEXT *));
11 static char **make_func_export_array __P((void));
12 @@ -301,6 +301,14 @@
13 #endif
14 }
15
16 +/* Prefix and suffix for environment variable names which contain
17 + shell functions. */
18 +#define FUNCDEF_PREFIX "BASH_FUNC_"
19 +#define FUNCDEF_PREFIX_LEN (strlen (FUNCDEF_PREFIX))
20 +#define FUNCDEF_SUFFIX "()"
21 +#define FUNCDEF_SUFFIX_LEN (strlen (FUNCDEF_SUFFIX))
22 +
23 +
24 /* Initialize the shell variables from the current environment.
25 If PRIVMODE is nonzero, don't import functions from ENV or
26 parse $SHELLOPTS. */
27 @@ -338,28 +346,40 @@
28
29 /* If exported function, define it now. Don't import functions from
30 the environment in privileged mode. */
31 - if (privmode == 0 && read_but_dont_execute == 0 && STREQN ("() {", string, 4))
32 - {
33 - string_length = strlen (string);
34 - temp_string = (char *)xmalloc (3 + string_length + char_index);
35 + if (privmode == 0 && read_but_dont_execute == 0
36 + && STREQN (FUNCDEF_PREFIX, name, FUNCDEF_PREFIX_LEN)
37 + && STREQ (name + char_index - FUNCDEF_SUFFIX_LEN, FUNCDEF_SUFFIX)
38 + && STREQN ("() {", string, 4))
39 + {
40 + size_t name_length
41 + = char_index - (FUNCDEF_PREFIX_LEN + FUNCDEF_SUFFIX_LEN);
42 + char *temp_name = name + FUNCDEF_PREFIX_LEN;
43 + /* Temporarily remove the suffix. */
44 + temp_name[name_length] = '\0';
45
46 - strcpy (temp_string, name);
47 - temp_string[char_index] = ' ';
48 - strcpy (temp_string + char_index + 1, string);
49 + string_length = strlen (string);
50 + temp_string = (char *)xmalloc (name_length + 1 + string_length + 1);
51 + memcpy (temp_string, temp_name, name_length);
52 + temp_string[name_length] = ' ';
53 + memcpy (temp_string + name_length + 1, string, string_length + 1);
54
55 /* Don't import function names that are invalid identifiers from the
56 environment, though we still allow them to be defined as shell
57 variables. */
58 - if (legal_identifier (name))
59 - parse_and_execute (temp_string, name, SEVAL_NONINT|SEVAL_NOHIST|SEVAL_FUNCDEF|SEVAL_ONECMD);
60 + if (legal_identifier (temp_name))
61 + parse_and_execute (temp_string, temp_name,
62 + SEVAL_NONINT|SEVAL_NOHIST|SEVAL_FUNCDEF|SEVAL_ONECMD);
63
64 - if (temp_var = find_function (name))
65 + if (temp_var = find_function (temp_name))
66 {
67 VSETATTR (temp_var, (att_exported|att_imported));
68 array_needs_making = 1;
69 }
70 else
71 report_error (_("error importing function definition for `%s'"), name);
72 +
73 + /* Restore the original suffix. */
74 + temp_name[name_length] = FUNCDEF_SUFFIX[0];
75 }
76 #if defined (ARRAY_VARS)
77 # if 0
78 @@ -2537,7 +2557,7 @@
79 var->context = variable_context; /* XXX */
80
81 INVALIDATE_EXPORTSTR (var);
82 - var->exportstr = mk_env_string (name, value);
83 + var->exportstr = mk_env_string (name, value, 0);
84
85 array_needs_making = 1;
86
87 @@ -3388,22 +3408,43 @@
88 /* */
89 /* **************************************************************** */
90
91 +/* Returns the string NAME=VALUE if !FUNCTIONP or if VALUE == NULL (in
92 + which case it is treated as empty). Otherwise, decorate NAME with
93 + FUNCDEF_PREFIX and FUNCDEF_SUFFIX, and return a string of the form
94 + FUNCDEF_PREFIX NAME FUNCDEF_SUFFIX = VALUE (without spaces). */
95 static inline char *
96 -mk_env_string (name, value)
97 +mk_env_string (name, value, functionp)
98 const char *name, *value;
99 + int functionp;
100 {
101 - int name_len, value_len;
102 - char *p;
103 + size_t name_len, value_len;
104 + char *p, *q;
105
106 name_len = strlen (name);
107 value_len = STRLEN (value);
108 - p = (char *)xmalloc (2 + name_len + value_len);
109 - strcpy (p, name);
110 - p[name_len] = '=';
111 + if (functionp && value != NULL)
112 + {
113 + p = (char *)xmalloc (FUNCDEF_PREFIX_LEN + name_len + FUNCDEF_SUFFIX_LEN
114 + + 1 + value_len + 1);
115 + q = p;
116 + memcpy (q, FUNCDEF_PREFIX, FUNCDEF_PREFIX_LEN);
117 + q += FUNCDEF_PREFIX_LEN;
118 + memcpy (q, name, name_len);
119 + q += name_len;
120 + memcpy (q, FUNCDEF_SUFFIX, FUNCDEF_SUFFIX_LEN);
121 + q += FUNCDEF_SUFFIX_LEN;
122 + }
123 + else
124 + {
125 + p = (char *)xmalloc (name_len + 1 + value_len + 1);
126 + memcpy (p, name, name_len);
127 + q = p + name_len;
128 + }
129 + q[0] = '=';
130 if (value && *value)
131 - strcpy (p + name_len + 1, value);
132 + memcpy (q + 1, value, value_len + 1);
133 else
134 - p[name_len + 1] = '\0';
135 + q[1] = '\0';
136 return (p);
137 }
138
139 @@ -3489,7 +3530,7 @@
140 /* Gee, I'd like to get away with not using savestring() if we're
141 using the cached exportstr... */
142 list[list_index] = USE_EXPORTSTR ? savestring (value)
143 - : mk_env_string (var->name, value);
144 + : mk_env_string (var->name, value, function_p (var));
145
146 if (USE_EXPORTSTR == 0)
147 SAVE_EXPORTSTR (var, list[list_index]);
还有xxx-2.patch:
1 --- bash-3.2-orig/parse.y 2014-09-25 18:00:08.742924560 +0200
2 +++ bash-3.2/parse.y 2014-09-25 17:59:33.488769406 +0200
3 @@ -253,9 +253,21 @@
4
5 /* Variables to manage the task of reading here documents, because we need to
6 defer the reading until after a complete command has been collected. */
7 -static REDIRECT *redir_stack[10];
8 +static REDIRECT **redir_stack;
9 int need_here_doc;
10
11 +/* Pushes REDIR onto redir_stack, resizing it as needed. */
12 +static void
13 +push_redir_stack (REDIRECT *redir)
14 +{
15 + /* Guard against oveflow. */
16 + if (need_here_doc + 1 > INT_MAX / sizeof (*redir_stack))
17 + abort ();
18 + redir_stack = xrealloc (redir_stack,
19 + (need_here_doc + 1) * sizeof (*redir_stack));
20 + redir_stack[need_here_doc++] = redir;
21 +}
22 +
23 /* Where shell input comes from. History expansion is performed on each
24 line when the shell is interactive. */
25 static char *shell_input_line = (char *)NULL;
26 @@ -424,13 +436,13 @@
27 {
28 redir.filename = $2;
29 $$ = make_redirection (0, r_reading_until, redir);
30 - redir_stack[need_here_doc++] = $$;
31 + push_redir_stack ($$);
32 }
33 | NUMBER LESS_LESS WORD
34 {
35 redir.filename = $3;
36 $$ = make_redirection ($1, r_reading_until, redir);
37 - redir_stack[need_here_doc++] = $$;
38 + push_redir_stack ($$);
39 }
40 | LESS_LESS_LESS WORD
41 {
42 @@ -487,14 +499,14 @@
43 redir.filename = $2;
44 $$ = make_redirection
45 (0, r_deblank_reading_until, redir);
46 - redir_stack[need_here_doc++] = $$;
47 + push_redir_stack ($$);
48 }
49 | NUMBER LESS_LESS_MINUS WORD
50 {
51 redir.filename = $3;
52 $$ = make_redirection
53 ($1, r_deblank_reading_until, redir);
54 - redir_stack[need_here_doc++] = $$;
55 + push_redir_stack ($$);
56 }
57 | GREATER_AND '-'
58 {
59 @@ -3767,7 +3779,7 @@
60 case CASE:
61 case SELECT:
62 case FOR:
63 - if (word_top < MAX_CASE_NEST)
64 + if (word_top + 1 < MAX_CASE_NEST)
65 word_top++;
66 word_lineno[word_top] = line_number;
67 break;
parse.y的用途本人还需要了解一下,源码中第一行注释:/* Yacc grammar for bash. */
在xxx-1.patch中,可以看到为了进行边界区分,这里定义了可能包含shell函数的环境变量的前缀和后缀:
+/* Prefix and suffix for environment variable names which contain
+ shell functions. */
+#define FUNCDEF_PREFIX "BASH_FUNC_"
+#define FUNCDEF_PREFIX_LEN (strlen (FUNCDEF_PREFIX))
+#define FUNCDEF_SUFFIX "()"
+#define FUNCDEF_SUFFIX_LEN (strlen (FUNCDEF_SUFFIX))
如果是要导出的函数,定义它,然后不要从特权模式下的环境中导入函数,我老是记得有个权限最小原则来着:
if (privmode == 0 && read_but_dont_execute == 0 && STREQN ("() {", string, 4))
- {
- string_length = strlen (string);
- temp_string = (char *)xmalloc (3 + string_length + char_index);
/*撤掉原本对是否为特权模式和是否执行的判断,引入前面新增的特性:FUNCDEF_PREFIX,增强if语句条件为真时的约束条件*/
+ if (privmode == 0 && read_but_dont_execute == 0
+ && STREQN (FUNCDEF_PREFIX, name, FUNCDEF_PREFIX_LEN)
+ && STREQ (name + char_index - FUNCDEF_SUFFIX_LEN, FUNCDEF_SUFFIX)
+ && STREQN ("() {", string, 4))
+ {
/*将前缀和后缀应用到实际代码中*/
+ size_t name_length
+ = char_index - (FUNCDEF_PREFIX_LEN + FUNCDEF_SUFFIX_LEN);
+ char *temp_name = name + FUNCDEF_PREFIX_LEN;
+ /* Temporarily remove the suffix. */
+ temp_name[name_length] = '\0';
- strcpy (temp_string, name);
- temp_string[char_index] = ' ';
- strcpy (temp_string + char_index + 1, string);
+ string_length = strlen (string);
+ temp_string = (char *)xmalloc (name_length + 1 + string_length + 1);
/*新增memcpy(),多层次防御*/
+ memcpy (temp_string, temp_name, name_length);
+ temp_string[name_length] = ' ';
+ memcpy (temp_string + name_length + 1, string, string_length + 1);
不要从环境中导入非法标识符的函数名,虽然允许定义为shell变量,前面提到过legal_identifier注定要发挥作用的
- if (legal_identifier (name))
- parse_and_execute (temp_string, name, SEVAL_NONINT|SEVAL_NOHIST|SEVAL_FUNCDEF|SEVAL_ONECMD);
+ if (legal_identifier (temp_name))
+ parse_and_execute (temp_string, temp_name,
+ SEVAL_NONINT|SEVAL_NOHIST|SEVAL_FUNCDEF|SEVAL_ONECMD);
- if (temp_var = find_function (name))
+ if (temp_var = find_function (temp_name))
{
VSETATTR (temp_var, (att_exported|att_imported));
array_needs_making = 1;
}
else
report_error (_("error importing function definition for `%s'"), name)
后面的代码就先不一段段贴出来了,还没对此做深入研究
xxx-2.patch又回归到了parse.y,其中有一段:
/* Where shell input comes from. History expansion is performed on each
line when the shell is interactive. */
- static char *shell_input_line = (char *)NULL;
从change diff得到的这三个patch来看,显然核心在于variables.c和parse.y,前者之前已经提到过很多次,parse.y发挥的具体作用还需要我们去了解
这里识别代码的边界的问题采用了前缀、后缀的方式,这是比较可行的,我们无法根据一个变量或语句的起始是'或{ 来进行有效的判断,env X='() { (a)=>\' sh -c "echo date"; cat echo就是利用未闭合的{来进行代码注入的。提到这里,在shell中:
赋值语句 : var=value
变量解析 : ${var}
命令解析 : ${command}
双引号 " " : 变量内容,并做转义
单引号 ' ' : 变量内容,但不做转义
反单引号 ` ` : 同 $()
End Of File : "EOF"
; 指示多个命令序列化执行
& 指示该命令在后台异步执行
笔者由于手头工作的原因,这两天时间里只对此次Bash环境变量代码执行的漏洞进行了有限的关注和分析,可以说是一次浅尝辄止的跟踪与接触,对情况了解的并不是很深入,包括前面的一些分析也都只是大致看了几眼。诚如余弦所说,CVE-2014-0627的风险等级是10,而四月份的HeartBleed则为5,虽然因为利用方式相对比较复杂,媒体关注度不是很高,但后劲非常大。此外,针对此次漏洞的新的利用方式也将不断涌现,若不予以关注和警惕,届时将有无数服务器和网站沦为受害者。基于以上种种原因和笔者自己的兴趣,后续会依旧关注和研究这个问题一段时间。
目前的互联网看起来相对比较平静,没有波涛澎湃,但实际上暗潮涌动,我们所见的只为冰山一角。从HeartBleed被誉为世纪性大漏洞以来就已经注定了网络环境的不太平,要说世纪大漏洞此次的Bash漏洞才真的是世纪性的,Bash在互联网之前就已经存在了,而且这个漏洞潜伏了这么久。大家可以想象,这个两个所谓跨纪元的漏洞前后相隔不到半年,可以预测,在未来的几年里网络安全环境会更加跌宕起伏,不太平,作为信息安全工作者如果我们不做出更多的努力,就会有更多的用户成为受害者。
最后再插一句:Unix系统的发明者肯·汤普森在Unix诞生十年之后的一次演讲中曾经说过自己当时在系统中留了一个后面,而十年之后该后门没人发现,依旧可用。再回顾一下Bash,我们用了它二十几年,却全然无知其存在的隐患,我们最信赖的,结果却是伤我们最深的!
##补充
关于此次漏洞的讨论和分析可以参考如下地址:
https://bugzilla.redhat.com/show_bug.cgi?id=1141597#c23
https://www.reddit.com/r/netsec/comments/2hbxtc/cve20146271_remote_code_execution_through_bash/
算上时差,笔者感觉在前沿信息方面虽然有了互联网,但国内始终落后国外半拍,大家可以自己感受下
##updated in 2014-09-29
笔者没有想过26号的补丁出来之后就能一切都安安稳稳了,但是觉得起码在一段时间里,不管出现什么样的利用方式,漏洞应该算是可以防住了。但是上午看到一篇微博,提到第二次的补丁似乎有某种方法可以绕过(狗肉盖饭:http://weibo.com/mbqdpz?source=webim)
当时看到这张图不免有几分震惊,而且还看到GNU官方除了第三次补丁,以bash4.2为例:http://ftp.gnu.org/gnu/bash/bash-4.2-patches/bash42-050
9月27号(国外时间)更新,部分于28号又做了修改(http://ftp.gnu.org/gnu/bash/bash-4.2-patches/bash42-049),看完patch后,发现的确做了些变更
+ #define BASHFUNC_PREFIX "BASH_FUNC_"
+ #define BASHFUNC_PREFLEN 10 /* == strlen(BASHFUNC_PREFIX */
+ #define BASHFUNC_SUFFIX "%%"
+ #define BASHFUNC_SUFFLEN 2 /* == strlen(BASHFUNC_SUFFIX) */
当然还有其他,补丁出来后首先自然想的是否公开了其他绕过技巧,但是因为在网上并没有看到,所以猜测要不是处于评估阶段,就是这第三次的补丁只是为了增强安全性
查找资料找到以下几个有参考价值的地址:
http://seclists.org/oss-sec/2014/q3/781
https://access.redhat.com/security/cve/CVE-2014-7187
http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2014-7187
http://lcamtuf.blogspot.com/2014/09/bash-bug-apply-unofficial-patch-now.html
这里看到,上面的四个链接中两个提到的都是CVE-2014-7187,而不是最初的CVE-2014-6271,这是因为从最初的CVE-2014-6271到不完整的修复方案,中间又出现了CVE-2014-7169,CVE-2014-7186,直到目前的CVE-2014-7187,一下子冒出这么多来,让人怀疑官方的修复不够认真
根据redhat官方的披露,CVE-2014-7187的详情:
“An off-by-one error was discovered in the way Bash was handling deeply nested flow control constructs. Depending on the layout of the .bss segment, this could allow arbitrary execution of code that would not otherwise be executed by Bash.”
还有描述如下:
“Not vulnerable. The concrete .bss segment layout generated by GCC and the linker only allows overwriting a variable whose contents is already controlled by the attacker.”
结合seclists中的讨论,上次的补丁限制过于严格,影响了向后兼容性,破坏了Bash一些困难会用到的正常功能,最终CVSS的评分为4.6分
shellshock的网站上:https://shellshocker.net/ 公布了几个CVE相应的exploit,内容如下:
Exploit 1 (CVE-2014-6271)
There are a few different ways to test if your system is vulnerable to shellshock. Try running the following command in a shell.
env x='() { :;}; echo vulnerable' bash -c "echo this is a test"
If you see "vulnerable" you need to update bash. Otherwise, you should be good to go.
Exploit 2 (CVE-2014-7169)
Even after upgrading bash you may still be vulnerable to this exploit. Try running the following code.
env X='() { (shellshocker.net)=>\' bash -c "echo date"; cat echo ; rm -f echo
If the above command outputs the current date (it may also show errors), you are still vulnerable.
Exploit 3 (???)
Here is another variation of the exploit. Please leave a comment below if you know the CVE of this exploit.
env -i X=' () { }; echo hello' bash -c 'date'
If the above command outputs "hello", you are vulnerable.
Exploit 4 (CVE-2014-7186)
bash -c 'true <<EOF <<EOF <<EOF <<EOF <<EOF <<EOF <<EOF <<EOF <<EOF <<EOF <<EOF <<EOF <<EOF <<EOF' ||
echo "CVE-2014-7186 vulnerable, redir_stack"
Exploit 5 (CVE-2014-7187)
(for x in {1..200} ; do echo "for x$x in ; do :"; done; for x in {1..200} ; do echo done ; done) | bash ||
echo "CVE-2014-7187 vulnerable, word_lineno"
需要的同学可以使用上述的几个exploit,如果测试均无问题,那就可以安个心了
如果这几个exploit还不尽兴,可以看一下FireEye的 Shellshock in the Wild
##附:
其实很想跟到Bash的源代码中去探测个究竟,但是近7M的源代码,上千个文件,虽然只需要关注主要的几个源文件如:evalstring.c、variables.c,但由于C语言中没有类,函数调用使用非常多,因此给阅读源码造成了一定的困难,好在发现了sourcecodebrowser。这个网站做的事情就是帮助分析源代码,对函数、变量、结构体等等进行区分,可以同时查看源代码,还有一点很好的是给出一个很直观和形象的函数调用和被调用关系图,越是使用得多和重要的函数,其关系图越复杂,下图为parse_prologue调用的函数关系图:
/*此次事件虽然基本到此为止了,但笔者对此事有较大的关注热情,之后还会对此继续关注和跟踪一段时间*/