Features
The web application’s core functionality mainly revolved around document management within an organisation. Two privilege levels existed for users; standard users and server admins. We were provided with one user for each privilege level. Some hours into the test, we had already found a couple of stored XSS vulnerabilities, as well as broken access controls that allowed standard users to invoke administrative functions.
Good start, but the web application was built with PHP.
Needless to say, custom built PHP applications combined with document upload/download functionality are a recipe for juicy vulnerabilities, so that’s where we focused our efforts. It is also worth mentioning that the target web server was kind enough to introduce itself without any real poking:
server: Apache
Methodology
Ideally, to get Remote Code Execution (RCE) we’d upload a PHP file and then request it’s corresponding URL after identifying where our uploaded file gets stored. Unfortunately for us, we couldn’t find such a vulnerability since the web application seemed to be storing our uploaded files outside of the web application’s DocumentRoot
. We arrived at this conclusion after encountering this peculiar response to one of our requests that showed where our uploaded files were being kept in the file system:
pageImageAvailable(
1,
'63dpi',
"https://redacted.com/php/fetchurl.php?path=/2023-02-20/H3xH6tha//////page-63dpi-000001.png"
);
// view
We also found that there was developer documentation for the web application publicly available on the internet which revealed that the DocumentRoot
was set to Apache’s default /var/www/
.
Next Steps
The next best thing would be a file inclusion vulnerability. There were a number of endpoints that fetched internal files from the OS’s file system and they all sanitised directory traversal sequences by replacing them with underscores. That is, all but one…
The application contained something called ‘projects’ which were basically storage units for uploaded documents. There was a feature that allowed downloading entire projects as ZIP files. The request to invoke this feature looked like the following:
POST /php/wsZipper.php?xss=UxlwITGWvV HTTP/1.1
Host: redacted.com
Cookie: PHPSESSID=...;
Content-Length: 40
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Connection: close
wsid=112762723&pwd=s3cretP4ssw0rd&xss=UxlwITGWvV
Response:
HTTP/1.1 200 OK
date: Tue, 21 Feb 2023 10:01:37 GMT
server: Apache
expires: Thu, 19 Nov 1981 08:52:00 GMT
cache-control: no-store, no-cache, must-revalidate
pragma: no-cache
strict-transport-security: max-age=31536000
x-frame-options: deny
Content-Length: 36
content-type: text/html; charset=UTF-8
connection: close
OK-112762723_2023_02_21_10_01_37.zip
The request would cause the server to generate the ZIP file and then return the name of the generated file in the response. The client-side application would then use the file name from the above response to download the ZIP file using the following URL:
https://redacted.com/php/exportDownloader.php?fp=112762723_2023_02_21_10_01_37.zip&prefix=wszip&ws=112762725&xss=gxI2YIXzcu
Replacing fp
parameter with a file that didn’t exist showed something interesting:
https://redacted.com/php/exportDownloader.php?fp=doesntexist&prefix=wszip&ws=112762725&xss=gxI2YIXzcu
ERR - No file exists /opt/redacted/pentest/private/wsarchives/112762725/adam@ruptura-infosec.com/doesntexist
The path of the file to download was being shown in the response if the file didn’t exist. Naturally, the next step was to try injecting directory traversal sequences to access a file we weren’t supposed to:
https://redacted.com/php/exportDownloader.php?fp=../doesntexist&prefix=wszip&ws=112762725&xss=gxI2YIXzcu
ERR – No file exists /opt/redacted/pentest/private/wsarchives/112762725/adam@ruptura-infosec.com/__doesntexist
Again, there was some sanitisation going on. Trying different bypasses didn’t yield any progress either. But notice how one of the other parameters is also present in the path (namely the ws
parameter). Injecting directory traversal sequences here showed the following:
https://redacted.com/php/exportDownloader.php?fp=doesntexist&prefix=wszip&ws=../112762725&xss=gxI2YIXzcu
ERR - No file exists /opt/redacted/pentest/private/wsarchives/../112762725/adam@ruptura-infosec.com/doesntexist
Not only is the parameter used in the path, but also directory traversal sequences within the parameter were not being sanitised!! 😀
Continuing
But hold the phone. In the file path, between the ws
parameter and the fp
parameter was the email address of our current session. We confirmed this by performing the same steps using another account. Summarising what we have so far:
- The URL in question returns the contents of the file at
/opt/redacted/pentest/private/wsarchives/{ws_parameter}/{account_email}/{fp_parameter}
. - The
fp
parameter cannot contain directory traversal sequences. - The
ws
parameter can contain directory traversal sequences.
With that said, to exploit this endpoint we need to have an account with an email address that can either contain directory traversal sequences, or be set to the parent directory of our target file (‘etc’ if the target file was /etc/passwd
).
There was no functionality to change the email address of an existing user. Therefore we had to register a new user with an invalid email address. There were 3 ways to register a new user:
- The server admin can register a new user using a form in the administration panel.
- The server admin can register new user(s) by importing a CSV or XLSX file.
- An unauthenticated user can register using a registration form.
Long story short, only the third option was feasible for our goal since, unlike the other 2 methods, the email field wasn’t being validated:
POST /php/formregister.php HTTP/1.1
Host: redacted.com
Cookie: PHPSESSID=...
Content-Length: 77
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Connection: close
e=etc&fn=First&ln=Last&p=s3cretP4ssw0rd&pr=s3cretP4ssw0rd&legal=on&token=
However, the backend didn’t allow for the email address to contain any forward slashes, nor did it allow for them to start with a period for some reason. In other words, assuming we were able to pull this off we could not read files which had a parent directory that was hidden
(RIP🪦 .ssh/id_rsa
) .
Login As
There was still another obstacle though. It wasn’t possible to login to the new account without verifying the email address. Even the button to manually verify accounts on the admin panel didn’t work for accounts that had an invalid email address. Luckily, it didn’t take too long before finding a button on the admin panel that relieved us.
Putting it all together, after registering our etc
user we authenticated to the application using our admin user. Then we used the convenient ‘login as’ button associated with the etc
user. Finally, we requested the following URL to read /etc/passwd
:
https://redacted.com/php/exportDownloader.php?fp=passwd&prefix=wszip&ws=../../../../../../../&xss=gxI2YIXzcu
Sweet!! Investigating further revealed that we were only able to read local files and that the files were not included using PHP’s include
function (or the like).
Nevertheless, a win is a win. 🤓)
Conclusion
The target at hand did have protections in-place on some of the endpoints which we poked at. But when it comes to security, applying security measures to only a subset of a system’s components is not enough. The vulnerability displayed here is a prime example of the commonly-mentioned notion that a target’s security posture is only as strong as the weakest link in the chain. For every obstacle we encountered there was only one way around and, thankfully, that was enough to develop a full exploit.
Credits
Our client for permission to publish this.