一拖再拖的我- -总是不想去接触框架,因为感觉跳来跳去的好麻烦- -不过总归还是要走上这条路的,那就亮剑吧。
这一次,是唯一一次没有看别人文章,自己分析CVE的一次,而且我的调试插件还出了问题,只能跟踪半分钟= =只能靠var_dump大法了。
最真实的手动跟踪,可能会误人子弟,但是真的很真实了= =假如从头到尾,每个函数都跟踪进入看一看的话,说不定不需要我这么麻烦。
欢迎大家关注小菜鸡的公众号哇,“不爱吃韭菜的韭菜”,虽然都是从我的csdn转载过去= =但是我感觉微信的排版好看一点,而且在手机上看方便一些。(俺的公众号绝对永不开通流量主,不赚钱只分享)
先找到payload,看能不能打成功再说。
代码执行的payload: post:/public/index.php?s=index/index data: _method=__construct&method=get&filter[]=call_user_func&get[]=phpinfoRCE payload有挺多的:
_method=filter&c=system&f=whoami _method=__construct&method=GET&filter[]=system&get[]=whoami _method=__construct&method=POST&filter[]=system&s=whoami但是发现每条都是这个鬼样子,wtf?
这里是因为disable_function里面禁用了system函数= =我们还要自己改php.ini文件= =
这里说明一下,假如创建站点的时候使用了生产环境: php的版本环境后面会默认跟上一个p,那么和普通的有啥不同呢?看了下disable_function,发现普通环境的是空的,生产环境是禁用了一大堆:
把平常要用的几个函数删掉,然后重启apache,可以了:
先从最简单的一条payload开始:
_method=filter&c=system&f=whoami先看下漏洞触发点:
thinkphp\library\think\Request.php 1065行 是call_user_func导致的。
然后根据函数名,查找全局使用:
只有2个= =cookie还是显示灰色的,说明该文件里没有被使用,那就是input函数了:
thinkphp\library\think\Request.php 1011行
然后再根据input函数查找使用,发现有点多= =往上再套娃的话会更多:
那就从正向再开始,到时候对接一下:
这里建议跟着调试走,自己眼睛看得看死个人,安装一个调试插件xdebug,我的有点问题- -调试时间太长会断掉,有时间弄一下。
(补)
后来我找到方法,解决超时500问题了,打开phpstudy这个部分:
点进去要调试的站点,加上这一句:
我设置成了3600秒。
跟踪结果:(有好几条路,反正都会进入input()函数)
public\index.php=》包含start.php文件 thinkphp\start.php=》执行run()函数 thinkphp\library\think\App.php run() =》 124行跳转 thinkphp\library\think\App.php exec() =》296行跳转 thinkphp\library\think\App.php module() =》 411行跳转 thinkphp\library\think\App.php invokeMethod() =》 194行跳转 thinkphp\library\think\App.php bindParams() =》 234行跳转 thinkphp\library\think\Request.php param() =》 624行跳转 thinkphp\library\think\Request.php post() =》 705行跳转 thinkphp\library\think\Request.php input() 1007行的array_walk_recursive()函数 =》 thinkphp\library\think\Request.php filterValue()=》 1065行:执行call_user_func()数据流还算清晰- -大多数都是比较正常普通的流程,也没有太多专门去绕过一些限制,所以中间的具体过程就不细说了,最后在call_user_func触发。 这么多跟踪,而且在里面穿插了很多不必要的函数和跳转,我肯定懒得全部看完= =注意payload里面的特殊点,_method=filter,我们删掉它,再跟踪一次,发现了区别: 在thinkphp\library\think\Request.php input()函数,1007行部分,我们输出var_dump($data),发现两次的值是不一样的:
(1) (2)
那说明问题出在了array_walk_recursive()函数上面:
(其实并不是,有没有_method=filter都会对$data进行unset)
没听过array_walk_recursive()函数,查一下:
懂了,$data是遍历的数组:
[$this,‘filterValue’]代表执行的函数是当前类的filterValue函数
$filter是参数;
那现在的主要任务就是把这一大段函数看懂了:
先是调用了array_pop()函数:
输出大法启动:
也就是把之前数组中,最后的那个null去掉了,没啥大事,继续往下:
然后进入了foreach语句,继续输出大法:
这里这么混乱,是因为调用了好几次这个函数,但是可以看到,只有第三次是可以成功执行的。
我再来一次输出,就好理解了:
这就是3次执行该函数的情况,第一行的system代表的是值,作为call_user_func的第二个参数,
然后遍历下面的数组,数组内容作为call_user_func的第一个参数。
很容易知道,在第二次进入该函数的时候,会成功执行payload。
在这里出现的is_callable就是检查这个函数能不能被调用而已:
假如我们删掉 _method=filter呢?
what?数组不见了?
回到array_walk_recursive()函数旁边,再输出一下:
比较一下:
(1)
(2)
那么找到了,就是getFilter()搞得鬼:
在thinkphp\library\think\Request.php 1034行:
输出一下就能发现,是在1040行对$filter做了手脚,我们搜索一下哪些地方对$this->filter进行了操作,找到了1025行的filter()函数:
两次输出对比一下:
(1)
(2)
结果就是,如果不加上_method=filter的话,就只会调用第一次filter函数,但是可以看到,第二次才会赋值我们的payload。
查找使用,只有一处使用了,我们又回到了
thinkphp\library\think\App.php run()函数 87行
(正常正常,我们的利用过程已经走完了,现在是回溯找原因的时候)
但是这里的run()函数只执行了一次,这个第二次到底从何而来?
我们猜测一下,_method=filter,而这个filter正是我们现在在研究的函数名,可能就是动态调用函数了。
我们之前说过,传_method=filter会执行2次filter函数,不传只有一次。
当我们在唯一一次调用filter()函数的后面,进行一次输出时,发现yyy输出在两侧,说明第二次调用filter在输出的后面:
那么我们在exec()前面输出一次:
证明了什么?证明了第二次执行filter函数,是在87行filter和122行exec之间!我们就可以少跟踪很多函数。
首先这两个部分一看就感觉不是= =
这一块大大的可能:
跟进routeCheck()函数,来到当前文件的534行:
因为我已经找到了,所以中间多余的就不看了,直接来到563行:
进行了路由检测,跟进去:
thinkphp\library\think\Route.php 836行:
在checkRouteAlias前面插入一句输出,发现并没有调用到这里,那就往下看:
跟进method()方法里,来到thinkphp\library\think\Request.php 501行:
重点就是这里了,首先通过输出大法,判断了一下,无论传不传_method=filter都会进入到这个函数2次,所以直接看内容:
进入函数的时候没有传参,而$method默认是false,所以进入elseif:
这里先是进入了get函数,跟进一下,thinkphp\library\think\Config.php 101行:
输出一下,发现我们无论传不传_method=filter,都是同样的结果,那就没必要跟进去了:
然后接下来就是取 $_POST[_method],并转换为大写,然后动态执行:
因为前面有个$this->,所以也不能直接eval,需要利用这个类下的其他函数,所以filter函数的第二个执行点我们已经找到了,完工。
整理一下:
全军出击:
public\index.php=》包含start.php文件 thinkphp\start.php=》执行run()函数 thinkphp\library\think\App.php run()然后兵分三路:
上路:(执行filter()函数,为了给$this->filter赋值)
thinkphp\library\think\App.php run() =》 107行跳转 thinkphp\library\think\App.php routeCheck() =》 564行跳转 thinkphp\library\think\Route.php check() =》 848行跳转 thinkphp\library\think\Request.php method() 509行动态执行filter函数。下路:(执行call_user_func函数)
thinkphp\library\think\App.php run() =》 124行跳转 thinkphp\library\think\App.php exec() =》296行跳转 thinkphp\library\think\App.php module() =》 411行跳转 thinkphp\library\think\App.php invokeMethod() =》 194行跳转 thinkphp\library\think\App.php bindParams() =》 234行跳转 thinkphp\library\think\Request.php param() =》 624行跳转 thinkphp\library\think\Request.php post() =》 705行跳转 thinkphp\library\think\Request.php input() 1007行的array_walk_recursive()函数 =》 thinkphp\library\think\Request.php filterValue()=》 1065行:执行call_user_func()至此第一个最简单的payload已经看完了。
嗯嗯嗯?中路呢?我,21段中路法王。hhhc。
再看看另外几个payload:(这里测试的时候发现,网上的那些payload ,其实method=POST、method=GET是可以去掉的,而且filter也没必要是数组)
_method=__construct&method=GET&filter[]=call_user_func&get[]=phpinfo _method=__construct&method=GET&filter[]=system&get[]=whoami _method=__construct&method=POST&filter[]=system&s=whoami几乎是一样的啊,那就拿第二条开刀,
这个payload很有意思,会执行2次:
这里的_method换成了__construct,跟过去看一下:
thinkphp\library\think\Request.php 130行
先输出大法看看$options的内容:
发现又是第二次才会有我们的payload,然后继续看,下面就是遍历数组,然后调用property_exists()函数:
就是检查当前__construct所在的Request类,有没有_method、method、filter、get属性,
输出一下检查结果,也可以往上翻,在文件开头的初始化定义,对比一下:
可以看到除了_method,其他三个都在,然后给类的属性赋值。
然后后面的一部分,没什么好说的,不是很重要。
再看看我们的payload:(我已经把多余的去掉了)
_method=__construct&filter=system&get[]=whoami我们回到call_user_func附近,输出大法看看:
这次的遍历,成功了两次,所以执行了两次。
换一个payload:
_method=__construct&filter=system&s=whoamiOVER。。。。。
没有调试插件,几乎是全手动跟踪,看参数值也是全靠var_dump,真的会误人子弟的啊,修插件的话,下次一定。