内容纲要

前言

这几天抽空研究学习了 CVE-2016-3714 这个漏洞 , 并分析了恶意代码的执行过程 . 该漏洞是一个非常经典的漏洞 , Google 上有很多师傅已经写过了源码级分析报告 , 但都没有明确说明恶意代码完整的执行流程 .

本文将从 ImageMagick 主函数( ConvertMain() 函数 ) 开始 , 逐步分析源码及恶意代码被拼接执行的具体原因 .


CVE-2016-3714

漏洞环境

根据 NVD( National Vulnerability Database , 国际漏洞数据库 ) 可以得知 , 当 ImageMagick 的版本符合以下条件时 , 该应用可能存在 ImageTragick 漏洞

  1. Version < 6.9.3_10

  2. 7.0.0_0 < Version < 7.0.1_1


漏洞复现

在 Vulhub 上存在该漏洞的复现环境 , 其对应 ImageMagick 的版本为 6.9.2 , 符合对应版本 .

docker build && docker-compose up -d 即可成功搭建环境 .


任意代码执行

访问 http://127.0.0.1:80 , 即可看到当前目录下存在三个文件 .

进入 docker 环境的 bash , 查看 /tmp 目录下的文件

然后访问 demo.php 文件

再次查看 /tmp 目录下的文件 , 会发现生成了 success 文本文件 , 其中存放了 /etc/passwd 的内容

  • demo.php 文件内容

    调用了 shell_exec() 函数来执行系统命令 , identify 命令主要用于获取一个或多个图像文件的格式和特性 , 我们来看 vul.jpg 的文件内容

  • vul.jpg 文件内容

    该文件明显不是一个 .jpg 格式文件 , 但能看到其中执行了系统命令 .


反弹 Shell

上文是通过访问 demo.php 来调用 vul.jpg , 从而执行恶意代码 . 现在我们手工上传 vul.jpg 图片 , 同时修改上传数据包

访问 upload.php , 并且打开 BurpSuite , 监听 80 端口 , 同时上传 vul.jpg 文件 , 查看上传数据包 .

在本地构造一个反弹 Shell 的链接 , 并且通过 Base64 编码

 注意 , 构造链接时需要使用绝对路径 " /bin/bash "

在数据包中替换对应部分 , 如下所示

 url() 中的路径可以任意指定 , 不过需要使用 https 协议

 base64 -d 进行 base64 解码操作

同时 SSH 远程服务器 , 并且监听对应端口

然后发送修改后的请求 , 即可拿到目标主机的 Shell

能看到我们反弹了目标主机的 Shell , 并且能执行系统命令 !

你此时肯定有很多疑问 , 比如为什么恶意文件需要构造成上述格式 ? 为什么恶意文件中的代码可以被系统执行 ? 为什么一定要使用 HTTPS 协议 ? 等等 .

下面我将从源码入手 , 对上述这些问题一一作出解答 .


漏洞源码分析

一般来说 , 我们分析一个程序的执行流程 , 会先去看它的主函数( 即 " Main() " 函数 ) , 对于旧版本的 ImageMagick 而言 , 其主函数位于 utilities 目录的 convert.c 文件中 , 我们从这入手

 在较新的 ImageMagick 版本中 , 已不再使用 Main() 函数 . 因此我这里选择使用 ImageMagick 6.9.0_0 这个版本的源代码来分析 . 
  1. utilities/convert.c : 90

    这里定义了 main() 函数 , 该函数非常简单 , 仅调用了 ConvertMain() 函数 , 并返回其返回值 . 我们追踪 ConvertMain() 函数 , 看一看它的作用 .

  2. utilities/convert.c : 67

     ConvertMain() 函数定义了 image_info 参数 , 该参数十分重要 , 贯穿整个解析过程 . 
    
     MagickCoreGenesis() : 该函数用于初始化 ImageCore 环境 ( magick/magick.c : 1232 )
    
     MagickCoreTerminus() : 该函数用于销毁 ImageCore 环境 ( magick/magick.c:1360 )
    
     AcquireExceptionInfo() 函数用于程序异常处理 , 不去管它 . 而 AcquireImageInfo() 函数用于创建 Image_Info 结构 , 查看定义( magick/image.c : 329 )发现其调用了 GetImageInfo() 函数( magick/image.c : 1281 ) , 该函数用于初始化 Image_Info 结构 .
    
     此时 , Image_Info 参数已经被初始化完毕 . 并被带入了 MagickCommandGenesis() 函数 中. 这个函数是重点 , 注意其第二个参数为 ConvertImageCommand , 后面将要用到
  3. wand/mogrify.c : 112

     MagickCommandGenesis() 函数会将图像处理选项应用于命令行中指定的图像 , 它会寻找并处理命令行中的特殊选项( 例如 -debug , -bench 等 ) , 然后将处理后的选项参数传递到真正处理请求的函数中 . 即 command 参数代表真正处理请求的函数
    
     这里是一个函数动态调用的过程 , 程序会根据不同的 command 参数调用不同的函数 , 而command 参数来源于 MagickCommandGenesis() 函数的第二个参数 . 
    
     从上文了解到这里传入的值是 ConvertImageCommand , 因此现在会调用 ConvertImageCommand() 函数
  4. wand/convert.c : 492

    这里定义了 ConvertIMageCommand() 函数 , 该函数会读取一张或者多张图片 , 并且对不同的图片应用不同的操作过程 , 根据结果以不同的格式写出图像 .

    在读取图片的时候 , 会调用 ReadImages() 函数 , 我们追踪该函数 .

  5. magick/constitute.c : 845

    这里定义了 ReadImages() 函数 . 该函数会读取一个或者多个图像 , 将他们放入一个图像列表并返回 . 在函数定义的最后 , 会调用 ReadImage() 函数

    继续追踪 ReadImage() 函数

  6. magick/constitute.c : 352

    这里定义了 ReadImage() 函数 , 该函数将从文件或者文件句柄中读取图像或图像序列 . 根据函数注释 , 其过程分为三个步骤

    1. 根据文件前缀或者后缀来确定图像类型
    2. 根据不同的图像类型调用不同的图像处理程序
    3. 在调用图像处理程序时 , 会释放并解码委托程序来处理图像 .

    那什么是委托程序呢 ? 在继续往下看代码之前 , 你要先了解 ImageMagick 委托机制 , 现在由我来解释一下 .


