Dynamic caching: What could go wrong?
Tl;Dr
The Engintron plugin for CPanel presents a default configuration which could expose applications to account takeover and / or sensitive data exposure due to cache poisoning attacks.
Whenever a client sends a request to a web server, the received response is processed and served by the back-end service each time.
In case of an high traffic volume, this behavior could generate a server overload, resulting in service performance issues. To solve this problem, reverse proxies implements mechanisms such as the web cache. When a user sends a request to a reverse proxy, the nginx core module will first check if a valid response is available in its caching storage. if no valid response is found, the original client request is then forwarded to the webserver, and the response is stored for future use before being send back to the client.
When another user will request the same resource, the nginx core will serve the response stored in the cache, instead of forwarding the request to the backend server, resulting in a much more fluent browsing for the client.
Once the cache time is expired, the cached response is deleted. When another user request the same resource, the flow starts again by getting a new response from the webserver, storing it in the cache and so on.
Web-cache is also a complex mechanism which could easily be misconfigured, resulting in a wide variety of attack vectors.
Finding the bug
Engintron is an Nginx implementation for CPanel, which comes with some pre-enabled advanced functionalities, such as a micro-caching service for dynamic HTML content.
This caching service allows the storage of dynamic HTML responses in the cache for 1 second. To avoid caching responses containing sensitive information, the application avoids caching responses for requests carrying cookies or urls with some common prefix
The cache key is set to $MOBILE$scheme$host$request_uri
, meaning that two users sending a request to the same URL could receive the same responses.
Attack scenario
Scenario: A small webapplication used to send personal information for a candidacy, hosted by an Apache Web Server behind Nginx and running on a CPanel instance. The Nginx implementation is handled with the plugin “Engintron”.
Session handling is required, as the application allows to resume the candidacy later on by using a password set during the initial submission.
The first step requires some basic personal information, an email address and a password.
The email and the password can be used to resume the candidacy later on.
When submitting this form, the backend writes the provided information into a temporary file, and the client gets redirected to the second step.
After a redirect to step2.php, some additional information are required, before being able to submit the final candidacy.
When the form gets submitted the temporary file is deleted, and the session is discarded as not useful anymore.
The attack
the response where the session cookie is being set is not handling cache control, exposing the application to some sort of web-cache poisoning attack, which would lead to account takeovers and sensitive data disclosure.
As mentioned at the beginning of this article, Engintron presents a micro-caching service which holds dynamic HTML resources in the reverse proxy cache for 1 second. Let’s analyze some responses:
A legitimate request carrying the set “session-cookie” results in the Engintron cache being ignored. To verify this it is possible to send several requests in a short period of time, looking for discrepances in the responses, or for some header containing cache directives, such as “X-Nginx-Upstream-Cache-Status”. In this case, the cache-status header explicitly declare a bypass of the cached context.
Using the Burpsuite intruder we can send the request 100 times in a short period of time to verify it
and as expected, there is no difference in any of the responses
This happens because the “session-cookie” cookie matches the engintron regex and prevents caching of private responses.
To get a cached response we have to provide a request which does not get validated by any of the Engintron cache bypass conditions.
This is possible simply by removing the Cookie header from the request.
The cached response contained a really interesting header: “Set-Cookie”. This header is setting the session-cookie value for the current user, identifying a session.
By sending the request twice in the same second, we would get the same set-cookie header.
To verify this, we can wrap a curl command in a while loop and observe that multiple responses are carrying the same value.
Because of this, an attacker could automate the process of retrieving valid session cookies from the cached context, and try to use them to retrieve user sensitive information before he submits the final form.
To perform this attack I’ve built a small tool written in GO.
Please excuse me for my bad code writing skill, I will try to explain the relevant parts of the exploit code.
The exploit starts a thread which collects a new cookie for each second, storing it in a JSON file
The cookie struct is holding the cookie value, how many times it got used by the script (Count), and if it got used to retrieve sensitive information (Consumed).
the JSON file looks like this
While the first thread collects cookies, a second thread is spawned to collect any new sensitive data associated with the stolen sessions.
When the “mydata.php” page content length is greater then 933, it means that some data has been stored, and in that case a copy of the response would get saved.
Follows a video showing a proof of concept of the mentioned exploit
This kind of application logic is common in many scenarios such as job candidacies. The impact of this issue highly depends on the kind of data processed by the application.
Another thing to note is that detecting attacks like this would be really difficult, as the generated traffic would look legit.
Remediation
As a workaround for this issue it is recommended to disable the dynamic cache service from Engintron. To do this, it is necessary to comment the line 53 in the configuration file located at /etc/nginx/common_http.conf