Spring Actuator Security, Part 3: Finding Exposed Actuators using Dynamic Testing with ffuf

Spring Actuator Security, Part 3: Finding Exposed Actuators using Dynamic Testing with ffuf

·

9 min read

This is part three of a series on the security implication of Spring Actuators. I recommend having read at least the first part to understand the context.

In the previous article, we discussed how you can leverage static code analysis using semgrep to detect misconfigured Spring Actuators. However, you may not always have access to the source code of the application: maybe you want to know if you have exposed actuators somewhere in your corporate network. Or maybe you are a bug bounty hunter and want to check if there are juicy actuators that you can get your hands on.

In any case, being able to find a URL to access and point at and say "look, there's an exposed actuator right there, do something about it" can be a much better call to action than an abstract "I found this weird thing in your code, can you take a look?". So, let's talk about catching actuators in the act.

We will be using the vulnerable demo application I provide on GitHub as an example for the rest of this blog post. It provides the actuators endpoint on http://localhost:8081/actuator. If you want to follow along at home, now is a good time to set it up.

Finding Actuators Using Your Browser

The most basic approach for finding exposed actuators is to do so by hand. Trivially, you can simply visit a website running Spring and try to access /actuator/ to see what happens. If actuators are enabled and the default path wasn't changed, it will show you the list of active endpoints. Easy.

However, it is fairly easy to miss something with this approach. Maybe the actuators are living under a different base path. Or maybe the actuators are running on a different port. Clearly, this approach has its limitations. So, like any good engineer, let's start looking at how this process can be improved.

Automatically Querying Many URLs for Actuators

The first thing we may want to do is to automate the process of checking these URLs. For this, I prefer to use the swiss army knife of request generators, ffuf. Written in Go, it is a lightning fast tool for sending lots of HTTP requests based on patterns you provide, and filtering the responses to show you only those you are interested in.

To demonstrate a simple example: Here's how we can check our demo application at http://localhost:8081/ using ffuf:

$ echo actuator > endpoints.txt
$ ffuf -w endpoints.txt:PATH -u http://localhost:8081/PATH

In the first command, we create a file containing the string "actuator" on a single line. With the second command, we invoke ffuf, tell it to use the word list "endpoints.txt" and alias it to PATH, and to generate and query one URL for each line in the endpoints.txt file, replacing PATH with the line from the file. The output looks like this:


        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v1.5.0
________________________________________________

 :: Method           : GET
 :: URL              : http://localhost:8081/PATH
 :: Wordlist         : PATH: endpoints.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405,500
________________________________________________

actuator                [Status: 200, Size: 1758, Words: 1, Lines: 1, Duration: 99ms]
:: Progress: [1/1] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 0 ::

ffuf tells us that by templating the value "actuator" into the URL, it received a response with the HTTP response code 200, with a size of 1758 bytes. This does not guarantee that actuators are actually running there, but it's a good first indicator that we should be taking a closer look.

Now, sometimes system operators try to be clever and move the actuator endpoint to a different URL - maybe they integrate them into an existing URL scheme, or maybe they are just sick of people scraping their systems (cough) and want to make it a bit harder to find. This is where ffuf can start to really help us. For example, let's say we have a list of commonly used alternative actuator base URLs - for example, by going through public code repositories and looking at how they are using the management.endpoints.web.base-path setting. We may end up with something like this:

$ cat endpoints.txt
actuator
actuators
admin
manage
management
manager
metrics
analytics
internal
internal/actuator
api
api/system
api/v1/system
api/manage
api/v1/manage
api/internal
api/internal/actuator
v1/system
v1/actuator
v1/actuators
v1/manage

Given this list, we can easily check if any of these endpoints is available on the server using ffuf, using the same command as above. For example, if we change the actuator base path in the demo application to internal and run the wordlist against it, we get:

        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v1.5.0
________________________________________________

 :: Method           : GET
 :: URL              : http://localhost:8081/PATH
 :: Wordlist         : PATH: endpoints.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405,500
________________________________________________

internal                [Status: 200, Size: 1758, Words: 1, Lines: 1, Duration: 144ms]
:: Progress: [21/21] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 0 ::

ffuf just generated all the requests for us, and only returned those that returned an HTTP status code that indicated something interesting (by default: 200,204,301,302,307,401,403,405,500, as shown above).

Scanning Many Pages

So far, we have only scanned a single page - but maybe your engagement covers a large number of (sub)domains, and you want to check all of them. Or you collected a list of subdomains for Bug Bounty hunting using a tool like amass. In any case, let's assume that the (sub)domains are in a file called domains.txt.

