Thinkphp5.1 反序列化ajax链
环境搭建
composer create-project topthink/think tp5.1.38 5.1.38
composer.json
1 | "require": { |
composer update
该反序列化漏洞属于二次触发漏洞,需要有一个入口,因此我们将控制器中的Index控制器修改一下:tp5.1.38\application\index\controller
1 |
|
public文件夹下的index.php文件加两句话
1 | Container::get('app')->run()->send(); |
POC
先跑脚本得到payload调试走一遍,看完文章的分析后再回来细想
1 |
|

Debug
下断点,传payload
观察POC,最外层套的是think\process\pipes\Windows对象,因此会进入该对象中,来到了Windows.php下的__destruct魔术方法,这是我们pop链的起点
close方法用来关闭文件啥没用,关键在removeFiles里,unlink() 可删除文件,这里的filename我们可以控制,存在任意文件删除漏洞,但这并非重点
我们的重点是要RCE。file_exists函数通过路径来判断文件是否存在,其传入的参数需要是一个字符串,我们将filename属性改为了一个think\model]\Pivot对象(为什么是Pivot后面会说),因此会触发它的toString方法,注意这里是第一次跳转,从Windows.php到Conversion.php
在这里大家可能会有疑问,不是Pivot对象吗,怎么变成了Conversion:
Pivot对象内部没有toString方法,其构造方法调用的是其父类Model类的构造方法。我们再来看Model类,在它的内部复用了被trait修饰的Conversion对象,因此当Pivot被当成字符串输出时,就会调用Conversion类的toString方法
跟进toJson方法
跟进toArray方法
截出toArray关键部分,最关键的一行代码就是$relation->visible($name),这意味这如果我们可以控制relation和name,就可以调用relation这个类的visible方法,且参数由我们控制,或者可以调用relation这个类的__call方法(该类没有visible时就可以自动调用),且参数由我们控制,而之前对tp5RCE中的分析我们也能看出,这个__call很危险,很有可能里面就有call_user_func
那我们就看看该如何控制relation和name,首先看relation如何获取:先是调用了getRelation方法,注意了,这里我们要的是最终走到192行,故这个if必须满足,也就是说getRelation必须返回空,跟进getRelation,看是否可以控制其返回空(这里有个点需要注意一下,这个getRelation并不在Conversion.php中,而是在RelationShip.php中,因为这两都是trait类,这个this指的是进行了多继承的子类(至少同时继承了Conversion和RelationShip),故可以调用在RelationShip.php中的getRelation方法,这是第二次跳转)
1 | public function getRelation($name = null) //$name="a" |
由于$name为a不是null,并且我们没给$this->relation进行赋值,因此直接return一个null回来,回到toArray中,接下来在getAttr方法中真正获取relation(getAttr方法在Attribute类中,这是第三次跳转)
跟进getData方法
可以看到我们可以控制data,只需要再控制name即可返回我们构造的值,往回看,传入getData的name就是传入getAttr的参数name,也就是toArray中的key,而key可以通过我们传入的append控制,至此,我们成功控制了relation,而$relation->visible($name)的参数name也是通过我们传入的append控制。综上所述只需要控制data和append即可(data[key]用来控制relation,append[key]用来控制name)。接下来需要找到一个同时继承了这三个类的子类(Conversion,RelationShip,Attribute),找到了Model.php里的Model类:
但是这里有个问题,Model是一个抽象类,不能直接实例化,我们需要找到具体实现他的一个子类,找到了Pivot类
全局搜索visible,发现没有一个能用的(内不含危险方法),只能找__call,想起了我们的老朋友Request.php
可以看到这里有令人安心的call_user_func_array,而且hook可以控制,但是很遗憾,在此之前进行了array_unshift,简单来说就是把this添加到了参数的第一位,如此一来,我们不再能够通过call_user_func_array来直接调用系统命令(因为系统命令参数里面不能加this),转换思路,将call_user_func_array当成一个能够调用Request类内其他方法的跳板(因为加了this)。
想想之前的tp5RCE,也许我们这次也可以通过覆盖filter来进行RCE,找到filterValue方法:
我们要想办法利用的就是filterValue里面的call_user_func来最终执行命令,这需要我们控制filter和value,而tp5RCE告诉我们,filter和value都可以在input方法里面控制
可以看到这个array_walk_recursive,可以将filter和data传入并调用filterValue,而这个filter是通过getFilter获得的,相当于是获得了this->filter,可以自行控制,对于data的控制我们进入getData
如果我们让name为空就可以跳过这个if,保留data的值。往上找找谁调用了input,是否直接传入了可控的参数,找到了param方法
可以看到这里终于是可以控制的this->param,但是基于动态命令执行的宗旨,我们可以不控制param,转而控制get参数(url传就行),于是data已经被控制,接下来只需要控制name为空即可
param()方法中的name还是不可控。虽然param()方法的默认name是空字符串,但是别忘了我们需要使用__call里面的call_user_func_array来当跳板调用它,第一个参数是this,所以这里name还是不可控,继续往上寻找调用了param的方法看是否可控,找到了isAjax函数:
可以看到这里调用param的第一个参数传入的是$this->config['var_ajax'],我们只需要控制它为空即可成功控制name为空。好了,通过call_user_func_array当跳板调用isAjax,pop链就完成了,只需要再控制一下this的参数,即可实现RCE
参考链接:
两位大佬一个正向分析,一个反向分析,很有收获
ThinkPHP5.x反序列化漏洞全复现 - Boogiepop Doesn’t Laugh (boogipop.com)
- Title: Thinkphp5.1 反序列化ajax链
- Author: Saofe1a
- Created at : 2024-10-10 11:39:55
- Updated at : 2024-10-10 11:39:08
- Link: https://saofeia.github.io/2024/10/10/Thinkphp5.1 反序列化ajax链/
- License: This work is licensed under CC BY-NC-SA 4.0.