在先知社区看到的一篇关于信呼OA后台getshell的文章,感觉还不错就拿来记录一下
信呼OA后台getshell分析
环境搭建
phpstudy+信呼OA版本2.6.3
源码链接:https://github.com/rainrocka/xinhu
使用phpstudy创建网站,以及对应数据库
然后将源码文件导入对应文件夹下
访问网站进行安装
安装完后告诉我们后台账号和密码admin:123456
,出现改密码提示更新一下密码就行
前置知识
了解路由结构
我们先从源码index.php看起,第八行包含config.php配置文件
跟进一下,发现也包含了三个php文件
我们跟进查看,发现rockFun.php文件只是定义了一系列方法,Chajian.php文件则是定义了Chajian类,rockClass.php文件定义了rockClass类。接着如果未被定义,那么默认项目和入口分别为webmain
和index
,后面就是其他一些定义内容。
回到index.php,$_uurl
由$rock->get('rewriteurl');
决定
跟进一下(注意这里是rewrite()
方法)
1 | public function rewrite($m,$a,$s) |
相当于是从外部接收$m,$a,$s
参数进行重写,返回值url
由于这里默认没有接收参数,所以$_uurl
为空,那么执行else语句跟进jm类的gettoken()
方法
1 | public function gettoken($na, $dev='') |
继续跟进rock类的get()
方法
1 | public function get($name,$dev='', $lx=0) |
相当于接收GET请求参数m,d,a
不仅如此,回到index.php我们继续往下看发现还有请求参数ajaxbool
我们看向最后一行代码,跟进一下View.php
如果参数m包含|
,那么以|
分割开赋值给$m
和$_m
然后调用strformat()
方法替换其中的占位符,最后分别赋值给$actfile
和$actfile1
1 | public function strformat($str) |
我们继续往下看
将$m
与ClassAction
拼接后实例化,继续将$a
与Action
拼接,然后检测方法是否存在,如果存在则调用该方法
具体来讲和我们路由中的传参参数有关,也就是
1 | 对$_GET['m']ClassAction类进行实例化 |
再往下则是该CMS自写的模板解析功能
以上就是框架大概的运行原理,下面我们定义一个自己的控制器
定义目录/文件结构如下
rev1veAction.php内容如下
1 |
|
注:这里有个坑点,参考文章的示例没有Class直接就是
rev1veAction
这样。一开始按照文章来结果复现不出来,后面调试一下才注意到这里的类名应该是拼接上ClassAction
,原理前面也分析过就不赘述了
tpl_rev1ve_tester.html内容如下
1 | <h1>test模板</h1> |
我们先登录管理员账号,然后执行以下命令即可实现
1 | ?d=test&m=rev1ve&a=tester |
漏洞分析
漏洞触发点
位于webmain\main\flow\flowAction.php
的inputAction()
方法
我们跟进一下,往下翻代码注意到存在写入php文件
1 | $apaths = ''.P.'/flow/input/mode_'.$modenum.'Action.php'; |
这里的参数$modenum
和$rs['name']
被拼接到写入文件的注释里,我们可以尝试注释掉前后代码实现RCE
看看两个参数是怎么来的,往上翻
这里$rs
是调用m()
跟进一下,并传参为flow__get
的表名
第34行代码出现实例化Model类,那么就继续调用该类的getone()
方法
1 | public function getone($where, $fields='*', $order='') |
反正是从数据库中获取的,并且没有对其进行过滤
利用思路
insert data -> flow_set
数据表- 执行inputAction 方法
- 代码执行
能insert数据的地方有好几个,最初是选用同一文件位于88行flowsetsavebefore()
方法
不过这里可以看到过滤非常严格,而我们写的马需要注释符/**/
以及()
,所以这条路是走不通的。
继续翻代码,找到也有执行SQL语句的webmain\system\beifen\beifenAction.php
的huifdatanewAjax()
首先我们是管理员账号,所以不用去管参数$adminid
是否为1。接着POST接收参数folder
和sid
,获取数据库所有表名,继续检查给定路径下的文件是否存在,如果存在则执行m('beifen')->getbfdata()
方法,跟进一下
1 | public function getbfdata($file, $path='') |
调用file_get_contents()函数
读取json数据,然后json解码
解码后的数据存储在$data
中,然后获取$data[tab]
的数据判断文件表是否存在
1 | if(!in_array($tab, $alltabls)){ |
如果不存在调用arrvalue()
方法,跟进一下
1 | /** |
相当于是获取$data[$tab]['createsql']
的值。如果$createsql
存在,执行query()
方法
其他都可控主要上传的图片文件名都是随机的。想要执行sql语句就必须控制文件名。
比如上传文件返回随机文件名a.png,那么文件内容必须为
1 | ["a.png" => ["createsql" => "select 1"]] |
我们无法提前预知生成的文件名,那这条路也就走不通
通过翻找代码,找到webmain\task\openapi\openkqjAction.php
的apiAction()
方法
这里有判断请求方式,如果为POST请求且不为空那么调用kqjcmd
类的postdata()
方法
跟进一下,这里对POST请求数据进行json解码并赋值给$barr
,然后foreach遍历赋值给$dtype
我们往下看,如果存在headpic键那么调用saveheadpic()
方法
1 | if($dtype=='headpic'){ |
继续跟进saveheadpic()
1 | private function saveheadpic($snid, $uid, $headpic, $face='') |
这里的路径以及文件名可控,然后对$headpic
进行base64解码
漏洞利用
首先insert一条flow_set数据(ps:直接用sql insert太麻烦了直接正常构造一个正常数据,然后sql update)
新增模块列表,填写下基本信息
然后提交抓包查看下对应id,结果为161
然后构造payload
1 |
|
注:这里的可控文件名
kqj1_u1.jpg
在前面分析过,id为我们刚刚创建的模块列表id,$
符号前要加\
防转义
然后根据我们的链子来构造路由,POP链如下
1 | openkqj::apiAction() -> kqjcmd::postdata() -> kqjcmd::saveheadpic() |
构造POST请求包
1 | POST /index.php/post?d=task&m=openkqj|openapi&a=api&sn=1 HTTP/1.1 |
注:headpic为前面构造的payload,base64编码
上传成功后,执行我们前面保存的文件中的sql语句
1 | POST /index.php?a=huifdatanew&d=system&m=beifen&ajaxbool=true |
最后调用webmain\main\flow\flowAction.php
的inputAction()
方法,将木马拼接到文件中触发漏洞
1 | /index.php?a=input&m=flow&d=main&id=1&setid=161 |
直接访问webmain\flow\input
目录下的model_test123Action.php
文件,成功命令执行
(这里test123是模块编号)
也可以按照给定路由结构去访问
1 | ?d=flow&m=mode_test123|input |
以上就是整个信呼OA后台getshell的分析和利用