0x00 前言
最早是2018.7.21 i春秋巅峰极客赛接触到这个漏洞,这也是我第一次认真的搭建环境,分析漏洞。跟着大佬的博客,算是比较清楚的弄懂了这个漏洞。
0x01 漏洞环境
- OneThink 1.0
- /Temp/Runtime目录可读可写
0x02 漏洞分析
因为ThinkPHP对缓存设计逻辑的漏洞,以及缓存文件名可猜测的原因,导致了这个漏洞
先看一下ThinkPHP的缓存文件的配置:
ThinkPHP中一些系统常量的定义都在ThinkPHP/ThinkPHP.php中定义,缓存路径也在这里:
发现TEMP_PATH 默认值为RunTime/Temp
目录。
但是 缓存数据文件存储位置为ThinkPHP/Conf/convention.php
下面一步步分析 OneThink 在登陆的时候缓存文件是如何存储的:
登陆时,请求的网页是http://127.0.0.1/onethink/index.php?s=/Home/User/login.html
搜索login,在onethink\Application\Admin\Controller\PublicController.class.php
下找到对应方法
其中的D函数是TP中获取model的方法,这里相当于获得了一个MemberModel类的实例,
登陆成功后,调用Member->login方法,跳转到定义:onethink\Application\Admin\Model\MemberModel.class.php
其中$uid
是从数据库中查询出来的用户名对应的用户id。
登陆时,调用autoLogin()函数,继续跟进,找到get_username()函数
终于找到了对于用户名的缓存操作
在第一次登陆的时候是没有缓存的,if条件应该直接进入else部分,又因为$list是从$uid所在行的第二行拿到的,所以应该是用户名,所以在下面调用S方法缓存数据的时候传入的$list我们是可控的
再进入S函数,查看一下这个函数的具体设计
我们传入的$list就是S函数中的value参数,分析if条件的话可以知道程序直接进入第二个elseif,初始化$cache,关键在最后的set函数,set函数中的$value仍然是我们可控的,进入set函数中,set函数在ThinkPHP/Library/Think/Cache/Driver/File.class.php
发现$filename被file_put_contents直接调用,如果不开启数据压缩的话,$data则是我们控制的$value序列化以后存入的值
下面再知道文件名,就完全可控了,看看文件名是怎么定义的。
'onethink_'.md5(md5($name));
最后就是onethink_md5(md5(name)).php
了
0x03 漏洞利用
现在我们构造payload:
用户名注册为%0aphpinfo();#这样在存储缓存文件的时候就可以写入webshell,#注释了序列化的剩余部分,前面的%0a的作用则是为了不让$data中<?php\n//".sprintf('%012d',$expire).$check.$data."\n?>
的//
注释攻击代码
在OneThink的注册页面中的注册用户名为:%0aphpinfo();#
,但是考虑到有长度限制,可以利用burp抓包改包。
之后用这个用户名登陆,之后访问Temp/Runtime/namemd5.php
,即可发现phpinfo得以执行.但是注意一点,就是其$name
变量不是usename,而是前面S()函数的参数即sys_active_user_list
访问http://127.0.0.1/onethink/Runtime/Temp/onethink_d403acece4ebce56a3a4237340fbbe70.php
0x04 漏洞修复
临时解决方案,通过强化缓存的文件名规则,让动态缓存文件难以被反向找到。 修复补丁下载地http://pan.baidu.com/s/1gdAFMSz
补丁修改了最后filename()函数的一句话
private function filename($name) { |
这样将一个可控的或者说可知的文件名,改成了未知不可控的文件名。
0x05 总结
花了一下午的时间,找漏洞,还是在有博客的帮助下。大牛们挖洞真的也不容易啊。。。
第一次大型CMS的代码审计感觉非常烦躁,好多同名函数,一会就弄混了,希望之后能渐渐游刃有余起来。