Trustwave Corporation

05/30/2023 | News release | Distributed by Public on 05/30/2023 13:12

Hunting For Password Reset Tokens By Spraying And Using HTTP Pipelining

As is tradition with my blog posts, let's start off a definition of what HTTP pipelining is all about.

"HTTP pipelining is a feature of HTTP/1.1 which allows multiple HTTP requests to be sent over a single TCP connection without waiting for the corresponding responses. HTTP/1.1 requires servers to respond to pipelined requests correctly, with non-pipelined but valid responses even if [the] server does not support HTTP pipelining. Despite this requirement, many legacy HTTP/1.1 servers do not support pipelining correctly, forcing most HTTP clients to not use HTTP pipelining." (taken from https://en.wikipedia.org/wiki/HTTP_pipelining)

And from an F5 technical article because I love that they used the term "shoving" to describe this: "In technical terms, HTTP pipelining is initiated by the browser by opening a connection to the server and then sending multiple requests to the server without waiting for a response. Once the requests are all sent then the browser starts listening for responses. The reason this is considered an acceleration technique is that by shoving all the requests at the server at once you essentially save the RTT (Round Trip Time) on the connection waiting for a response after each request is sent." (taken from https://community.f5.com/t5/technical-articles/http-pipelining-a-security-risk-without-real-performance/ta-p/286621)

Are you all listening at the back? Ok, let me setup the scene here and then we'll invite our pipelining friend back to the party when needed.

Moving onto password reset tokens. You know the score on these. You've forgotten your password, so you select "forgot password" from an application, likely put in your e-mail address (or username, etc. etc.) and it (a good implementation) will e-mail a unique "random" time limited one-time password link (a token) to the registered e-mail address on file. You need to imagine me doing air quotes with my fingers on the "random" bit there. (Work with me.) As the owner of this e-mail address, you check your e-mail, follow the link (which contains the token) and you get to set a new password, without needing to know the previous. You then log in and get on with your day. Note how I said a good implementation will do this in the above. There are many ways this can be done badly.

Bad password reset mechanisms will invoke a server side action straight away to issue the account with a new (temporary) password, sent over cleartext e-mail to the user's registered e-mail - using the new password (in effect) as the token, allowing them to login and then the server forces them to change it. This is bad times and leads to a vulnerability we call "Password Reset DoS" down in pentester partyland. You have to assume that bad people are requesting password resets for other users and instead go down the 'issue password reset links with a token' route instead. Assume everyone is bad until the link (with a valid token) is followed and it is only at this point that they are verified as being a legit good person, who owns that e-mail address.

Other bad password token reset mechanisms will issue a password reset link with a predictable token value, a static value, one which does not expire and one which can be used again and again. The very very very bad reset password links will also have a user-defined username/id parameter present with no kind of Hash-based Message Authentication Code (HMAC) integrity check present or lacking any server side validation, opening the doors to a whole lot of pain (and compromise) - and that's another blog post.

I'm going to setup a scenario whereby the password reset mechanism is not the worst, on the face of it, it looks pretty good. However, the more we dig into it we can find some minor issues and if we bring our friend HTTP pipelining back to the hacker party, we can combine them altogether for arbitrary account take overs.

First rule of pentester club is that in order to break anything you first need to understand how it works. It is then you're able to formulate an attack plan after visualising it, which helps a lot from our often black box perspective. As a pentester from this perspective, we don't get all the details right and we have to make assumptions, but that's ok. "Give me six hours to chop down a tree and I will spend the first four sharpening the axe", which I believe Abraham Lincoln is quoted as saying.

Imagine the web application before you now, the bright glow from the monitor showing the forgotten password page lighting up your room, asking for an e-mail address. You've got an account already, you just want to learn how this thing rolls, so you put in your e-mail address and click that forgot password button.

*YOU'VE GOT MAIL* (cue Outlook new e-mail ding sound… is it even a ding?)

"Please visit https://127.0.0.1/resetpassword/36525646 to reset your password."

You put your e-mail address in again, because you're a pentester, because this is life. I joke… but you do it again because you need to compare against your baseline for deviations, much like a scientist does with their control group samples.

*DING*

"Please visit https://127.0.0.1/resetpassword/1657177506 to reset your password."

We do it again and again, and again, because that's just how we roll and because we're all about the bigger sample size for better analysis. 50 will do for now, just so we can eyeball.

If we extract the token from all 50, this a list of them in the order in which we received them.

365256460

1657177506

1986055580

741532205

527890360

442059229

987251296

717599616

29792672

825171448

1798342323

498299249

1320462756

800784560

107292283

1986043443

537545646

910649865

1767536999

296777708

929634488

867032357

980003670

1313317313

530880191

1300024965

464686643

1181592875

693890782

829894330

1718925158

1468835528

837921297

1336689766

362190361

808466195

206029262

433891936

1158511253

1455609935

1893679034

799600364

1521862935

863837241

966780233

257827295

1272555532

494910317

197257454

118248652

If we bring "sort -n" out of the tool box, this is those 50 tokens arranged by size, smallest to largest.

29792672

107292283

118248652

197257454

206029262

257827295

296777708

362190361

365256460

433891936

442059229

464686643

494910317

498299249

527890360

530880191

537545646

693890782

717599616

741532205

799600364

800784560

808466195

825171448

829894330

837921297

863837241

867032357

910649865

929634488

966780233

980003670

987251296

1158511253

1181592875

1272555532

1300024965

1313317313

1320462756

1336689766

1455609935

1468835528

1521862935

1657177506

1718925158

1767536999

1798342323

1893679034

1986043443

1986055580

The smallest number is 29792672 (we'll call it 29 million something). The largest is 1986055580 (we'll call it 1 billion something). Now that's quite a range I'm sure you will agree.

Now putting our pentester hat/hoody/mask back on, we can knock something up locally to represent what we think is happening in old server land. We don't know the inner thing happening from our black box perspective, but based on our observations of the output token (the range differences), the following simple code with respect of srand() will give us the same outcome, using min and max ranges as 10000000 and 2000000000.

[Link]

Figure 1. Code to generate random values between 10000000 and 2000000000 to simulate the server logic.

Now we may be wrong (and we probably are) on what exactly is going on with respect of initiating the seed value - they may be using time, they may be using a static value, they may be using something else. However, like I said earlier, that's ok, we're just looking to see if we've got something viable to attack here.

Remember earlier when I said that a few small/minor weaknesses with this password reset mechanism will allow this attack? Well ponder this… in this application, each time we ask for a new reset password token link from the server, the old one still remains valid. (You're supposed to gasp at this!) That's point number one. Point number two is that each of these tokens are valid for 10 hours. (Shock horror, audience gasps again!)

It is at this point I take my inspiration from another technique, heap spraying, cue Wikipedia: "In computer security, heap spraying is a technique used in exploits to facilitate arbitrary code execution. The part of the source code of an exploit that implements this technique is called a heap spray. In general, code that sprays the heap attempts to put a certain sequence of bytes at a predetermined location in the memory of a target process by having it allocate (large) blocks on the process's heap and fill the bytes in these blocks with the right values." (taken from https://en.wikipedia.org/wiki/Heap_spraying)

I am able to use the concept of heap spraying against this password reset mechanism. This, together with the speed/magic of HTTP pipelining makes this attack possible, way within the 10 hour time window before a token expires.

What would I do next? I'm glad you asked. This is what I would do.

I would generate locally all values within those ranges using something like C/seq/Python and output them to a file. Based on the ranges in this example, this will take a longggggg while and take up lots of disk space - tea and biscuits (possibly a crumpet) time.

I'd then fire up Burp and use normal Intruder to continuously request password resets for our proof of concept (victim) account. This is something which will need to be throttled down and limited because this has the potential for DoS as lots of server side things are happening here involving the server's mail server and your local mail server. If this were a real attack then the victim would be getting spammed - a quiet attack this is not! It doesn't matter too much the speed of the password reset requests (as in it doesn't need to be the speed of light), what matters is that it gets started ahead of time as to fill our imaginary 'heap' up server side (Note, not an actual memory heap, I'm just using the same concept). Think of it as the more lottery tickets you buy then the more likely you are to win - even if this is a tiny tiny tiny percentage increase, it's still an increase in the right direction!

Now, for the big guns. We would bust something out like Burp's Turbo Intruder, feed in our generated file from earlier with all the generated numbers in the ranges, tweak a few things, and fire them torpedoes!

Spoiler alert. I actually made the above story into a reality, and set this up locally.

Pull up a seat, sit back and relax.

To represent creating password reset link tokens on the server side, I used the code shown earlier to seed 50 token values (randomly, between 10 million and 2 billion) and saved them to a local file, 50.txt.

[Link]

Figure 2. Last 10 lines of the 50 token values randomly generated earlier (full file contains 50 tokens).

I then create files (named after each of the numbers) without any file extension, inside the web server's web root, under a "/resetpassword/" directory, e.g., "1313317317" becomes the file "/resetpassword/1313317317". This is achieved with a bash 'for' loop.

[Link]

Figure 3. Creating files with numbers as the actual filenames to represent valid tokens to live on the web server.

The web server (Apache) will be serving files from here. When a file (representing a token) is correctly requested it would return a "HTTP 200 OK" message else this would be a "HTTP 404 Not Found". This recreates a valid password reset token link server side and a not so valid one. This represents me having used the heap spray concept/technique (to request a password reset link with a token being created server side), but only 50 times.

I then created a big (and I mean bigggg) file with ranges from 10 million to 99 million with this Python script. (Note, the highest number this will generate will actually be "98999999" based on this code due to the loop starting at "0" because computers and arrays, but as this is a proof of concept this is fine. We'd be very unlucky if the token was "99000000" and we could tweak the 'for' loop to go up to "99000001" if we wanted to accommodate, but for illustrative purposes, this is fine, just something I wanted to make you aware of!)

[Link]

Figure 4. Code to create our list of tokens to hunt for.

As this is a proof of concept I also picked this smaller range to make it more manageable. In reality you'd probably still do this approach and increment/generate the next ranges in segments (up to 2 billion) accordingly if you didn't get lucky. I ran the program and directed the output into a text file, numbers_to_hunt_for.txt.

After a couple of minutes we have our file, with all values from 10 million up to just one shy of 99 million.

[Link]

Figure 5. Last 10 lines of our list of tokens file to hunt for (full file contains 89 million tokens).

We then fire up Apache to host the web server, servicing requests to /requestpassword/.

We then open up Burp and configure Turbo Intruder, feeding in our numbers_to_hunt_for.txt file. I've not shown this because I don't want this blog post to become a complete step-by-step guide/tutorial, this is not the intention of this post!

Testing this locally, without taking into account network latency, I had in the region of 10,500 requests per second from Turbo Intruder!

[Link]

Figure 6. Turbo Intruder busting out ~10,500 requests per second!

Normally we wouldn't see it from this perspective, but if we tail -f the web server logs while this attack is going on we'll be spammed (scrolled out!) by tons of requests with HTTP 404 being returned. If you recall back, the 404's is the server telling the attacker the token supplied is invalid.

[Link]

Figure 7. View from the web server of invalid token requests, scrolling past at light speed.

Around 30 minutes later, the needle in the haystack is found and we are rewarded with a HTTP 200 being returned in Burp's window, for token "29792672" ("/resetpassword/29792672").

[Link]

Figure 8. Turbo Intruder finding a valid token of "29792672" with a HTTP 200 OK response.

This is how it would look on the web server, which we wouldn't see from the attacker perspective, but I've added it in to give you the 360 view - because I'm all about the views.

[Link]

Figure 9. View from the web server of invalid tokens (HTTP 404) followed by a valid token ("29792672") at the bottom (HTTP 200).

In the real world, we'd then connect using this token and set a new password for the user. It is worth noting that in the real world (a real application!), there may be some other active reset password tokens already 'in the wild' triggered by actual users which you may stumble upon using this method.

Beautiful, isn't it? I speak with my pentester hat firmly on of course.

With the defence hat on, how does this get fixed? Reset tokens should be random and unique - there are plenty of crypto articles out there about how to seed and do this, so I won't repeat. Lots of frameworks have specifics you can and should use to do things securely. The great fail in this example was that when a new token was generated that the old one wasn't nuked. This allowed us to do our spraying thing. If we had been hunting for just one valid token it would have been possible but taken a while longer. This brings me onto the last point, token expiry. The token expiry time also combined for this to happen. This expiry should be reduced down to a much much shorter time, something like an hour would be good - it all depends on the criticality of the application and your threat model of course - you may want this value to be 5 minutes!

I wanted this blog post to be about using spotting patterns in things, pairing together (and attacking) lots of minor weaknesses, applying techniques from other places (aka spraying) and using (and abusing) the super power that is HTTP pipelining to achieve great things.

As always, thanks for reading! Happy hunting.