Web cache poisoning is an attack where an attacker takes advantage of flaws in the caching mechanism. They attempt to store an altered and malicious response in the cache entry, forcing the website to serve malicious information to its users.
Web caching allows faster and more smooth surfing by downloading a local copy of a resource and preventing subsequent browser requests from being sent to the remote server. Attackers can exploit vulnerable applications by injecting malicious data into cache memory, which prompts the webserver to send the user a malicious HTTP response.
To understand the web cache poisoning vulnerability, it is essential to know how web caches function.
What is web cache, and how does it work?
The web server always temporarily stores a replica copy of the webpage in its memory whenever a client requests to access a specific web page. To keep the remote server from overloading (from requests), each subsequent incoming request for the same resource is fulfilled using the replica copy stored in memory.
A web cache is a replica copy of the server's response, allowing for faster delivery of web objects (resources) to the client. Additionally, storing data closer to the user reduces network traffic and improves the overall performance of a website.
Based on a set of rules, a web cache only stores HTTP responses for a limited time. Cache keys are the primary mechanism through which a web cache keeps track of what response content to cache. The cache uniquely identifies a response using cache keys, components of an HTTP request. The values of one or more response headers and all or a portion of the URL route are often included in a cache key.
Understanding using an Example
GET /images/info/img?country=IN HTTP/1.1
Host: www.vulnerablesite.com
User-Agent: Mozilla/…..
Accept-Language: en-US
The cache key is represented by the requested parts highlighted in yellow in the above code. Instead of sending the request to the origin, the cache will respond with that record if it matches the cache key to an existing record. As a result, the origin has to deal with a few requests, which lowers the cost to the application server and results in faster response times.
Finding Unkeyed Inputs
The parts of a request that the cache ignores while evaluating whether the requested resource should be supplied from the cache are known as unkeyed inputs.
Let’s consider the following scenario to understand this concept better:
The application uses the Accept-Language header to determine which language the website should display. The issue is that although the Accept-Language header is not cached, it is still used to specify the response format.
Request 1
GET /images/info/img?country=IN HTTP/1.1
Host: www.vulnerablesite.com
User-Agent: Mozilla/…..
Accept-Language: en-US
Request 2
GET /images/info/img?country=IN HTTP/1.1
Host: www.vulnerablesite.com
User-Agent: Mozilla/…..
Accept-Language: FR
Although the requests have different Accept-Language headers, both will receive a page in English if the first request is cached. The Accept-Language header was used in both requests but not cached, which makes it an unkeyed input. In this instance, attackers can use unkeyed input to serve malicious content to the users.
Identifying and Exploiting Web Cache Poisoning
For the practical demonstration, we will use Portswigger labs.
Web Cache Poisoning with an Unkeyed Header
Unkeyed headers are used by some websites to dynamically create URLs for importing resources, including externally hosted JavaScript files. In this instance, an attacker could alter the URL to link to their own malicious JavaScript code if they changed the relevant header's value to a domain they control.
If the cached response includes the malicious URL, each user requesting a matching cache key will have the attacker's JavaScript file imported and executed in their browser session.
Follow the steps below to execute the attack successfully:
-
Navigate to the home page of the portswigger lab and capture the request using any Proxy (Burp Suite in our case).
-
Observe the GET / request to the home page and send it to the repeater tab.
-
In the repeater tab, add the cache-buster as a query parameter with any random value: ?cb=1234
-
Add the X-Forwarded-Host header with any arbitrary hostname:
X-Forwarded-Host: example.com
-
Send the request and observe that the X-Forwarded-Host header has been used to dynamically generate an absolute URL for importing a JavaScript file stored at /resources/js/tracking.js.
-
Resend the request, and note that the X-Cache: hit header appears in the response. This indicates that the response was retrieved from the cache.
-
Navigate to the exploit server (portswigger), and change the file name and body with the malicious payload.
-
Store the exploit and Navigate back to the repeater tab, remove the cache buster, and add the exploit server ID in X-Forwarded-Host.
-
Send the request and observe that the XSS is triggered.
Web Cache Poisoning with an Unkeyed Cookie Parameter
In this scenario, the request line and the Host header are included in the cache key but not the Cookie header.
Follow the steps below to execute the attack successfully:
-
Navigate to the home page and capture the request using burp suite.
-
Observe the GET / request to the home page and send it to the repeater tab.
-
Note that the application reflects the fehost cookie inside a double-quoted JavaScript object (data) in the response. fehost=prod-cache-01
-
Modify the value of the fehost cookie and note the same value is reflected in the response.
-
Insert the XSS payload in the cookie to execute the XSS attack:
fehost=TEST12"-alert(1)-"TEST12
-
Send the request and observe that the XSS is triggered.
Web Cache Poisoning via an Unkeyed Query String
In some scenarios, it might be possible to insert a malicious payload into a query parameter and cache the response from the server if the query parameter of the request is unkeyed and is getting reflected in the response. Users would subsequently receive the poisoned response if they sent a matching request without a query string.
This method converts reflected XSS into stored XSS because the attack is a typical script injection, and the script is cached in the web cache. While this approach is easily detectable when performed directly, it may avoid detection in much more complex situations.
Follow the steps mentioned below to execute the attack successfully:
-
Navigate to the home page and capture the request using Burpsuite.
-
Observe the GET / request to the home page and send it to the repeater tab.
-
Insert arbitrary query parameters in the request URL. Notice that if you modify the query parameters, you can still get a cache hit. This shows they are not a part of the cache key.
-
Add the origin header as a cache buster and send the request.
-
Observe that your injected parameters are reflected back in the response when the request gets a cache miss.
-
Remove the query parameter and observe that it still be reflected in the cached response.
-
Insert an arbitrary query to escape the reflected string and inject an XSS payload:
GET /?hacker='/><script>alert(1)</script>
-
Repeat the request until the payload is reflected in the response and X-Cache: it is present in the headers.
-
Remove the origin header from the request and replay the request until you have poisoned the cache for other users.
Web Cache Poisoning via an Unkeyed Query Parameter
Some websites exclude the complete query string from the cache key. Some websites, however, exclude query parameters irrelevant to the back-end program, such as those for monitoring or targeted advertising. UTM parameters such as utm_content are excellent targets for testing this vulnerability.
Follow the steps mentioned below to execute the attack successfully:
-
Open the home page and observe the GET / request to the home page and send it to the repeater tab.
-
Add the cache-buster query parameter and utm_content parameter to the request:
?hacker=123&abc=xyz&utm_content=abc123
-
Send the request repeatedly until the request encounters a cache miss. Note that this unkeyed parameter, along with the rest of the query string, is reflected in the response.
-
Send a request with a utm_content parameter that escapes the reflected string and injects an XSS payload:
GET /?utm_content='/><script>alert(1)</script>
-
Send the request to solve the lab.
Parameter Cloaking
Follow the steps below to execute the attack successfully:
-
Open the home page, observe the GET / request to the home page and send it to the repeater tab.
-
Insert the utm_content parameter and send the request.
-
Observe that the parameter is supported and excluded from the cache key.
-
Insert a semicolon (;) to append another parameter to utm_content, the cache treats this as a single parameter. This indicates that the cache key does not include the additional parameter.
Param miner tool can automate this process. It combines advanced diffing logic from Backslash Powered Scanner with a binary search technique to guess up to 65,536 parameter names per request. Parameter names come from a carefully curated built-in wordlist and harvest additional words from all in-scope traffic.
To use Param miner, right-click on a request in Burp and click "Guess (cookies|headers|params)." If you use Burp Suite Pro, identified parameters will be reported as scanner issues.
-
Install the Param miner from the BApp store.
-
After loading the tool, right-click param miner > select the rails param cloaking scan.
-
In Burpsuite Pro, you navigate to the target issues to check the Parameter Cloaking issue as reported.
-
In the HTTP history, observe the GET /js/geolocate.js?callback=setCountryCookie request and send it to the repeater tab.
-
Send the request and observe that every page imports the script /js/geolocate.js, executing the callback function setCountryCookie().
Note that you can control the function's name executed on the returned data by modifying the callback parameter.
-
Insert a semicolon between the second callback parameter and the utm_content parameter; the second callback parameter is not included in the cache key. However, it still replaces the callback function in the response.
Note: Both ampersands (&) and semicolons (;) are considered delimiters by the Ruby on Rails framework. When combined with a cache that does not permit this, the attacker can exploit another quirk to override the value of a keyed parameter in the application logic.
-
To exploit the XSS, send the request again with alert(1) as a callback function.
Web Cache Poisoning via a Fat GET Request
If an application allows "fat GET" requests, which include a body in the request, and the request body is unkeyed and included in the response, it can create an opportunity for cache poisoning.
An attacker could inject a malicious payload in the GET request, and since the request body is not included in the cache key, the response would be cached. If a legitimate user then makes a regular GET request that matches the same cache key, they would receive the poisoned response from the cache.
Follow the steps below to execute the attack successfully:
-
In the Burpsuite, navigate to the HTTP history, observe the GET /js/geolocate.js?callback=setCountryCookie and send it to the repeater tab.
-
Insert a duplicate callback parameter in the request body to control the function’s name in the response.
callback=myFunction
-
To automate this process with param miner, right-click < param miner < select fat GET scan.
-
To execute the XSS, send an alert(1) as the callback function.
-
Open the response in the browser to check the executed XSS>
Cache Key Injection
Keyed components are frequently packaged together in a string to form the cache key. You can craft two requests with the same cache key if the cache doesn't properly escape the delimiters between the components.
Follow the steps below to execute the attack successfully:
-
Navigate to the login panel while capturing the request in the Burpsuite.
-
Observe that the redirect at /login, send it to the repeater tab.
-
Insert the utm_content parameter and send the request.
-
Follow the redirection and observe that the page at /login/ has an import from /js/localize.js. The lang parameter is vulnerable to client-side parameter pollution as it does not encode the value.
Note that the login page refers to a location /js/localize.js that can be exploited by injecting a response header into the Origin request header if the cors parameter is set to 1.
-
Modify the request as follows:
GET /js/localize.js?lang=en?utm_content=z&cors=1&x=1 HTTP/1.1
Origin: x%0d%0aContent-Length:%208%0d%0a%0d%0aalert(1)$$$$
-
Again modify and send the request:
GET /login?lang=en?utm_content=x%26cors=1%26x=1$$Origin=x%250d%250aContent-Length:%208%250d%250a%250d%250aalert(1)$$%23 HTTP/1.1
-
Observe that the /login?lang=en has been poisoned executing the XSS payload.
Since the beginning of this article, we have covered various attack techniques for web cache poisoning and learned how to identify and exploit them. However, it is also necessary to understand how to fix this type of vulnerability. The next section of this article discusses the remediations and best practices developers should follow to fix/prevent web cache poisoning issues.
Remediations:
-
Caching should be turned on for security reasons but only for static answers. This prevents attackers from exploiting caching to get a malicious response from the server.
-
Make sure that only static resources—like *.js, *.css, and *.png files—that are always the same are cached.
-
Eliminate unkeyed inputs from the cache layer, replace them with a cache key, or deactivate unkeyed inputs entirely.
-
Restrict fat GET requests. Be careful that this might be possible by default with some third-party technologies.