Rating:
# KVCloud (WeCTF 2020)
_This solution was developed by teammates Jeff Delamare, Lydia Doza, and Evan Johnson (me) through_
_several hours of remote collaboration._
## The Challenge
The target was a server running a vulnerable Flask application. The app's "official" function was
to serve as an intermediary between users and a Redis database, but the intended solution did not
involve compromising the database.
Many thanks to the challenge authors and WeCTF organizers for putting this one together!
### Summary
The challenge handout was a [zip archive](https://github.com/wectf/2020p/blob/master/kvcloud/handout.zip)
containing source code for a small Flask app that implements a
simple HTTP-based API to interact with a Redis database. The app provides ways to set and get
specific keys. It also has some helper functions to interact with the backend database and a handler
for a `/debug` route that executes user input as code (!!!). However, the `/debug` route handler
refuses to do anything interesting unless it receives a POST request from 127.0.0.1 (i.e. localhost,
the machine running the Flask app).
The Dockerfile in the handout instructs Docker to copy a file of the same name from the project directory.
This implies that the flag on the server should be in `/flag.txt`. It also helpfully removes access to a bunch of
Redis commands that could be abused to extract the flag. We took this as a hint that the easiest path to
the flag would not be through Redis.
### Helpful background knowledge
#### For reading the challenge source code
- familiarity with Flask [routing](https://flask.palletsprojects.com/en/1.1.x/quickstart/#routing)
and the [`Request` objects](https://flask.palletsprojects.com/en/1.1.x/quickstart/#accessing-request-data) available
inside route handlers
- familiarity with Python 3.x, specifically:
- format strings, i.e. `"{}".format(5)` becomes `"5"`
- the `**` operator to pass keyword arguments from a `dict`, i.e. `some_function(1, **{'a': "xyz", 'b': 3})` becomes `some_function(1, a="xyz", b=3)`
#### For the exploit itself
- HTTP request [methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) and [headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods)
- Python file I/O or use of the `os` module to run shell commands
## Spotting vulns
`exec` stuck out like a sore thumb. It interprets a string as Python code and executes that code
without a care in the world for what it might do. Knowing that, it was clear to us that if we
could get the program to pass an input from us as the argument to `exec`, we could do pretty
much anything.
However, inputs to that function had to come from HTTP requests originating (or appearing to
originate) from the same machine. Spoofing the source IP of packets seemed plausible, but we felt
there might be an easier way when we noticed a few key details:
- `redis_get` and `redis_set` both dump strings read from query parameters in a request URL
directly into the strings they send to the database.
- `redis_get` and `redis_set` both allow custom destination addresses and ports to be passed as
arguments, and the routes that use them pass the values from any URL query parameters named
"redis_addr" and "redis_port" to these functions without any checks. So we could control where
the command strings these functions construct by adding the appropriate query parameters to
request URLs.
- `redis_get` and `redis_set` send the command strings they construct directly through a TCP
connection to the specified destination. (HTTP requests are also typically sent/received over TCP).
- While both helpers always prefix their command strings with a hard-coded string, `redis_get` in
particular starts its command string with `"GET "`. This correctly constructs a command for Redis,
but it could also be the start of a HTTP GET request.
Combining those observations, we were very confident that we could craft a request URL that would
cause the server to send itself HTTP requests from the `redis_get` function.
## Putting the pieces together
From our initial observations, we had a possible way to execute arbitrary code and a way to make
the target server send itself a GET request. However, the code path we needed for remote code
execution (RCE) was only reachable by a POST request.
Jeff mentioned that HTTP request smuggling is a thing, and we started looking into that.
A cursory glance at a few blogs seemed promising--following the GET with a POST was what we
wanted to do. However, the examples we found didn't work either on our local test instances or the
challenge server (_Hindsight: because it was SSRF, not request forgery!_).
As a result, it took some experimentation for us to make `redis_get` send two requests.
After some effort, we managed to get proof of concept on a local instance of the Flask app.
Our POST request (appended to the GET constructed by abusing `redis_get`) could reach the `exec` call.
However, our Python payload wasn't getting through. Eventually, after a bit of research and
much experimentation, we figured out the issue.
## Payload breakdown
_note: This is a cleaner, refined version of the solution we actually used. Our initial solution_
_attempted to overwrite our copy of the flag in the database after we'd retrieved it, but that_
_could just as easily be done manually._
### Hijacking `redis_get`
The /debug route where a form field’s contents are passed to Python’s `exec` function rejects requests
made from remote hosts, so we need to send a request to that route from the server itself. `exec`
executes a string as Python code without safety checks, so it provides easy remote code execution--if
we can get to it. To do that, we need to trick the server into sending itself a POST to `/debug`.
#### Send data to chosen destination
The function takes keyword arguments to specify alternative destinations for a string sent directly over
TCP. By default, the destination is some Redis database server. However, the code handling the /get route
allows the alternate destination to be set using query parameters in the request URL.
#### Turn a database command into a HTTP request
The `redis_get` function always prefixes the string it sends with `‘GET ’`, and otherwise just uses whatever
input the `/get` route found in the `key` query parameter. If the resulting string is sent over TCP to a
Redis database server, the database will treat it as a command to get the value of some key. If it is
instead sent to an HTTP server, it will be interpreted as an HTTP GET request! So setting the
correct destination address and port for this function causes the Flask app to send itself a GET request.
#### The URL
`http://kvcloud.sf.ctf.so:80/get?redis_port=80&redis_addr=localhost&key=[payload]`
The payload needs to be URL encoded so it can be sent as the value of a request parameter. The target
Flask app will un-encode the payload before passing it to `redis_get`. `redis_addr=localhost` and
`redis_port=80` set the custom destination address and port, respectively. The port needs to be the port
on which the server handles HTTP requests. This is specified on the last line of `app.py`.
### POSTing for RCE
The `redis_get` function sends our payload input to any destination we choose, but it always prefixes
our input with GET! But a POST request is the only way to reach the `exec` call.
Following the mandatory GET request with a POST took some tuning, because we started out with the
wrong mechanism (request smuggling) in mind.
Ultimately, we found that we needed two sequential requests: a minimal GET request and a POST request carrying
Python code to retrieve the flag.
Both requests get sent by the target server, to the target server. The GET request’s only job
is to consume a hard-coded prefix in a format string and not make a mess, so it
doesn't need to request a particular URL. (We chose the URL for a site designed to respond slowly for app testing purposes.)
All that matters is that the POST request goes to the `/debug`
route. The request will pass the origin IP check because it came from the server itself.
#### Request text
```
GET http://slowwly.robertomurray.co.uk/ HTTP/1.1
Host: robertomurray.co.uk
POST http://localhost/debug HTTP/1.1
Host: localhost
Keep-Alive: timeout=200 max=1000
Content-Type: application/x-www-form-urlencoded
Content-Length: 61
cmd=redis_set('ohplease', str(open('/flag.txt', 'rb').read())
```
The initial `GET ` (with a space) is hard-coded into the `redis_get` function. Everything after those
four characters is the string we needed passed as the `key` argument to `redis_get`. That entire
string must be URL encoded.
### Flag exfiltration
We tried to send data directly back over to the `redis_get` call using Flask’s `send_file` function,
but couldn’t get it to work. Ultimately, we settled on using the Redis database itself, since we could
retrieve and set keys using the target server’s intended functionality.
The Python code needed for this is just `redis_set('ohplease', open('/flag.txt', 'r').read())`.
During competition, we used multiple statements separated by semicolons to avoid any confusion or extra
complexity around newlines in the request construction.
This was our first time smuggling a request and we were working hard enough to figure things out,
so we didn’t want to have to think about any rules for passing newlines in a form field. We also used
binary read mode in our initial solve because we were tired and in a hurry to get the flag before the
CTF game ended.
After our payload script stored the flag in the Redis database, Lydia retrieved it using the `/get`
route for its stated purpose. I then used the `/set` route to set a new value for the key we'd used.
#### How it works
`open('/flag.txt', 'r')` returns a handle to a file object in read (text) mode. That object's `read()` method
that gets all of the file's contents as a string.
`redis_set('ohplease', value)` sends "SET ohplease <value>" to the default destination, which is the Redis
database server. This sets the value of key 'ohplease' to whatever string `value` represents. In this case,
it's the contents of the flag file.
## What I learned
### My first SSRF!
I didn't know about SSRF going into this challenge. Since our breakthrough arose from discussion
of request smuggling, that was the term on my mind while solving.
My understanding after further review and reading is that our attack was not request smuggling,
but rather simply sending two valid requests in sequence. Since we tricked the server into
sending a request for us, this was a server-side request forgery (SSRF) attack.
Request smuggling is different: it uses differences in the handling of malformed requests by a front-end and
back-end server to sneak a request from the attacker to their target.
#### Resources
SSRF: [https://owasp.org/www-community/attacks/Server_Side_Request_Forgery](https://owasp.org/www-community/attacks/Server_Side_Request_Forgery)
Request Smuggling: [https://portswigger.net/web-security/request-smuggling](https://portswigger.net/web-security/request-smuggling)