ThinkPHP5_反序列化分析
最后更新时间:
文章总字数:
预计阅读时间:
ThinkPHP5.1 反序列化总结
影响版本:5.1.37-5.1.41
先在index里面加一个反序列化的入口:
由于这个反序列化链实在是太长了,而且需要逆着分析,向上找调用,没法进行调试(调试是顺着分析),所以分成两大部分来分析,第一部分跳跃的类有点多,涉及到了php的多继承和抽象类,第二部分主要集中在Request类中不同方法的互相调用,而连接两个部分的关键在于访问不存在的方法时自动调用的__call
方法。
第一部分:
首先是第一部分,反序列化的入口点(根据常识,往往是__destruct
魔术方法,因为类总会被销毁,而这个魔术方法就会被自动调用,从而开启pop链),在Windows类的__destruct
方法:
跟进close看看,就是一个关闭文件的操作,无法利用,关键在removeFiles,跟进:
可以看到这里的filename我们可以控制,存在任意文件删除漏洞(热知识:只要是类的成员变量,反序列化的时候就可以控制),任意文件删除不是重点,我们的重点是要RCE。看到这个file_exists函数,这个函数通过路径来判断文件是否存在,也就是说传入的参数需要是一个字符串,如果我们控制filename是一个类,就会自动调用该类的__toString
方法,于是我们需要寻找一个有危险方法的__toString
。看到Conversion.php的__toString
方法,注意这里是第一次跳转,从Windows.php到Conversion.php
跟进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方法,这是第二次跳转)
可以看到这里name默认为空,返回了relation,而relation我们还没有进行获取,所以返回的就是空,返回空满足后,回到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目前还不可控,虽然第二个if里面调用了getData,但是:
于是我们要直接控制data,且使得name就是空,就可以跳过这个if,保留我们的data值,往上找找谁调用了input,是否直接传入了可控的参数,找到了param方法:
可以看到这里终于是可以控制的this->param,但是需要注意到第一个if里面还对this->param进行了加工,$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));
,虽然我们可以完全不用管这个,直接控制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,皆大欢喜,开始写脚本构造序列化内容:
1 |
|
可以看到成功进行了RCE。
话说这条链真的是有点太长了,复现都挺麻烦了,真想知道是怎么挖出来的
更多这条利用链的变体可以参考ctfshow上相关的题。
参考链接