0x00 前言php
今天在Seebug的公众号看到了Typecho的一个前台getshell分析的文章,而后本身也想来学习一下。保持对行内的关注,了解最新的漏洞很重要。git
0x01 什么是反序列化漏洞github
如题所说,这是一个反序列化致使的代码执行。看过我以前文章的人应该不会陌生。PHP在反序列化一个字符串时,至关于激活了以前休眠的一个对象。当在激活的时候,会调用几个魔法方法来进行初始化以及相关操做。而若是魔法方法中存在危险操做,好比数据库操做,文件读写操做,系统命令调用等,那么就有可能会形成相应的漏洞。用户经过可控变量调整系统运行流程。进入到危险函数,从而致使漏洞。sql
0x02 分析shell
官方在前两天已经作了修复。如今已经不存在这个漏洞了。咱们找到之前的版本进行安装。连接以下:
数据库
https://github.com/typecho/typecho/releases/tag/v1.1-15.5.12-beta
在根目录的install.php中咱们发现:编程
咱们定位到了漏洞现场。而后问题来了,反序列化是将一个休眠对象(字符串)激活为一个对象。那么咱们接下来找到在激活的过程当中,有哪些方法能够调用,它们才是利用的关键。咱们来搜索这三个方法:数组
__toString() __destruct() __Wakeup()
我没有找到wakeup方法,而后destruct方法也不能使用。而后搜索__toString()的时候发现有三处,分别以下:app
var\Typecho\Config.php public function __toString() { return serialize($this->_currentConfig); } var\Typecho\Db\Query.php public function __toString() { switch ($this->_sqlPreBuild['action']) { case Typecho_Db::SELECT: return $this->_adapter->parseSelect($this->_sqlPreBuild); case Typecho_Db::INSERT: return 'INSERT INTO ' . $this->_sqlPreBuild['table'] . '(' . implode(' , ', array_keys($this->_sqlPreBuild['rows'])) . ')' . ' VALUES ' . '(' . implode(' , ', array_values($this->_sqlPreBuild['rows'])) . ')' . $this->_sqlPreBuild['limit']; case Typecho_Db::DELETE: return 'DELETE FROM ' . $this->_sqlPreBuild['table'] . $this->_sqlPreBuild['where']; case Typecho_Db::UPDATE: $columns = array(); if (isset($this->_sqlPreBuild['rows'])) { foreach ($this->_sqlPreBuild['rows'] as $key => $val) { $columns[] = "$key = $val"; } } return 'UPDATE ' . $this->_sqlPreBuild['table'] . ' SET ' . implode(' , ', $columns) . $this->_sqlPreBuild['where']; default: return NULL; } } \var\Typecho\Feed.php public function __toString() { 。。。后边再贴代码 }
可是必定能用吗?函数式编程
咱们能够看到随后的$config进入了Typecho_Db类的构造方法,咱们来跟进:
在第120行的时候,发现传入的参数用来作字符串拼接。这样就会调用__toString()方法了.
而后咱们对以前搜索到的方法跟进,发现前两个是无法用的。而后咱们重点关注最后一个。这个函数主要的目的是拼接xml文件。而后在整个过程当中,并无直接进入危险函数,因此此时陷入僵局。可是就这样就无解了吗?
0x03 __get()方法的逆袭
__get()方法在调用一个对象不存在的成员变量时候将会调用此方法。能够参考我以前的文章。
那么这样的话咱们只要可以找到一个get方法中存在危险操做,这样的话,这个漏洞仍是可能的。咱们来试试看。
全局来搜索__get()方法,而后在\var\Typecho\Request.php
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); }
在方法的最后调用了_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; }
咱们来梳理一下整个逻辑,install.php (反序列化) ==> Db.php (将$config中的参数拿来拼接,调用toString方法) ==> Feed.php (调用一个不存在成员变量,将会调用__get()方法) ==> Request.php (调用get()方法,而后调用_applyFilter(), 而后调用call_user_func()致使了代码执行)
0x03 利用方法
完整的利用过程,咱们能够看
p0的文章 http://p0sec.net/index.php/archives/114/ 或者 Seebug的公众号 http://mp.weixin.qq.com/s?__biz=MzAxNDY2MTQ2OQ==&mid=2650942666&idx=1&sn=5c84d6d69463a0a430e01dfa68c2d3ab&chksm=80796ef8b70ee7ee8ba5d88feb8d794bee19a55b6e17dff45fcee6ba6ee726fb4e2e029d50bd&scene=0#rd
0x04 思考
这个实际上是当时看到122行的时候:
我想这里直接调用了call_user_func()函数,$adapterName我是可控的,咱们不直接能够命令执行吗?还要长长的执行链干什么啊?
而后我查了call_user_func()的使用方法:
若是传入的是数组,那么数组中的第一个值是一个类名,第二个是类中的一个方法。可是怎么样第一个值才能是类呢?这个时候我想到了匿名类。在函数式编程中常常会用到匿名函数,匿名类。因而我去查了一下发如今PHP7中加入了匿名类的定义,因此以下代码是能够的:
call_user_func(array( new class(){ public function isAvaliable() { echo "Hello, World"; } }, 'isAvaliable' ) ); //执行结果 Hello, World
这里不能用的缘由在于被以前的$adapterName = 'Typecho_Db_Adapter_' . $adapterName;给打断掉了。若是你用类和字符串作拼接。很明显是不行的。