| Field | Detail |
|---|---|
| Platform | PortSwigger Web Security Academy |
| Type | Web Cache Poisoning — Parameter Cloaking, Inconsistent Parsing |
| Difficulty | Practitioner |
| Objective | Use the parameter cloaking technique to poison the cache with a response that executes alert(1) in the victim's browser |
| Note | A user regularly visits the home page using Chrome |
Parameter Cloaking¶
Intercepting / showed the usual response plus a script import worth noting:
GET / HTTP/2
Host: 0ad500060344d5c4808503e80070009a.web-security-academy.net
Cookie: country=[object Object]
<script type="text/javascript" src="/js/geolocate.js?callback=setCountryCookie"></script>
That explains the country=[object Object] cookie — there's a JSONP-style request being made. Checking /js/geolocate.js?callback=setCountryCookie in HTTP history:
const setCountryCookie = (country) => { document.cookie = 'country=' + country; };
const setLangCookie = (lang) => { document.cookie = 'lang=' + lang; };
setCountryCookie({"country":"United Kingdom"});
Classic JSONP — the callback query parameter directly names the function invoked with the geolocation data. In Repeater, changing callback to an arbitrary value confirmed the response follows it:
GET /js/geolocate.js?callback=Teto HTTP/2
Teto({"country":"United Kingdom"});
The response also carries Cache-Control: max-age=35 — it's cacheable. JSONP callback parameters are a high-value injection point: whatever string lands in callback gets executed as a function call verbatim, so controlling it is straightforward code execution rather than just data reflection. The challenge is controlling what callback evaluates to at the backend without changing what the cache sees in the request, so the cache stays keyed on the legitimate-looking callback=setCountryCookie while the backend executes something different.
I ran Param Miner's "Guess query params" to find an unkeyed parameter:
utm_content came up again as ignored by the cache key. Confirmed — requesting / and /?utm_content=teto both served the same cached / response.
The cloaking technique: smuggle a second callback parameter inside the unkeyed utm_content value, using ; as a separator. The cache sees callback=setCountryCookie (since utm_content and everything appended to it is unkeyed), but the backend parses ; as an additional parameter delimiter and resolves duplicate callback values by taking the last one — so the smuggled value wins at the backend while the cache stays oblivious to its presence. This is a step up from plain unkeyed-parameter poisoning: it's not enough that utm_content is unkeyed — the exploit depends on the backend and the cache parsing the same request differently.
GET /js/geolocate.js?callback=setCountryCookie&utm_content=teto;callback=miku HTTP/2
miku({"country":"United Kingdom"});
The smuggled callback=miku won at the backend. Swapping it for the actual payload:
GET /js/geolocate.js?callback=setCountryCookie&utm_content=teto;callback=alert(1) HTTP/2
After a few attempts to get the timing right, the alert fired and the lab was solved.
Lab solved 9.9