环境变量注入

这篇文章只是对p牛的 我是如何利用环境变量注入执行任意命令 这篇文章进行总结

环境变量注入条件:用户可以控制环境变量,有执行命令的点但命令不可控

例子:

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

其中PHP的system调用的是系统的popen(),而popen()最终执行的是sh -c "echo hello"

sh通常只是一个软连接。在debian系操作系统中,sh指向dash;在centos系操作系统中,sh指向bash。

dash

dash源码

ENV

main函数有关环境变量的代码:

1
2
3
4
5
6
7
8
9
10
if (
#ifndef linux
getuid() == geteuid() && getgid() == getegid() &&
#endif
iflag
) {
if ((shinit = lookupvar("ENV")) != NULL && *shinit != '\0') {
read_profile(shinit);
}
}

可以看到代码会先判断iflag的值

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#define iflag optlist[3]

const char optletters[NOPTS] = {
'e',
'f',
'I',
'i',
'm',
'n',
's',
'x',
'v',
'V',
'E',
'C',
'a',
'b',
'u',
0,
0,
};

options(int cmdline)
{
char *p;
int val;
int c;
int login = 0;

if (cmdline)
minusc = NULL;
while ((p = *argptr) != NULL) {
argptr++;
if ((c = *p++) == '-') {
val = 1;
/* .... */
} else if (c == '+') {
val = 0;
} else {
argptr--;
break;
}
while ((c = *p++) != '\0') {
if (c == 'c' && cmdline) {
minusc = p; /* command is after shell args*/
} else if (c == 'l' && cmdline) {
login = 1;
} else if (c == 'o') {
minus_o(*argptr, val);
if (*argptr)
argptr++;
} else {
setoption(c, val);
}
}
}

return login;
}


setoption(int flag, int val)
{
int i;

for (i = 0; i < NOPTS; i++)
if (optletters[i] == flag) {
optlist[i] = val;
if (val) {
/* #%$ hack for ksh semantics */
if (flag == 'V')
Eflag = 0;
else if (flag == 'E')
Vflag = 0;
}
return;
}
sh_error("Illegal option -%c", flag);
/* NOTREACHED */
}

通过以上代码可以知道setoption函数会解析传入的参数,当传入了-i时,iflag就为1

结论:所以在dash中需要传入-i参数才能执行read_profile(shinit),解析ENV变量。但在php的system函数中不能使用

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

PS1、PS4

PS1、PS2、PS4这三个环境变量也会被expandstr函数解析

但是PS1有限制,需要进入交互式shell中才能执行

PS4则只能解析变量,无法执行命令

bash

bash源码

BASH_ENV

在bash中有个和ENV类似的变量:BASH_ENV

直接那上面的payload改:BASH_ENV='$(id 1>&2)' bash -c 'echo hello'

可以发现不需要-i也能执行了

分析这段代码

1
2
3
4
5
6
7
8
9
10
11
 /* 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;
}

从注释中可以看到,当使用sh时,act_like_sh的值会为1,就不会解析BASH_ENV了

1
2
3
4
if (shell_name[0] == 's' && shell_name[1] == 'h' && shell_name[2] == '\0')
act_like_sh++;
if (shell_name[0] == 's' && shell_name[1] == 'u' && shell_name[2] == '\0')
su_shell++;

所以只能在bash -c的情况下使用

ENV PS1 PROMPT_COMMAND

与dash同样,ENV PS1也能使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

/* bash */
if (act_like_sh == 0 && no_rc == 0)
{
#ifdef SYS_BASHRC
# if defined (__OPENNT)
maybe_execute_file (_prefixInstallPath(SYS_BASHRC, NULL, 0), 1);
# else
maybe_execute_file (SYS_BASHRC, 1);
# endif
#endif
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"));
}

不过必须是通过sh调用,而不是直接使用bash

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

PS1用法一样。在bash中还有一个变量PROMPT_COMMAND,设置了这个环境变量后,进入交互式模式前,会执行这个变量里包含的命令

PROMPT_COMMAND='id' bash

BASH_FUNC_xxx%%

variables.c的initialize_shell_variables函数用于将环境变量注册成SHELL的变量

1
2
3
4
#define BASHFUNC_PREFIX		"BASH_FUNC_"
#define BASHFUNC_PREFLEN 10 /* == strlen(BASHFUNC_PREFIX */
#define BASHFUNC_SUFFIX "%%"
#define BASHFUNC_SUFFLEN 2 /* == strlen(BASHFUNC_SUFFIX) */
1
2
3
4
5
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个字符等于() {

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

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

再将变量名改成system中执行的函数名,就能实现覆盖

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


但是设置BASH_FUNC_myfunc%%的方法并不完美,因为BASH_FUNC_是在Bash 4.4下引入的,centos 7的bash版本默认为Bash 4.2

Bash 4.2的补丁

可以看到在4.2下的BASHFUNC_SUFFIX是(),而不是%%

更改payload:env $'BASH_FUNC_echo()=() { id; }' bash -c "echo hello"

解决了文章开头提出的问题

总结

dash bash 条件
ENV sh或者dash下额外的 -i -c 参数
PS1 交互环境下
BASH_ENV × 可以在 bash -c 时注入任意命令 sh -c 下无效
PROMPT_COMMAND × 交互环境下
BASH_FUNC_xxx%% 4.4及以上 √
BASH_FUNC_xxx() 4.4以前 √
shellshock 存在shellshock漏洞

环境变量注入
https://www.dr0n.top/posts/7b50f6ef/
作者
dr0n
发布于
2022年8月25日
更新于
2024年3月21日
许可协议