ImageMagick Delegate( 委托 )机制

我在 Google 上没有找到关于 ImageMagick Delegate 机制的文档 . 因此我这里无法给出具体的定义 . 不过 , 该机制实现的功能是很容易理解的 .

  • ImageMagick 是一个功能强大的开源图形处理软件 , 可以用来读 , 写 , 处理超过 90 种的图片文件 .

  • 由于 ImageMagick 功能强大 , 性能较好 , 对很多编程语言都有拓展支持 , 所以在程序开发( PHP , Ruby , NodeJs , ... )中被广泛使用 , 而且许多图像处理插件都依赖于 ImageMagick 库 .

  • ImageMagick 之所以支持那么多的文件格式 , 是因为它内置了非常多的图像处理库 . ImageMagick 将这些图像处理库命名为 " Delegate "( 委托 ) . 每个 Delegate 对应一种格式的图片文件 .

  • 这些 Delegate 是不能单独处理图片文件的 , 不过它可以根据不同格式的图片文件 , 利用系统函数 system() 来调用外部程序对图片文件进行处理 .

举个例子 , 在 www/source/delegates.xml 文档中定义了 ImageMagick Delegate 机制的默认配置 . 我们来看一下该文件的内容 .

  • 文件格式转换

    ImageMagick Delegate 程序在处理图片格式文件时 , 会根据其内容将其自动转换为 ImageMagick 内置的某种图片格式 .

  • 占位符的定义

    虽然 ImageMagick Delegate 程序会根据不同的图片格式调用不同的系统命令 , 但是这些命令大都需要一些共同的参数( 比如 文件名 , 文件大小 ... ) . 使用占位符 , 能很大程度上减小代码的复杂性 .

  • 委托程序的定义

    ImageMagick Delegate 程序会根据转换后不同的文件格式 , 调用不同的系统命令 .

此时 , 如果您对安全问题比较敏感 , 就立刻能发现问题 : ImageMagick Delegate 程序调用了系统命令 , 还拼接了占位符字符串 . 那么如果攻击者可以控制对应占位符的值 , 那么就可能拿到命令执行漏洞 .

那么是否存在这种可能呢 ? ( 废话 , 不存在就不会有这篇文章了 )

前辈们注意到了 HTTPS 协议 , 来看下 ImageMagick Delegate 程序是如何解析该协议的 .

ImageMagick Delegate 程序会调用 Curl 这个系统命令来解析 HTTPS 内置格式 , 来分析一下这个指令 .

curl -s -k -o %o https:%M

 -s : 安静模式 , curl 不会显示进程进度和错误信息

 -k : 允许不安全的服务器建立 SSL 连接

 -o : 指定输出文件

  %o , %M : 两个占位符 , %o 已经被定义为 输出的图片文件 , 而我们现在还不确定    %M 代表什么 .

总的来说 , 若想利用该委托程序 , 被解析的文件就必须被解析为 HTTPS 这个内置格式 . 也就是说 , 图片内容至少要包含一个 HTTPS 链接 .

