Recently I’ve been looking at SSRF in Kubernetes. When testing for SSRF, I find it very useful to have a webserver/reverse proxy that I control and can configure to do a number of tasks. I’ve been using Caddy for this. In this post I’ll show you how to use Caddy to test for SSRF.

Setting up Caddy

The first benefit I found, from a researcher/pentester standpoint was ease of installation. Caddy is written in Golang and, in common with most Golang utilities, runs as a single binary. Whilst there are more complex installation methods available you can just grab a binary from the releases page extract the archive (checking signatures/checksums ofc!) and then run it.

Configuring Caddy

There’s two ways to configure Caddy, JSON and Caddyfile. For the kind of simple examples I’m using for SSRF, Caddyfiles are generally a better option as the file is more readable.

In the general case if you have a file called Caddyfile in a directory, you can start the proxy with caddy run. If you want to use a different file, you can use the --config flag. For example, if you have a file called myconfig in the current directory, you can start the proxy with caddy run --config myconfig.

Caddy logging

The first setup I wanted was just to have a log of any requests hitting a port. Handy for determining whether a request is causing an SSRF. Having a server listening on all interfaces on port 80, responding with a fixed string and logging the output looks like this. Note that the log directive is inside the :80 block, as we’re logging request to that port.

# Simple logger for access to port 80
:80 {
  respond "Hello World"
  log {
    output file 80access.log
  }
}

Once you’ve got that, just start it with sudo caddy run and you should see a file called 80access.log in the current directory. If you hit the server with a request, you should see something like this in the log file:

2022/12/27 16:34:42.141 info    http.log.access.log0    handled request {"request": {"remote_ip": "217.155.25.114", "remote_port": "19000", "proto": "HTTP/1.1", "method": "GET", "host": "my.test.server", "uri": "/", "headers": {"Accept-Language": ["en-GB,en;q=0.5"], "Accept-Encoding": ["gzip, deflate"], "Connection": ["keep-alive"], "Upgrade-Insecure-Requests": ["1"], "User-Agent": ["Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0"], "Accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"]}}, "user_id": "", "duration": 0.000073504, "size": 11, "status": 200, "resp_headers": {"Server": ["Caddy"], "Content-Type": ["text/plain; charset=utf-8"]}}

Running on multiple ports and TLS

Adding new ports is just a matter of having another port block in the Caddyfile. For example adding a port 443 block looks like this:

# Simple logger for 443 with valid TLS
my.test.server:443 {
  tls my@email.address
  respond "Hello Secure World"
  log {
    output file 443access.log
  }
}

With this directive I’ve specified a host my.test.server and the tls directive which takes your e-mail address as an argument. The combination of these two settings unlocks a fun trick, which is that caddy will automatically request a TLS certificate for the host you specify (assuming of course that you point the DNS A record at the machine running the server and it’s Internet facing).

This is very useful for services that require a valid TLS certificate.

Internal TLS and “on-demand”

Of course sometimes you’ll be testing on an internal network, so that setup won’t work. If you don’t need valid TLS certs you can use the internal directive to generate self-signed certs. You can then add the on_demand directive to have Caddy generate a cert for any SNI host that’s requested.

:443 {
  tls internal {
    on_demand
  }
  log {
    output file 443access.log
  }
}

It’s worth noting you can use on_demand on an internet facing host too and Caddy will request a valid TLS cert for any host name that points at that server, but it’s not advised as it can cause rate limiting issues with Let’s Encrypt.

Reverse Proxy

So Caddy makes a nice way of handling things like TLS and providing simple responses. Sometimes you might want to use it as a front-end for a webserver running in some other language (e.g. Ruby). Setting Caddy as a reverse proxy is pretty easy just add a line like reverse_proxy :8080 and replace :8080 with the port your webserver is listening on.

Redirect

A handy technique for SSRF can be where you want to convert an initial request which is a POST/PUT to be a GET request. You can do this with Caddy using the redir option. For example if you want to redirect a request to http://169.254.169.254

# Redirect to metadata server
my.test.server:8444 {
  tls my@email.address
  redir http://169.254.169.254
  log {
    output file 8444access.log
  }
}

File server

You can also use Caddy to serve files from a specific directory with the root and file_server directives.

:80 {
  root * /my_files
  file_server
  log {
    output file access.log
  }
}

Respond with JSON

you can use the respond directive and one thing I wanted to do was respond with a JSON object. This is pretty easy, just use the respond directive and then specify the content type and the JSON object, and the content type is application/json. One trick to note is that I needed to remove all spaces in the response for it to work.

Debug

As with any testing things don’t always go to plan, so adding the debug directive at the top of a Caddyfile (not in a port block) will give you a lot more information about what’s going on.

{
  debug
}

Conclusion

This is just a small section of the things you can do with Caddy, and I’d recommend reading the docs for more ideas, but hopefully it’s useful to people wanting to have simple servers for SSRF testing :)


raesene

Security Geek, Kubernetes, Docker, Ruby, Hillwalking