CVE-2016-3714 - ImageMagick漏洞分析和解决

2016年5月3号,一个被广泛使用的图片处理库ImageMagick被爆出存在一处远程命令执行漏洞(CVE-2016–3714),当其处理的上传图片带有攻击代码时,攻击代码中的远程命令将会被执行,进而可能控制服务器。
这个漏洞被命名为ImageTragick,甚至还有了一个属于这个漏洞自己的网站(https://imagetragick.com/)
同样,SAE上也是安装了这个库的,在PHP运行环境中,也是有PHP-Imagick这个扩展的,这就意味着SAE的PHP环境也是有相同的远程命令执行漏洞的。
因此SAE在第一时间修复了该漏洞,并执行了更严格的策略,和官方提供的解决方案相比,更加严格的杜绝了类似现象的发生。下面就来看看这个漏洞的产生,以及SAE是如何修复这个漏洞的。

漏洞原因

这个漏洞产生的原因,在ImageTragick的网站也是有说明,主要是由于一个ImageMagick有一个功能叫做delegate(委托),作用是调用外部的lib来处理文件。而调用外部lib的过程是使用系统的system命令来执行的( https://github.com/ImageMagick/ImageMagick/blob/e93e339c0a44cec16c08d78241f7aa3754485004/MagickCore/delegate.c#L347
所有的委托都是在一个配置文件中指定的,默认的配置文件在/etc/ImageMagick/delegates.xml(不同的系统和版本位置有一定区别),其中:

<delegatemap>
    ...
    <delegate decode="https" command="&quot;curl&quot; -s -k -o &quot;%o&quot; &quot;https:%M&quot;"/>
    ...
</delegatemap>

在文件的注释里可以看到它定义了很多占位符,比如%i是输入的文件名,%l是图片exif label信息。而在后面command的位置,%i和%l等占位符被拼接在命令行中。这个漏洞也因此而来,被拼接后的命令行传入了系统的system函数,因此只需使用反引号(`)或闭合双引号,就可以执行任意命令。
看官方给的poc:

push graphic-context
viewbox 0 0 640 480
fill 'url(https://"|id; ")'
pop graphic-context

就会在调用curl的同时,调用了id命令。于是漏洞就产生了。

漏洞修复

ImageTragick网站上给出了两种修复或者规避这个漏洞的方式:

  1. 处理图片前,先检查图片的 “magic bytes”, 如果图片头不是你想要的格式,那么就不调用ImageMagick处理图片。
  2. 使用一个policy文件来禁止一些有问题的操作,这个文件默认位置在 /etc/ImageMagick/policy.xml(不同的系统和版本位置有一定区别),可以按如下配置:
 <policymap>
  <policy domain="coder" rights="none" pattern="EPHEMERAL" />
  <policy domain="coder" rights="none" pattern="URL" />
  <policy domain="coder" rights="none" pattern="HTTPS" />
  <policy domain="coder" rights="none" pattern="MVG" />
  <policy domain="coder" rights="none" pattern="MSL" />
  <policy domain="coder" rights="none" pattern="TEXT" />
  <policy domain="coder" rights="none" pattern="SHOW" />
  <policy domain="coder" rights="none" pattern="WIN" />
  <policy domain="coder" rights="none" pattern="PLT" />
</policymap>

当然,直接升级到最新版的ImageMagick,也是能解决这个问题的。

SAE的做法

和上面给的做法不同的是,SAE并没有升级ImageMagick版本,也没有配置policy文件,而是采取了一个稍微‘暴力’点的手段来解决这个漏洞。

SAE的PHP运行环境,是放在一个“沙箱”当中的,因此一般情况下,从php层面是无法突破这个沙箱的限制,去读取系统,或者是其他应用的文件的,但是也有例外,比如这次的漏洞,ImageMagick成功的突破了SAE沙箱的限制,不仅能够读取系统的文件,还可以运行外部命令,这对于SAE的安全性来说,是无法容忍的。
先说一下沙箱的原理,沙箱的原理,就是利用LD_PRELOAD这个环境变量,将很多的函数hook起来,替换为我们自己实现的一个版本,在这个版本里,可以进行一些权限的检查,判断是否通过,如果通过,则正常执行,否则就直接返回错误。从而保证整个系统的安全性。最简单的,我们可以把open这个函数hook起来,在里面判断每个应用是不是有权限去读写对应的文件,并根据判断结果放行或者拒绝。
上面说道ImageMagick使用的是system函数来执行外部命令,那么很简单,只需要将system函数hook住,判断是不是ImageMagick调用的,如果是,则直接拒绝,从根本上解决执行外部命令的问题。
但是,仅仅这样是不够的,为什么呢?来看一下PHP的源代码:

#define DL_LOAD(libname)  dlopen(libname, RTLD_LAZY | RTLD_GLOBAL | RTLD_DEEPBIND)

PHP在加载一个扩展时,会添加上RTLD_DEEPBIND这个参数,man中对这个参数的解释如下:

RTLD_DEEPBIND (since glibc 2.3.4) Place the lookup scope of the symbols in this library ahead of the global scope.
This means that a self-contained library will use its own symbols in preference to global symbols with the same name
contained in libraries that have already been loaded. This flag is not specified in POSIX.1-2001.

意味着如果使用了这个参数,则程序在寻找符号时更倾向于使用自身的而不是全局空间中的,简单来说,就是LD_PRELOAD这种替换符号的形式对于使用RTLD_DEEPBIND加载的动态库文件是无效的。
因为这样,所以实际上ImageMagick调用的system并不是SAE沙箱中的system,而是系统中的system,那么如何解决?其实也很简单,把dlopen函数也hook住嘛,然后在这个函数里把RTLD_DEEPBIND参数去除掉,就可以了:

if (strcmp(so_name, "imagick.so") == 0) {                                               
    flag = flag & ~RTLD_DEEPBIND;
}

使用这种做法,即使再出现类似的漏洞,对于SAE来说,也是安全的,因为有了沙箱的保护,只要不突破沙箱,就无法实现外部命令调用,或者任意读取文件等行为了。