破壳漏洞(shellshock )分析CVE-2014-6271
前段时间的破壳漏洞让各个公司忙的够呛,漏洞也过去⼀段时间了,⼤⽜们的各种分析⽹上也是转来转去。等他们消停了,该我好好收集资料消化消化这个漏洞了。
漏洞简介
GNU Bash 4.3 及之前版本在评估某些构造的环境变量时存在安全漏洞,向环境变量值内的函数定义后添加多余的字符串会触发此漏洞,攻击者可利⽤此漏洞改变或绕过环境限制,以执⾏Shell命令。某些服务和应⽤允许未经⾝份验证的远程攻击者提供环境变量以利⽤此漏洞。此漏洞源于在调⽤Bash Shell之前可以⽤构造的值创建环境变量。这些变量可以包含代码,在Shell被调⽤后会被⽴即执⾏。破壳漏洞的严重性被定义为10级(最⾼),今年4⽉爆发的OpenSSL“⼼脏出⾎”漏洞才5级!
为什么这个漏洞如此受关注?
1、影响范围⼴,漏洞存在时间长。 Bash ,Unix shell 的⼀种。1989年发布第⼀个正式版本,原先是计划⽤在GNU 操作系统上,但能运⾏于⼤多数类Unix 系统的操作系统之上,包括Linux 与Mac OS X v10.4都将它作为默认shell 。它也被移植到Microsoft Windows 上的Cygwin 与MinGW ,或是可以在MS-DOS 上使⽤的DJGPP 项⽬。在Novell NetWare 与Android 上也有移植。
所以这个漏洞存在于⽬前主流的Linux 和MacOSX 操作系统。该漏洞会影响到与bash 交互的各种应⽤程序,如HTTP ,FTP ,DHCP 等等。 出问题的bash 代码已经存在20多年了。
漏洞原理要理解这个漏洞⾸先要知道什么是,下⾯引⽤左⽿朵耗⼦的⽂章
环境变量⼤家知道吧,这个不⽤我普及了吧。环境变量是操作系统运⾏shell 中的变量,很多程序会通过环境变量改变⾃⼰的执⾏⾏为。在bash 中要定义⼀个环境变量的语法很简单(注:=号的前后不能有空格):
然后你就可以使⽤这个变量了,⽐如:echo $var 什么的。但是,我们要知道,这个变量只是⼀个当前shell 的“局部变量”,只在当前的shell 进程中可以访问,这个shell 进程fork 出来的进程是访问不到的。
你可以做这样的测试:
上⾯的测试中,第三个命令执⾏了⼀个bash ,也就是开了⼀个bash 的⼦进程,你就会发现var 不能访问了。
为了要让shell 的⼦进程可以访问,我们需要export ⼀下:
这样,这个环境变量就会在其⼦进程中可见了。
如果你要查看⼀下有哪些环境变量可以在⼦进程中可见(也就是是否被export 了),你可使⽤env 命令。不过,env 命令也可以⽤来定义export 的环境变量。如下所⽰:
1$ var="hello world"
12
3
4
5$ var="hello coolshell"$echo$var hello coolshell $bash $echo$var
1$exportvar="hello coolshell"
有了这些基础知识还不够,我们还要知道⼀个基础知识——shell 的函数。
bash 的函数
在bash 下定义⼀个函数很简单,如下所⽰:有了上⾯的环境变量的基础知识后,你⼀定会想试试这个函数是否可以在⼦进程中调⽤,答案当然是不⾏的。
你看,和环境变量是⼀样的,如果要在⼦进程中可以访问的话,那么,还是⼀样的,需要export ,export 有个参数 -f ,意思是export ⼀个函数。如:
Bash 漏洞的测试代码
在Bash Shell 下执⾏以下代码:env x='() { :;}; echo vulnerable' bash -c "echo this is a test"如果输出:vulnerable
this is a test
表⽰存在漏洞。打了补丁会输出以下错误:
bash: 警告: x: ignoring function definition attempt bash: `x' 函数定义导⼊错误
this is a test
原理分析
Shell ⾥可以定义变量,POC 中定义了⼀个命名为x 的变量,内容是⼀个字符串:
() { :;}; echo vulnerable
⽽根据漏洞信息得知,这个漏洞产⽣于Shell 在处理函数定义时,执⾏了函数体之后的命令。但这⾥x 的值是个字符串,它是怎么转变成函数的呢。
1$envvar="hello haoel"1
2
3$ foo(){echo"hello coolshell"; }$ foo hello coolshell 1
2
3
4
5
6$ foo(){echo"hello coolshell"; }$ foo hello coolshell $bash $ foo bash: foo:commandnot found 1
2
3
4
5
6
7$ foo(){echo"hello coolshell"; }$ foo hello coolshell $export-f foo $bash $ foo hello coolshell
实际这个和Bash实现有关,在Bash中定义⼀个函数,格式为:
function function_name() {
body;
}
当Bash在初始化环境变量时,语法解析器发现⼩括号和⼤括号的时候,就认为它是⼀个函数定义:
[lu4nx@lx-pc ~]$ say_hello='() { echo hello world; }'
[lu4nx@lx-pc ~]$ export say_hello
[lu4nx@lx-pc ~]$ bash -c 'say_hello'
hello world
上⾯代码在新的Bash进程中,say_hello成了新环境中的⼀个函数,它的演变过程如下:
1、新的bash在初始时,扫描到环境变量say_hello出现⼩括号和⼤括号,认定它是⼀个函数定义
2、bash把say_hello作为函数名,其值作为函数体
typeset命令可以列出当前环境中所有变量和函数定义,我们⽤typeset看看这个字符串怎么变成函数的。继续上⾯定义的say_hello函数:[lu4nx@lx-pc ~]$ bash -c 'typeset' | fgrep -A 10 say_hello
say_hello ()
{
echo hello world
}
这⾥新启动了个Bash进程,然后执⾏了typeset,typeset会返回当前环境(新的环境)中所有定义,这⾥清楚看到say_hello被变成函数了。
漏洞产⽣原因
⽽这个漏洞在于,Bash把函数体解析完了之后,去执⾏了函数定义后⾯的语句,为啥会这样呢。
通过结合补丁,我对Bash的源码简单分析了下,Bash初始化时调⽤了builtins/evalstring.c⾥的parse_and_execute函数。是的,就等于Bash初始化环境时调⽤了类似其他⾼级语⾔中的eval函数,它负责解析字符串输⼊并执⾏。
继续看parse_and_execute的源码,关键点在这⾥:
218 else if (command = global_command)
219 {
220 struct fd_bitmap *bitmap;
它判断命令是否是⼀个定义成全局的,新的bash进程启动后,say_hello不仅被解析成函数了,还变成全局的了:
[lu4nx@lx-pc data]$ bash -c 'typeset -f'
say_hello ()
{
echo hello world
}
declare -fx say_hello
declare命令是Bash内置的,⽤来限定变量的属性,-f表⽰say_hello是⼀个函数,-x参数表⽰say_hello被export成⼀个环境变量,所以这句话的意思是让say_hello成了全局有效的函数。
其实Bash本⾝其实是想在启动时初始环境变量以及定义⼀些函数,⽽初始的⽅式就是去把变量名=值这样的赋值语句⽤eval去执⾏⼀次,如果出现了函数定义,就把它转变成函数,除此之外就不想让它⼲其他的了,可偏偏它在扫描到函数定义时,把它转变成函数的过程中不⼩⼼执⾏了后⾯的命令,这其实不是eval的错,这是做语法解析时没考虑严格,所以补丁加了这么⼀句话来判断函数体合法性:
if((flags & SEVAL_FUNCDEF) && command->type != cm_function_def)
上⾯的官⽅补丁打了补丁之后,随后被绕过
执⾏下⾯命令:
cve漏洞库1env X='() { (a)=>\' sh -c "echo date";cat echo
上⾯这段代码运⾏起来会报错,但是它要的就是报错,报错后会在你在当前⽬录下⽣成⼀个echo的⽂件,这个⽂件的内容是⼀个时间⽂本。下⾯是上⾯ 这段命令执⾏出来的样⼦。
原理分析:
官⽅提供的第⼀个补丁主要修改了:
1、参数类型和个数的限制,从注释中即可看出:
2、给builtins/evalstring.c ⽂件⾥的parse_and_execute 加⼊了类型判断:if ((flags & SEVAL_FUNCDEF) && command->type != cm_function_def)
{
不合法,不是函数定义
break;
}
...
// 逻辑为真就表明参数不合法
if (flags & SEVAL_ONECMD)break;
从上⾯即可看出补丁思路:如果不是函数定义、命令(command )超过⼀个就判为不合法。什么才算合法呢,Bypass POC 给出了答案:env X='() { (x)=>\' ./bash -c 'my echo hello'
只要函数体满⾜() {打头就⾏了。并且这条POC 也满⾜单个命令(command ),因为没出现“;”。
Bash Shell 在eval 的时候遇到语法问题(x)=被忽略了。语法出错后,在缓冲区中就会只剩下了 “>\”这两个字符
接着就来到重点了,新的bash 进程执⾏了这条命令:
$>\my echo hello
如果你了解bash,你会知道 \ 是⽤于命令⾏上换⾏的,于是相当于执⾏了
$>\my echo hello
然后在路径下⽣成了my ⽂件,内容为hello 。
Bash 语法极其怪异,让我们逐⼀分析。字符\是个转移字符,会保留后⾯跟的⽂本,\my 实际等于字符串my ,如果没有\,新的bash 进程会把
my 当作是命令。因为如果你在终端只输⼊\并回车,当前bash 进程会阻塞等待你输⼊,在POC ⾥,“输⼊”的就是my 。
1
2
3
4
5$ env X='() { (a)=>\' sh -c "echo date" ; cat echo sh: X: line 1: syntax error near unexpected token `='
sh: X: line 1: `'sh: error importing function definition for `X'Sat Sep 27 22:06:29 CST 2014
#define SEVAL_FUNCDEF 0x080 /* only allow function definitions */
#define SEVAL_ONECMD 0x100 /* only allow a single command */
字符>就是传说中的重定向,假设要把进程A的输出写⼊到⽂件B中,就写成如下:
A > B
其实你写成> B A形式也可以,不信试试:
[lu4nx@lx-pc /tmp]$ > hi date
[lu4nx@lx-pc /tmp]$ cat hi
2014年 09⽉ 27⽇星期六 01:06:06 CST
这种前缀写法我也是头⼀次见到,这次分析Shell源码,看得出它的设计极其像⼀个Lisp解析器,我以为这种写法是照顾Lisper,因为 Bash 结构基本上就是⼀个交互式(REPL)和eval,⽽Lisp解析器的核⼼就是eval,直到我看了Shell的Yacc语法分析(parse.y)后,我才恍然⼤悟。重定向的语法定义如下:
redirection: '>' WORD
{
redir.filename = $2;
$$ = make_redirection (1, r_output_direction, redir);
}
这⾥表⽰,输出的⽂件是取⾃$2,$2在这段表⽰参数WORD,如果输⼊的语句是> A B,那么WORD的实参就是A;如果输⼊的语句是A > B,那么WORD的实参就是B。
所以POC的思路就是定义⼀个语法不合法的函数体,绕过函数定义的检测代码,然后执⾏了后⾯的命令,最终让Bash在初始化的时候执⾏了>\my echo hello。
参考改编⽂章
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论