2016 年 5 月 3 号,一个被广泛使用的图片处理库 ImageMagick 被爆出存在一处远程命令执行漏洞(CVE-2016–3714),当其处理的上传图片带有攻击代码时,攻击代码中的远程命令将会被执行,进而可能控制服务器。

这个漏洞被命名为 ImageTragick,甚至还有了一个属于这个漏洞自己的网站( https://imagetragick.com/ )。

同样,新浪云上也是安装了这个库的,在 PHP 运行环境中,也是有 PHP-Imagick 这个扩展的,这就意味着新浪云的 PHP 环境也是有相同的远程命令执行漏洞的。

因此新浪云在第一时间修复了该漏洞,并执行了更严格的策略,和官方提供的解决方案相比,更加严格的杜绝了类似现象的发生。下面就来看看这个漏洞的产生,以及新浪云是如何修复这个漏洞的。

漏洞原因

这个漏洞产生的原因,在 ImageTragick 的网站也是有说明,主要是由于一个 ImageMagick 有一个功能叫做 delegate(委托),作用是调用外部的 lib 来处理文件。而调用外部 lib 的过程是使用系统的 system 命令来执行的, 相关代码

所有的委托都是在一个配置文件中指定的,默认的配置文件在/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 ,也是能解决这个问题的。

新浪云的做法

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

新浪云的 PHP 运行环境,是放在一个“沙箱”当中的,因此一般情况下,从 php 层面是无法突破这个沙箱的限制,去读取系统,或者是其他应用的文件的,但是也有例外,比如这次的漏洞,ImageMagick 成功的突破了新浪云沙箱的限制,不仅能够读取系统的文件,还可以运行外部命令,这对于新浪云的安全性来说,是无法容忍的。

先说一下沙箱的原理,沙箱的原理,就是利用 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.

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

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

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