securityctf

SSRF to Redis CTF Solution

by Steve Marx on

The HashCache Capture the Flag (CTF) challenge has fallen to Pierre Rosenzweig, a pentester and cybersecurity consultant at Wavestone France. Congratulations, Pierre!

In this post, I’ll describe the solution step by step. If you still want to try to solve the challenge yourself, stop reading now. Spoilers are coming.

The application to be hacked

If you haven’t already, take a look at the README for the challenge. The high-level goal is to read the flag key from Redis.

The system you’re attacking is pretty simple. There’s a web application written in Nim and a Redis server that is used to cache values (and hold the flag).

The HashCache web application can be used to get the hash of the content at a given URL. It caches these hashes for 5 minutes. The web application has two endpoints:

  1. / just returns the contents of index.html. This is just an HTML form that’s used to invoke /hash.
  2. /hash fetches a given URL, using a specified HTTP method and an optional Authorization header. It then hashes the body of the HTTP response and returns that. It also stores the resulting hash in Redis under the key hash:<SHA-1 hash of the query string>.

Server-side request forgery

Any time you see a system that makes user-specified HTTP requests, you should immediately think about server-side request forgery (SSRF) attacks.

The goal of an SSRF attack is to reach a network resource that you couldn’t otherwise reach, such as admin interfaces and databases. Those are often restricted to local network access, so you can’t attack them directly. In an SSRF attack, you (ab)use the target application itself to make the network connection for you.

In the case of the HashCache CTF, you have a web application that will happily make any HTTP request you ask it to. This means you can request, for example, http://redis:6379 to reach the Redis server. (If you’re running the system locally, you’ll probably be using http://127.0.0.1:6379 instead.)

Redis protocol

The existence of an SSRF is promising, but the only protocol allowed is HTTP, and Redis doesn’t accept HTTP requests. The task now becomes crafting an HTTP request that will look to Redis like valid instructions that help reveal the flag.

Redis speaks the REdis Serialization Protocol (RESP), a fairly simple text-based protocol. A good first step is to examine an HTTP request and seeing Redis interprets it. Below, I’m running the app with nimble run, and I’m using netcat to listen on port 12345. Then I can browse to the app’s UI on port 8080 and ask it to fetch the URL http://127.0.0.1:12345. Here’s the resulting HTTP request:

$ nc -l -p 12345
GET / HTTP/1.1
Host: 127.0.0.1:12345
Connection: Keep-Alive
content-length: 0
user-agent: Nim httpclient/1.2.6

Now I can connect to Redis and type those lines one by one to see what happens:

$ nc 127.0.0.1 6379
GET / HTTP/1.1
-ERR wrong number of arguments for 'get' command
Host: 127.0.0.1:12345

The RESP protocol uses \r\n (CRLF) to separate different commands. This is the same line ending used by HTTP, so Redis treats each line of the HTTP request as a separate command.

The response to the first command is no surprise. Redis has a command called GET, but it only accepts a single parameter.

The response to the second command is a problem. Redis hangs up the connection! This is because Redis added protection against this sort of SSRF attack back in 2017. To avoid being duped by HTTP requests, Redis hangs up as soon as it sees the command POST or Host: (both case insensitive).

What can you do before the Host: line?

Digging through Nim’s httpClient code, you can see that a Host: header is always sent as the second line of the HTTP request, so Redis will always hang up after this line is sent.

That means if you’re going to do something, it has to be part of the first line. You have control of three different parameters:

  • url, which impacts the first line of the HTTP request as well as the Host header
  • method, which is the very first thing sent as part of the request
  • authorization, which is a header that always appears after the Host header

CRLF injection

You may have heard of CRLF injection, which is where an attacker injects their own CRLF into some field of an HTTP request. This can modify the HTTP request. For example, suppose you supplied http://127.0.0.1:6379/foo\r\nSteve was here!\r\n as the URL. The resulting HTTP request would be something like this:

GET /foo
Steve was here!
 HTTP/1.1
Host: 127.0.0.1:6379

This looks very promising because it lets you sneak in commands, on lines by themselves, before the Host: line.

Unfortunately, Nim is one step ahead of you. httpClient explicitly asserts that the URL has no carriage return or line feed characters. That assert wasn’t always there, leading to CVE-2020-15693 in Nim 1.2.4, but it’s fixed in Nim 1.2.6, the version the CTF uses.

CRLF injection in the HTTP method

Nim blocked you from using the URL, but remember that you also control the HTTP method, which is right at the beginning of the HTTP request.

This turns out to work. If you use GET\r\nSteve was here!\r\n as your method parameter, you’ll get this HTTP request:

GET
STEVE WAS HERE!
 / HTTP/1.1
Host: 127.0.0.1:6379

Congratulations! You can now send arbitrary commands to Redis. Well, arbitrary commands with one caveat. The HTTP method is always converted to uppercase. This is actually a problem for collecting the flag, which is under the lowercase key flag.

My intention was that the challenge cannot be solved solely by injecting something into the method parameter, but it’s possible I missed something. If you can think of a way to do it with just all-uppercase commands, please let me know!

CRLF in the Authorization header

Nim’s httpClient doesn’t prevent CRLF injections into headers either, so you can use the same trick there. An authorization parameter of \r\nSteve was here!\r\n yields this request:

