First Steps
In the first stage of testing this application, uploading a file sent a HTTP request that looked like this:
POST /fileHandler.aspx HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryD4wQE8f4tbelwRJn
------WebKitFormBoundaryD4wQE8f4tbelwRJn
Content-Disposition: form-data; name="action"
FILEUPLOAD
------WebKitFormBoundaryD4wQE8f4tbelwRJn
Content-Disposition: form-data; name="file"; filename="helloworld.txt"
Content-Type: application/octet-stream
hello!
------WebKitFormBoundaryD4wQE8f4tbelwRJn
Content-Disposition: form-data; name="path"
C:\inetpub\wwwroot\uploads\896582\Files
------WebKitFormBoundaryD4wQE8f4tbelwRJn--
A few modifications here and there within a web proxy and it was possible to convert this seemingly standard file upload into a remote code execution vulnerability.
We were able to modify the “path” parameter to upload a file to any arbitrary location on the remote file server. It was also possible to give this any file extension, so naturally we opted for an .aspx (.NET app). I’m sure many of you can see where this is heading, it was possible to upload a malicious aspx webshell to any location, browse to that location somewhere in the webroot and subsequently execute server-side code. That was fun, but certainly not cool enough to make our hack of the month blogs :D.
The "Fix"
After reporting the issue to the client during the testing, a simple and seemingly effective patch was released. When the page now was generated, the current directory that the user was viewing was stored in the “path” parameter. In the previous test, this was the full path of the file system. In the new version, this parameter was encrypted and requests ended up looking something like:
POST /fileHandler.aspx HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=—-WebKitFormBoundaryD4wQE8f4tbelwRJn
——WebKitFormBoundaryD4wQE8f4tbelwRJn
Content-Disposition: form-data; name=“action”
FILEUPLOAD
——WebKitFormBoundaryD4wQE8f4tbelwRJn
Content-Disposition: form-data; name=“file”; filename=“helloworld.txt”
Content-Type: application/octet-stream
hello!
——WebKitFormBoundaryD4wQE8f4tbelwRJn
Content-Disposition: form-data; name=“path”
hx3f11M3j6Qj0YezHIJEyjLfOxb4JIRaxRnxpEC9ArCEFeclnI/DDqshd7TaLbif
——WebKitFormBoundaryD4wQE8f4tbelwRJn–
ASP.NET makes this really easy to do with their Data Protection APIs, meaning weak crypto implementations or use of weak keys were likely out of the picture for exploitation. Any attempt to modify the encrypted data would result in a “400 Bad Request”.
The application also utilised ASP.NET’s ViewState, which uses those same APIs to pass a serialised object to the client and back. Applications use this to pass around things like form data, logged in status, etc. between the client and backend. As the state is encrypted, it’s not possible for the client to modify it without knowledge of the key.
In this case, the current directory that the user was viewing was stored within the ViewState. An attacker would therefore not able to modify the ViewState object and therefore couldn’t pass an arbitrary path to the upload endpoint. Right?
Continued
Well, using this encrypted path assumes that it would be impossible for the client to somehow influence the data stored within the state. Except, there was another action within another endpoint that let us specify an arbitrary path and store it within the ViewState. We could then visit another page which would give us the encrypted path we had previously specified. Using this value, we could then pass the encrypted value back to the fileHandler.aspx endpoint and once again upload to whatever path we wanted!
We firstly send a request to the endpoint that encrypts any value we want, storing the path within the encrypted ViewState:
Conclusion
Although the client’s initial fix wasn’t bad, it highlights the importance of thoroughly retesting issues once a “fix” has been applied. In 90% of cases, we believe that this doesn’t take place and that many testers may have simply closed this issue off once the initial patch had been applied.
That being said this type of multi-stage exploit isn’t always easy to catch, either in a live assessment or in a source code review. Reviewing components in isolation to the rest of the system can lead to making false assumptions about how additional components can interact with each other.
We of course recommend that regular web application penetration testing is essential and to confirm that fixes are always fully validated.