MetInfo SQL注入漏洞详解 前言 之前CNVD爆出了一个最新版本的MetInfoSQL注入漏洞,根据payload正向分析了一遍漏洞原理,写一篇详细一点的
参数回溯 payload: admin/index.php?m=web&n=message&c=message&a=domessage&action=add&lang=cn¶137=1¶186=1¶138=1¶139=1¶140=1&id=42 and 1=1
需要在管理员页面触发,为一个bool类型的SQL盲注
问题出现在/app/message/web/message.class.php中的add函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public function add ($info) { global $_M; if (!$_M[form][id]){ $message=DB::get_one("select * from {$_M[table][column]} where module= 7 and lang ='{$_M[form][lang]}'" ); $_M[form][id]=$message[id]; } $met_fd_ok=DB::get_one("select * from {$_M[table][config]} where lang ='{$_M[form][lang]}' and name= 'met_fd_ok' and columnid = {$_M[form][id]}" ); $_M[config][met_fd_ok]= $met_fd_ok[value]; if (!$_M[config][met_fd_ok])okinfo('javascript:history.back();' ,"{$_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' ]); } }
可以看到在第七行处的$_M[form][id]
并没有被单引号包裹,出现了SQL注入漏洞,我们再回溯变量看看$_M[form][id]
是从哪里来的,可以看到第二行定义了全局变量$_M
1 2 3 4 5 6 public function __construct () { global $_M; parent ::__construct(); $this ->upfile = load::sys_class('upfile' , 'new' ); }
并且在message的构造方法中也定义了$_M
,且未初始化,判断$_M在message的构造方法中被赋值,在构造方法中还调用了父类的__construct
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public function __construct () { parent ::__construct(); global $_M; if (strpos($_SERVER['HTTP_REFERER' ], 'pageset=1' )!==false && strpos($_SERVER['HTTP_REFERER' ], 'lang=' )!==false && strpos($_SERVER['HTTP_REFERER' ], $_M['url' ]['site' ])!==false ){ preg_match('/lang=(\w+)/' , $_SERVER['HTTP_REFERER' ], $prev_lang); if ($prev_lang && $prev_lang[1 ] !=$_M['lang' ]){ $new_url="{$_M['url']['site_admin']}index.php?lang={$_M['lang']}&n=ui_set&pageset=1" ; echo "<script> parent.document.getElementsByClassName('page-iframe')[0].setAttribute('data-dynamic','{$_M['url']['site']}index.php?lang={$_M['lang']}'); parent.window.location.href='{$new_url}'; </script>" ; die ; } }
在web类的构造方法中并没有发现对$_M
的赋值操作,而且web类又调用了父类的构造方法,继续回溯web类父类的构造方法
1 2 3 4 5 6 7 8 9 10 11 12 public function __construct () { global $_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(); }
在这里的load_form函数中对传入的GPC参数进行了过滤和赋值,并且将处理过的GPC转存到$_M
中
至此,我们找到了$_M
赋值的地方,此时类的继承关系为:common->web->message
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 protected function load_form () { global $_M; $_M['form' ] =array (); isset ($_REQUEST['GLOBALS' ]) && exit ('Access Error' ); foreach ($_COOKIE as $_key => $_value) { $_key{0 } != '_' && $_M['form' ][$_key] = daddslashes($_value); } foreach ($_POST as $_key => $_value) { $_key{0 } != '_' && $_M['form' ][$_key] = daddslashes($_value); } foreach ($_GET as $_key => $_value) { $_key{0 } != '_' && $_M['form' ][$_key] = daddslashes($_value); } if (is_numeric($_M['form' ]['lang' ])){ $_M['form' ]['page' ] = $_M['form' ]['lang' ]; $_M['form' ]['lang' ] = '' ; } if ($_M['form' ]['metid' ] == 'list' ){ $_M['form' ]['list' ] = 1 ; $_M['form' ]['metid' ] = $_M['form' ]['page' ]; $_M['form' ]['page' ] = 1 ; } if (!preg_match('/^[0-9A-Za-z]+$/' , $_M['form' ]['lang' ]) && $_M['form' ]['lang' ]){ echo "No data in the database,please reinstall." ; die (); } }
在load_form
函数中我们发现了赋值给$_M
的过程和过滤函数daddslashes,跟入查看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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; }
在没有定义IN_ADMIN
的时候在addslashes之前还调用了sqlinsert函数,这也就是为什么作者说在第一次尝试注入的时候发现SQL关键词都被清除了,所以我们需要找到一个地方定义了IN_ADMIN
来绕过sqlinsert函数
正向分析 即然我们需要IN_ADMIN
值不为false,根据字面意思判断,就可以知道需要在管理页面找,而admin/index.php正好定义了IN_ADMIN
的值
而且在此文件中我们可以更改GET参数来调用各种模型,类名,操作名(操作名必须以do开头)
作者找到了domessage函数可以触发add方法,并且将GET的参数全部传递过去,成功注入,鉴于里面的逻辑太复杂,直接使用xdebug跟踪函数调用
输入payload之后直接跟到调用add函数的位置,可以在PHPstorm中看到所有的函数调用关系
在_load_class
中实例化了message类
在实例化的时候调用message类的构造方法,调用了message父类web的父类common的构造方法,并且使用了load_form
过滤了传入的GPC参数,这个地方在跟踪的时候发现居然跳到了web类中实现的load_form
函数,刚开始有点迷,后来想清楚了
在学面向对象的时候继承是一个很重要的概念,而这个时候就是面向对象的一个特性,此时调用方法是:$this->load_form();//表单过滤
,可以看到当前的\$this指向的是message类,这个时候去message类找load_form()
函数,发现没有这个函数,根据继承的特性,程序向message类的父类去找load_form()
函数,发现它的父类实现了load_form
,所以直接调用message父类的load_form
,而这个函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 protected function load_form () { global $_M; parent ::load_form(); foreach ($_M['form' ] as $key => $val) { $_M['form' ][$key] = sqlinsert($val); } if ($_M['form' ]['id' ]!='' && !is_numeric($_M['form' ]['id' ])) { $_M['form' ]['id' ] = '' ; } if ($_M['form' ]['class1' ]!='' && !is_numeric($_M['form' ]['class1' ])) { $_M['form' ]['class1' ] = '' ; } if ($_M['form' ]['class2' ]!='' && !is_numeric($_M['form' ]['class2' ])) { $_M['form' ]['class2' ] = '' ; } if ($_M['form' ]['class3' ]!='' && !is_numeric($_M['form' ]['class3' ])) { $_M['form' ]['class3' ] = '' ; } }
可以看到这里对每一个table传入的参数进行一次sqlinsert过滤,按理来说我们的payload已经被过滤了,通过监视$_M
,确实我们的payload已经为空了
但是为什么我们的payload最后还能执行呢,再跟踪函数,我发现在调用完web类的load_form
函数之后,调用了$this->upfile = load::sys_class('upfile', 'new');
,在这个函数中又重新调用了common的构造方法,结果把payload又赋值给了$_M
这个时候我们的payload又回到了$_M
的id参数中
这个时候,可以确定payload可以拼接入SQL语句中,但是接下来还需要将所有逻辑走通,接下来解答几个问题
为什么需要paraXXX参数 在执行domessage函数的时候,可以看到在执行add函数之前执行了一个check_field函数,我们进入查看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 function check_field () { global $_M; $messagecfg= load::mod_class('message/message_handle' ,'new' )->get_message_config(load::mod_class('message/message_database' ,'new' )->get_message_columnid()); $met_message_fd_class=$_M[form]['para' .$messagecfg[met_message_fd_class][value]]; $met_message_fd_content=$_M[form]['para' .$messagecfg[met_message_fd_content][value]]; $met_message_fd_email=$_M[form]['para' .$messagecfg[met_message_fd_email][value]]; $met_message_fd_sms=$_M[form]['para' .$messagecfg[met_message_fd_sms][value]]; $met_fd_back=$messagecfg[met_fd_back][value]; $paralist=load::mod_class('parameter/parameter_database' ,'new' )->get_parameter('7' ); foreach ($paralist as $key => $val) { $para[$val[id]]=$val; } $paraarr = array (); foreach (array_keys($_M['form' ]) as $vale) { if (strstr($vale, 'para' )) { if (strstr($vale, '_' )) { $arr = explode('_' ,$vale); $paraarr[] = str_replace('para' ,'' ,$arr[0 ]); }else { $paraarr[] = str_replace('para' ,'' ,$vale); } } } foreach (array_keys($para) as $val) { if ($para[$val]['wr_ok' ]==1 && !in_array($val,$paraarr)){ $info="【{$para[$val]['name']}】" .$_M[word][noempty]; okinfo('javascript:history.back();' ,$info); } } }
下面这些变量就是我们传入参数中paraXXX的内容:
1 2 3 4 5 $met_message_fd_class=$_M[form]['para' .$messagecfg[met_message_fd_class][value]]; $met_message_fd_content=$_M[form]['para' .$messagecfg[met_message_fd_content][value]]; $met_message_fd_email=$_M[form]['para' .$messagecfg[met_message_fd_email][value]]; $met_message_fd_sms=$_M[form]['para' .$messagecfg[met_message_fd_sms][value]]; $met_fd_back=$messagecfg[met_fd_back][value];
这一行if($para[$val]['wr_ok']==1 && !in_array($val,$paraarr))
判断了我们传入的参数中有无paraXXX
我们需要在传入的参数中有137,186,138,139,140,否则会进入if条件中的逻辑
1 2 $info="【{$para[$val]['name']}】" .$_M[word][noempty]; okinfo('javascript:history.back();' ,$info);
为什么id必须为42 1 2 3 $met_fd_ok=DB::get_one("select * from {$_M[table][config]} where lang ='{$_M[form][lang]}' and name= 'met_fd_ok' and columnid = {$_M[form][id]}" ); $_M[config][met_fd_ok]= $met_fd_ok[value]; if (!$_M[config][met_fd_ok])okinfo('javascript:history.back();' ,"{$_M[word][Feedback5]}" );
可以看到,从数据库中取出$_met_fd_ok
的值,如果$_met_fd_ok[value]
不为空的话,继续往下执行,我们进入数据库看看这条语句到底取出来的是哪些值
可以看到我们想让它的返回不为空,id的值必须为42或者44,所以我们的注入的时候必须保证id的值为44或者42
总结 metinfo的SQL注入漏洞是作者在几次给metinfo提交后met官方不予理睬才曝光的,希望厂商能多多重视安全吧