GET / HTTP/1.1
Host: 127.0.0.1:12345
Connection: Keep-Alive
content-length: 0
user-agent: Nim httpclient/1.2.6
authorization: 
Steve was here!

This is nice because it’s another place to inject Redis commands, but it occurs after the Host: line, so Redis will hang up before it even sees it.

Putting it all together

Recapping:

  1. The URL lets you reach Redis.
  2. The method lets you do CRLF injection, but it converts everything to uppercase.
  3. The authorization header lets you do CRLF injection too, but this appears after the host header, and Redis hangs up as soon as it sees that.

There’s no way to prevent the host header from being sent, and there’s no way to use the authorization header if Redis sees the host header first.

The key is that we need Redis to not see the host header. Fortunately, the Redis protocol gives us a way to do that. I said earlier that the Redis protocol uses CRLF line endings, but Redis supports arbitrary binary strings, so it has to somehow accept strings containing those characters.

The Redis protocol uses “bulk strings”, which are length-prefixed strings. When Redis sees a bulk string, it reads the given number of bytes without attempting to parse them. Using this, you can get Redis to skip past the host header altogether.

When using explicit RESP encoding, a Redis command is an array, where the first element is a string command and subsequent elements are parameters. An array of five elements is introduced with *5\r\n, and then the next 5 things parsed are the elements of the array. A bulk string of size 8 is introduced with $8\r\n. The next 8 bytes are the content of that string, and the next two bytes have to be \r\n to terminate the bulk string.

We can put those things together to get Redis to view the host header as just part of a string. Here are the parameters:

url = "http://127.0.0.1:6379"
method = "*1\r\n$20\r\n"
authorization = "\r\nGET flag\r\n"

I picked the length 20 (in $20) somewhat arbitrarily. It has to be long enough to jump past at least the h in host:, and it has to be short enough that Redis will still see my command GET flag.

Here’s what that looks like URL-encoded into a curl command:

curl 'http://127.0.0.1:8080/hash?url=http://127.0.0.1:6379&method=*1%0D%0A$20%0D%0A&authorization=%0D%0AGET%20flag%0D%0A'

And here’s the resulting HTTP request:

*1
$20
 / HTTP/1.1
Host: 127.0.0.1:12345
Connection: Keep-Alive
content-length: 0
user-agent: Nim httpclient/1.2.6
authorization: 
GET flag

But there’s one final wrinkle… the web application doesn’t actually give you the response body! The above instructs Redis to give up the flag, but there’s no way for you to see it.

Data exfiltration

The last step of the puzzle is to craft the right payload so the flag can be exfiltrated.

There are a lot of potential options here, but I tried to eliminate most of them in redis.conf. Without Lua scripting and file access, you’ll have to find an application-specific way to read the data.

The challenge app only provides one way to read data from Redis. When you make a request to the /hash endpoint, the query string is SHA-1 hashed, and then the key hash:<SHA-1 hash> is checked. If there’s a value there, it’s returned. So if you can find a way to get the value of the flag key into a key like hash:<SHA-1(foo=bar)>, you can then just request /hash?foo=bar to retrieve the flag.

The RENAME command fits the bill. Instead of trying to directly GET the flag, rename it to a hash you know you can access.

Below I’ve used the SHA1 hash of “gimme=now”, which is 37573C6A2DE58DFBD1C7507016D45291CBEFC92C. Be sure to convert your hash to uppercase because that’s what Nim does. Also be careful to use the right Redis hostname. If you’re running locally, outside of Docker, then you just want to use 127.0.0.1 or wherever you installed Redis. But in the deployed version or in a local version you ran with docker-compose, you’ll want to use redis as your URL hostname because that’s what the Docker setup uses.

$ curl 'http://hashcache-ctf.smarx.com/hash?url=http://redis:6379&method=*1%0D%0A$20%0D%0A&authorization=%0D%0ARENAME%20flag%20hash:37573C6A2DE58DFBD1C7507016D45291CBEFC92C%0D%0A'
ERROR: Failed to retrieve content.

$ curl http://hashcache-ctf.smarx.com/hash?gimme=now
flag{SSRF_2_R3d1s}

Takeaways

SSRF attacks exploit a server-side component that will contact protected endpoints on behalf of an attacker. If your application makes network requests, be careful about what parameters you let an attacker control. If you have to support potentially dangerous URLs, consider forwarding requests through a proxy server that’s isolated from your internal network.

Just because an attacker can only make HTTP requests doesn’t mean they can’t exploit non-HTTP services. Remember that an HTTP request is just some bytes sent over TCP. Many protocols, particularly text-based ones, can be tricked into handling an HTTP request as though it were something else.

Use passwords, even for internal services. This CTF would have been impossible if I had configured Redis to require a password on all inbound connections.

That was fun!

It was more work than I expected to craft this CTF challenge, but I had a lot of fun doing it. I also learned a bit as I went. I knew I wanted to show an SSRF attack to Redis, but I hadn’t anticipated a few of the defenses Nim and Redis have in place. This made the solution a little trickier than I had initially envisioned, but part of the reward of a good CTF challenge is overcoming those real-world stumbling blocks.

For those of you who tried to complete the challenge, thank you! I hope to make more of these challenges in the future, so please subscribe to the RSS feed and sign up for the newsletter so you don’t miss them.

Me. In your inbox?

Admit it. You're intrigued.

Subscribe

Related posts