那么是否存在某种格式的图片内容可以包含 HTTPS 链接呢 ? 前辈们注意到了 MVG 图片格式 .


MVG 图片格式

MVG 图片格式是什么 ? 我只听过 SVG ( Scalable Vector Graphics ) .

事实上 , MVG ( Magick Vector Graphics ) 是 ImageMagick 默认支持的一种图片格式 , MVG 与 SVG 格式类似 , 可以通过文本形式写入矢量图的内容( 想要深入了解该格式可以访问 MVG文件摘要 )

因此 , 我们在分析 MVG 图片内容时可以参考 SVG 图片内容 . 为了便于研究学习 , 我们查看漏洞复现时用到的 vul.jpg

 你可能会疑问 , 这是 JPG 格式图片 , 并不是 MVG 格式图片啊

 上文我提到过 , ImageMagick Delegate 程序在进行文件格式转换时 , 是根据 文件内容 来判断其对应的内置格式 , 而不是根据文件扩展名

 因此 , 你可以将文件名转换为任何你需要的格式 , 这也是该漏洞攻击面非常广的原因之一

那么 ImageMagick 是如何处理 MVG 格式文件呢 ? 我们来看代码 .


  1. coders/mvg.c

    该文件中定义了 ImageMagick 解析 MVG 格式文件的过程

    • coders/mvg.c : 92

      ImageMagick 会先根据文件内容前20个字符是否为 push graphic-context 来判断该文件是否为 MVG 文件 .

    • coders/mvg.c : 128

      然后调用 ReadMVGImage() 函数来创建一个渐变图像 , 该图像会被初始化为文件名指定的 Xserver 颜色范围 .

      在创建图像时 , 会通过 viewbox 参数来确定新图像大小 .

    • coders/mvg.c : 200

      在渲染图片时 , 会调用 DrawImage() 函数 , 我们追踪该函数 .

  2. magick/draw.c : 1694

    这里定义了 DrawImage() 函数 , 其中定义了很多解析关键字和解析规则 .

    image over 是 SVG 格式文件的语法( 定义于 coders/svg.c : 2299 ) , 但也可以在 MVG 格式文件中使用 . 我们可以在解析规则中找到反弹Shell时使用的 fill 关键字

    经过一系列操作 ,再次调用了 ReadImage() 函数

  3. magick/constitute.c : 523

    又回到了这里 , 现在将会调用 InvokeDelegate() 函数 , 并且在调用时将开头的 https 删除了 . 我们追踪该函数 .

  4. magick/delegate.c : 1279

    这里定义了 InvokeDelegate() 函数 , 该函数会用适当的 Image 属性来替换所有嵌入的格式设置字符( 占位字符 ) , 并执行拼接后的命令 , 来看一下具体步骤 .

  5. magick/property.c : 3261

    先来看 InterpretImageProperties() 函数 , 该函数用来解析一些特殊字符 .

    值得一提的是 , 在 3532 行 , 该函数调用了 GetMagickProperty() 函数来解析占位字符 , 我们追踪该函数 .

    可以看到这里对 "o" 和 "M" 两个占位符进行了解析 . 如果我们使用 fill url() 把 图片URL 填充到当前元素内 , 那么这里 string 就是具体的 图片URL .

  6. magick/delegate.c : 345

    再来看 ExternalDelegateCommand() 函数 .

    )

    经过一系列操作后 , 该函数将会判断要执行的命令中是否包含 "&;<>|" 这些特殊字符 , 若有就带入 system() 函数中执行 .

好了 , 整个流程走完了 , 纵观全局 , 程序并没有对输入数据进行有效的过滤( 其实有个白名单的 , 不过放行的字符太多了 , 等于没用 )

攻击者的恶意代码将通过上述流程被拼接到系统命令中 , 从而引发任意代码执行漏洞 .


总结

整个流程的分析过程还是比较困难的 , 我花了很长时间来整理思路 . 如果您发现哪里出现错误或者不准确的地方 , 非常感谢您的留言 .

在查阅相关文档时 , 我发现很多前辈都使用 GDB 工具来分析代码 , 看起来非常简便 . 我准备在下一周抽空学习如何使用 GDB 工具 .


Post Scripts :

 第一周实习和师傅去了一个现场 , 学到了很多东西 , 实际生产环境和实验环境的差别还是非常非常大的 , 很多问题在实验环境中并不会被暴露出来 . 

 下周要写一份报告 , 还会涉及到一个攻防平台的搭建 , 感觉很有意思啊 ! 又能看到很多新东西了哈哈~
最后修改日期:2020年3月9日

作者

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。