Clob-Mate
DOM clobbering + request size denial of service
Description
I heard there's a shortage of Clob-Mate, but you need your hacker fuel. You have to order some, no matter the cost.
Solution
Initial Analysis
This challenge gives us a simple form that when submitted, shows our "order status".
Looking at the source code, we can see that the endpoint that creates the order takes in article
, quantity
, username
, address
and email
, then generates an order_id
based on the base64-encoded value of article+quantity+username+address
.
This base64 value is then used in future URI paths that correspond to our order. This format of creating record IDs is a bit odd - alphanumeric IDs of a fixed length are the commonly-used format for these things, and more interestingly this format allows the user to create arbitrary-length URLs. This would come in handy later.
The app also exposes a /orders/<order_id>/get_user_infos
API that allows us to query the username
, address
and email
information of an order.
The /order/update
endpoint is where we get our flag - the admin needs to send a request that sets the order_status
to "accepted"
.
The admin would visit our order preview, where the inspect_order.html
template is rendered.
DOM Clobbering
Let's take a look at the preview page! Our goal here is make order.user.username
evaluate to "pilvar"
, so that we reach the code path where /order/update
request is sent with order_status=accepted
.
Quite interestingly, the fetch("/order/update")
call is performed again if an exception is raised in the try
block.
Note that none of the variables are declared with the var
or let
keywords, making all of them global variables. Because in HTML the global scope is the window
object, one effect of this is that if any of the HTML elements have their id
set to order
, the global variable order
(window.order
) would refer to that element!
This is known as DOM clobbering, a technique hinted by the challenge name.
Because the bottom of the page contains elements with their id
s set to order
, the original value of order
is a HTMLCollection
object containing these elements.
But since order
is being set in the try
block, this vulnerability can only happen if we trigger an exception at the JSON.parse
line before the order
variable is changed.
At this point we don't yet know how to trigger the exception, but let's first try and see if our hypothesis works. We could test this by adding a throw
statement before order
is changed.
The order
variable is indeed a HTMLCollection
!
Recall that our goal is to set order.user.username
. To control order.user
, we could use the name
attribute that is set on the anchor and paragraph tags.
Right now, our form body looks like this:
which sets the following body:
Now order.user
would return the anchor tag element. Great!
Curiously though, order.user.username
is an empty string, instead of undefined
.
This was strange indeed! The username
property is in fact part of the anchor tag element object's prototype.
It turns out that the anchor tag's username property actually refers to the username part of the href
value (see this). This meant that in order to set order.user.username
to pilvar
, all we had to do was to supply a URL starting with pilvar@
to the href
attribute.
Triggering the Exception
Now comes the tricky part - how do we trigger the exception in the first place?
My first thought was to look for interoperability issues between Flask's JSON response and JavaScript's JSON.parse
. I tried things like weird unicode characters and JSON comments, but nothing worked. One nap later I convinced myself that both Flask and JavaScript are probably spec-compliant when handling JSON, and I was probably not intended to find a JSON parsing 0-day.
If JSON parsing is out of the question, then the only way to cause an exception here is to make the /order/<order_id>/get_user_infos
endpoint return something that is not JSON in the first place! Going back to the /order/create
endpoint, I started to question the weird order_id
format.
We know that the user can create arbitrary-length order IDs, and we need get_user_infos
to somehow fail. Since the /order/<order_id>/preview
URL is 7 bytes shorter than the /order/<order_id>/get_user_infos
one, there is a 7-byte window where preview
would succeed but get_user_infos
will fail due to URL length limits enforced by the web server. This is a known technique that in some cases can be helpful in performing XS-Leaks.
In the case of Waitress, the 431 Request Header Fields Too Large response code is returned.
Using this script to binary search for the longest URL we could get before the error occurs, I got an approximate length for the order_id
to trigger this exploit.
The next step is to take our current payload, and pad any of the fields (except for article
) with enough bytes to get the corresponding order_id
length.
A few moments later the admin visits our preview page and gets the flag!
While this method used the 7-byte difference between the two URLs to calculate the order_id
length, the exploit is actually made much simpler by the fact that the Referer
header is sent on the second request containing the order URL (I only noticed this after the competition).
Because the error is caused by the total length of the request line + headers, the long Referer
header meant that the precision of calculating the required order_id
length was not that important and a large range of lengths would have worked.
Last updated