# 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 wasto serve as an intermediary between users and a Redis database, but the intended solution did notinvolve 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 asimple HTTP-based API to interact with a Redis database. The app provides ways to set and getspecific keys. It also has some helper functions to interact with the backend database and a handlerfor a `/debug` route that executes user input as code (!!!). However, the `/debug` route handlerrefuses 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 ofRedis commands that could be abused to extract the flag. We took this as a hint that the easiest path tothe 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) availableinside 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 codewithout a care in the world for what it might do. Knowing that, it was clear to us that if wecould get the program to pass an input from us as the argument to `exec`, we could do prettymuch anything.
However, inputs to that function had to come from HTTP requests originating (or appearing tooriginate) from the same machine. Spoofing the source IP of packets seemed plausible, but we feltthere 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 URLdirectly into the strings they send to the database.
- `redis_get` and `redis_set` both allow custom destination addresses and ports to be passed asarguments, 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 wherethe command strings these functions construct by adding the appropriate query parameters torequest URLs.
- `redis_get` and `redis_set` send the command strings they construct directly through a TCPconnection 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` inparticular 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 wouldcause 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 makethe target server send itself a GET request. However, the code path we needed for remote codeexecution (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 wewanted to do. However, the examples we found didn't work either on our local test instances or thechallenge 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 andmuch 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 requestsmade 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--ifwe can get to it. To do that, we need to trick the server into sending itself a POST to `/debug`.
#### Send data to chosen destinationThe function takes keyword arguments to specify alternative destinations for a string sent directly overTCP. By default, the destination is some Redis database server. However, the code handling the /get routeallows the alternate destination to be set using query parameters in the request URL.
#### Turn a database command into a HTTP requestThe `redis_get` function always prefixes the string it sends with `‘GET ’`, and otherwise just uses whateverinput the `/get` route found in the `key` query parameter. If the resulting string is sent over TCP to aRedis database server, the database will treat it as a command to get the value of some key. If it isinstead sent to an HTTP server, it will be interpreted as an HTTP GET request! So setting thecorrect 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 targetFlask 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 porton 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 prefixesour 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 thewrong mechanism (request smuggling) in mind.Ultimately, we found that we needed two sequential requests: a minimal GET request and a POST request carryingPython code to retrieve the flag.
Both requests get sent by the target server, to the target server. The GET request’s only jobis to consume a hard-coded prefix in a format string and not make a mess, so itdoesn'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.1Host: robertomurray.co.uk
POST http://localhost/debug HTTP/1.1Host: localhostKeep-Alive: timeout=200 max=1000Content-Type: application/x-www-form-urlencodedContent-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 thosefour characters is the string we needed passed as the `key` argument to `redis_get`. That entirestring must be URL encoded.
### Flag exfiltrationWe 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 couldretrieve 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 extracomplexity 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 usedbinary read mode in our initial solve because we were tired and in a hurry to get the flag before theCTF 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()` methodthat 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 Redisdatabase 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 discussionof 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 intosending 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 andback-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)