Web cache poisoning to exploit a DOM vulnerability via a cache with strict cacheability criteria - writeup

Closing up my advanced port swigger academy topics, i fell in love with web cache poisoning novel techniques by albinowax (James Kettle) the one and only.

This lab in particular just caught my attention, it was extremely fun and challenging, so I just had to do a write up on it - maybe i get myself to do these kind of writings more often.


Lets get going

First we see a logic that does some kind of geolocation simulation.

We see this .js provides a function declaration of initGeoLocate and takes jsonUrl as a param. At the end we see it changes DOM via innerHTML property. We see this taking place here:

So lets see where does this jsonUrl param gets passed from.

Okay, so upon loading / or any other page, initGeoLocate gets called and as param (that jsonUrl) gets loaded with ('//' + data.host + '/resources/json/geolocate.json'). Okay we are on a good path... We can't do much with regular geolocate.json as it is hardcoded, but we may be able to play with that data object.. Lets try to find its declaration.

Okay, so at the beginning of a response we see a json object literal of data. It has two properties: host and path. We are interested in host, as that is the one passed to initGeoLocate.

Upon some manual trial and error, running paramminer, it returned X-Forwarded-Host.

Adding that X-Forwarded-Host: test.me inside request, we see it gets reflected inside response, changing our wanted host property of data object.

So, by sending this request we can essentially control what gets passed to initGeoLocate, that is what .json gets evaluated and displayed in our response in "* shipping in <our point of control>"

But we need a way of distributing this to more users, this is where cache poisoning kicks in hard and in so many possible ways.

If we take a look at any response really, we can see if we've hit or missed cache (that is if we got response from cache or from a server itself), cached response time to live, and how old is this cached response we got back.

By knowing this, and knowing that X-Forwarded-Host is an unkeyed header - that is, request containing it will not get calculated into a cache key. So essentially if we hit a timing when this regular response for request at " / " path is about to expire, and we manage to get our response containing "host":"src=//exploitserver..." cached, anyone requesting " / " will get response that we got when we sent it with X-Forwarded-Host header included.

Lets make a quick poc.

Lets serve this json that has a "hi" value for "country" property that will get loaded into DOM later on. We have to serve it at /resources/json/geolocate.json as that path is hardcoded into javascript that passes it as argument.

We've sent our malicious request but only with /?bust=1 query (cache buster) as query is usually keyed input so it will get calculated into cache key, meaning we are now essentially poisoning(caching) only responses meant for ?bust=1 query so we don't poison or break things over at " / " or anywhere where real traffic is going on.

We see that we got a "miss" and age of 0, meaning we've got response directly from server, if we repeat this request, we would have got a hit and age: 1, meaning this got cached successfully.

But here, loading at ?bust=1, we got a CORS kicking in, so we have to allow this origin over at our exploit server.

Cool, we got our "Free shipping to hi" with cache poisoned. Now we just have to craft a XSS payload, load it into that json value instead of hi, and poison " / " path.

Had much fun playing with this on my break from preparing for CISSP (soo boring). Hopefully, you give some deserved love to cache poisoning after going through this post.

Huge thanks to James, Gareth, Zakhar and everyone contributing (contributing is a small word for what they've given) to security research.

Cheers,
Stay safe,
bigfella