Shellshock Attack Lab
前导知识
SHELL:命令行界面的解释器
Linux下常见Shell:
- Bourne Again Shell(/bin/bash)
- C Shell(/usr/bin/csh)
- K Shell(/usr/bin/ksh)
- Bourne Again Shell(
)/bin/bash
查看目前使用的shell是否是/bin/bash:
[email protected]:/usr/lib/cgi-bin$ echo $SHELL
/bin/bash
vim编辑shell脚本:
#!/bin/bash
echo "Hello World !"
可以使用 vi/vim 命令来创建文件,扩展名为 sh(sh代表shell),#! 是一个约定的标记,它告诉系统这个脚本需要什么解释器来执行,即使用哪一种 Shell。
shellshock原理
[04/14/2021 00:28] [email protected]:~$ foo='() { echo "wdnmd"; } ;echo "8848";'
[04/14/2021 00:29] [email protected]:~$ echo $foo
() { echo "wdnmd"; } ;echo "8848";
[04/14/2021 00:29] [email protected]:~$ export foo
[04/14/2021 00:29] [email protected]:~$ bash
8848
[04/14/2021 00:29] [email protected]:~$ declare -f foo
foo ()
{
echo "wdnmd"
}
foo本来是一个shell变量,但是在export foo使其成为环境变量后,在子shell进程中,foo成为了一个函数,并且命令行自动执行了echo "8848"的命令
分析:
foo='() { echo "wdnmd"; } ;echo "8848";'
foo () { echo "wdnmd"; } ;echo "8848";
foo变量的字符串被解释成了两个命令,分别是声明了一个函数foo 和 输出字符串 8848
查看variables.c的源码:
可以看到,只要子进程在传递父进程的环境变量的时候,匹配到
() {
这四个字符,就会将其解释为函数
此后,倘若该环境变量字符串包含多个用分号
;
隔开的shell命令,
parse_and_execute
函数会执行每一条命令
因此,对于满足shellshock的情景,我们可以构造出一个特殊的字符串,使得字符串前面部分是空函数定义,后面是用分号
;
隔开的恶意指令
- 一个攻击者能够控制的环境变量,该变量以 () { 开始。
- 必须调用bash
- 系统存在bash漏洞
Task 1: Attack CGI programs
-
CGI(通用网关接口,Common Gateway Interface)
一个在Web服务器中使用的技术,是最早的可以创建动态网页内容的技术之一。它会把一个HTTP请求转化为一次shell调用。
-
Apache HTTP Server
世界上最为广泛使用的Web服务器软件,使用了UNIX shell 环境变量来保存从Web服务器传递出去的参数
(注:Apache不是指阿帕奇武装直升机)
原理
当用户将CGI URL发送到Apache Web服务器时,Apache将检查该请求
如果是CGI请求,Apache将使用fork()启动新进程,然后使用exec())函数执行CGI程序
Shellshock的原理是利用了Bash在导入环境变量函数时候的漏洞,启动Bash的时候,它不但会导入这个函数,而且也会把函数定义后面的命令执行。在有些CGI脚本的设计中,数据是通过环境变量来传递的,这样就给了数据提供者利用Shellshock漏洞的机会。
实验步骤
/usr/lib/cgi-bin
目录下使用vim编辑文件
myprog.cgi
,并设置755权限
#!/bin/bash
echo "Content-type: text/plain"
echo
echo
echo "wdnmd"
#!
告诉系统其后路径所指定的程序即是解释此脚本文件的 Shell 程序
echo "Content-type: text/plain"
是告诉shell解析的时候保持为纯文本,不用其他的解释型语言来解析
使用
ifconfig
命令得到ip地址为
192.168.32.134
为了访问该cgi程序,在命令行中输入:(curl 是发起http请求的命令行工具)
curl http://localhost/cgi-bin/myprog.cgi
用浏览器打开也可以访问该cgi程序:
阿帕奇服务器从http请求中获得user_agent的信息并将它赋值给http_user_agent环境变量,而curl -A可以更改user_agent字段。因此,我们可以通过curl -A指令修改该字段从而发起攻击
使用curl指令构造攻击,查看/
etc/passwd
目录下的内容
task2 Attack Set-UID programs
task2A
#include <stdio.h>
void main()
{
setuid(geteuid()); // make real uid = effective uid.
system("/bin/ls -l");
}
让
/bin/sh
指向
/bin/bash.
sudo ln -sf /bin/bash /bin/sh
编译文件,设为root所有,SUID权限
gcc task2.c -o task2
sudo chown root task2
sudo chmod 4755 task2
创建环境变量foo
输入
./t
,system函数通过
fork()
函数创建子进程,并得到父进程的环境变量,bash被调用并执行放在末尾的
/bin/sh
指令
通过攻击,得到一个所有者为root的
Set-UID
程序
task2B
移除
setuid(geteuid())
将无法获取root权限
setuid(geteuid())
语句将真实的UID转换为有效的UID,当UID和EUID相同的时候(详见教材第一章,p20),privmod==0,才会执行环境变量中构造的函数,详见源代码中第342行的if语句
教材p20
当UID和EUID不相等时,privmode不为0,导致不能执行环境变量中构造的函数
Task 2C
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
char ** environ;
int main()
{
char * argv[3];
argv[0] = "/bin/ls";
argv[1] = "-l";
argv[2] = NULL;
setuid(geteuid()); // make real uid = effective uid.
execve(argv[0], argv, environ);
return 0 ;
}
编译代码,并文件成为root拥有的set-uid程序
输出了ls -l的结果,但是没有获得root权限
这是因为,不同于
system()
函数调用子bash程序作为单独的进程执行命令,
execve()
函数接受三个参数:1 运行的指令,2 指令用到的参数,3 传入新程序的环境变量,该函数会直接请求操作系统(而不是shell程序执行指定的命令),因此不会执行foo后面的
/bin/sh
task3
受shellshock攻击满足的条件:
- 一个攻击者能够控制的环境变量,该变量以 () { 开始。
- 必须调用bash
- 系统存在bash漏洞
其他可能受到ShellShock攻击的情景
-
DHCP客户端
一些DHCP客户端也可以将命令传递给Bash;连接到开放的Wi-Fi网络时,易受攻击的系统可能受到攻击。DHCP客户端通常从DHCP服务器请求并获取IP地址,但也可以提供一系列附加选项。恶意DHCP服务器可以在这些选项之一中提供一个字符串,用于在易受攻击的工作站或笔记本电脑上执行代码
-
Qmail服务器
当使用Bash处理电子邮件时,邮件服务器通过一种可以利用脆弱版本的Bash的方式传递外部输入。
ShellShock漏洞的根本问题是什么?我们可以从这个漏洞中学到什么?
- 根本问题在于在一定条件下,子进程会把父进程的环境变量解释为一个函数,并且通过
函数执行用分号分割的指令,因此会受到被精心构造的指令的攻击parse_and_execute()
附录
shellshock 308~369行
initialize_shell_variables (env, privmode)
char **env;
int privmode;
{
char *name, *string, *temp_string;
int c, char_index, string_index, string_length;
SHELL_VAR *temp_var;
create_variable_tables ();
for (string_index = 0; string = env[string_index++]; )
{
char_index = 0;
name = string;
while ((c = *string++) && c != '=')
;
if (string[-1] == '=')
char_index = string - name - 1;
/* If there are weird things in the environment, like `=xxx' or a
string without an `=', just skip them. */
if (char_index == 0)
continue;
/* ASSERT(name[char_index] == '=') */
name[char_index] = '\0';
/* Now, name = env variable name, string = env variable value, and
char_index == strlen (name) */
temp_var = (SHELL_VAR *)NULL;
/* If exported function, define it now. Don't import functions from
the environment in privileged mode. */
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);
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] = '('; /* ) */
}