$ cat domains.txt
domain.com
sub1.domain.com
sub2.domain.com
[... etc ...]

ffuf will help you scan all combinations of domains and endpoints quite easily:

$ ffuf -w endpoints.txt:PATH -w domains.txt:DOMAIN -u http://DOMAIN/PATH

Here, ffuf will combine all domains from domains.txt with all endpoints listed in endpoints.txt, and request them all. Note that the number of requests can explode quite quickly, since it means you are sending len(endpoints) * len(domains) requests.

However, it also reveals a potential issue: Since ffuf will only filter based on response code at the moment, you will likely get quite a number of false positives. But we don't want to waste our time checking hundreds of false positives! Can't we do anything to narrow it down a bit more?

Actually, we can.

Anatomy of the Actuators Endpoint

First, let's take a look at what the actuator response looks like. With all actuators enabled, accessing /actuator on the application results in a response like this:

{
   "_links":{
      "self":{
         "href":"http://localhost:8081/actuator",
         "templated":false
      },
      "beans":{
         "href":"http://localhost:8081/actuator/beans",
         "templated":false
      },
      "caches-cache":{
         "href":"http://localhost:8081/actuator/caches/{cache}",
         "templated":true
      },
      /* a bunch more lines like this */
      "mappings":{
         "href":"http://localhost:8081/actuator/mappings",
         "templated":false
      }
   }
}

If we change the configuration to only enable the health check actuator (the default), the result is shorter:

{
   "_links":{
      "self":{
         "href":"http://localhost:8081/actuator",
         "templated":false
      },
      "health":{
         "href":"http://localhost:8081/actuator/health",
         "templated":false
      },
      "health-path":{
         "href":"http://localhost:8081/actuator/health/{*path}",
         "templated":true
      }
   }
}

And even if we disable all actuator endpoints (by removing the "include" setting and configuring management.endpoints.web.exposure.exclude="health"), we still get a response on /actuator that looks like this:

{
   "_links":{
      "self":{
         "href":"http://localhost:8081/actuator",
         "templated":false
      }
   }
}

(While this response does not indicate any direct danger, it can still be interesting in a black-box engagement, as it tells you that the application is running Spring.)

Looking at these responses, we can see that the response will always start with the _links key in the JSON, which in turn will always start with the self key. So, this seems like a good indicator to use when building our automation.

Filtering Responses with ffuf

With this knowledge, we can start using the filtering functionality of ffuf. Briefly, you can tell it that a response must contain specific strings (using matchers), or inversely, cannot contain them (using filters). Using the knowledge about actuators, let's write a matcher that requires the response to contain the string {"_links":{"self": :

$ ffuf -w endpoints.txt:PATH -w domains.txt:DOMAIN -mr '{"_links":{"self":' -u http://DOMAIN/PATH

With this command, ffuf will only return responses that contain this substring, significantly cutting down on false positive results. You can also refine this further using additional matchers and filters, if you find that you are still getting too many incorrect responses. While this will likely not be necessary for this use case, in other situations, being able to filter based on response size or HTTP response code could be the more fruitful approach.

Other Options, and Why You Should Still Use ffuf

Of course, you can also use existing tools to find actuators - ZAP has an active scan rule for it, and Nuclei as well. So, why bother with ffuf?

Well, the existing Nuclei and ZAP rules are quite limited - they will not find actuators that are located on a different endpoint than /actuator (in the case of Nuclei), or only check for the /actuator/health endpoint (in the case of ZAP). So, you will likely miss a lot of potential targets when using them.

More importantly, I prefer to understand how my workflows are working and how I can tweak them. With ffuf, I have full control over the requests and can craft them however I need them. Add an authentication header (or a header required by the bug bounty program)? Sure, let's just add -H "Authorization: Bearer ... to the call. Or a cookie? -b "CookieName=value CookieName2=value2". Record the responses you receive? -od /path/to/folder.

ffuf is my swiss army knife for creating lots of HTTP requests, and I believe that it deserves a place in the toolbox of any security tester dealing with HTTP - regardless of if you want to use it for fuzzing, enumeration, or recon. This article only scratches the surface, and does not even mention features like recursion, or using extensions for your wordlists. If you want to know more, I recommend the epic tome of knowledge aptly named "Everything you need to know about ffuf" by Codingo and p4fg as a starting point.

This concludes the third article in my series on Spring Actuators. In the next and final article of the series, I will switch perspectives to the defender and discuss how you can secure an application that is using actuators.

Once again, I would like to thank the Security Community at my employer, iteratec, for their helpful input on this issue.