DVWA: Weak Session IDs – Impossible difficulty Part I


DVWA stands for Damn Vulnerable Web Application, and it certainly lives up to its name.

It’s intended for beginners to the field of hacking – which definitely describes me – and includes a list of challenges commonly seen in real hacking engagements like SQL Injection, Cross-Site Scripting and File Inclusion/File Upload vulnerabilities, etc.


How you proceed through the challenges is up to you, but I decided to just iterate through the list provided. All of them were fun, especially SQL injection, where I found that working with SQL daily helps to understand exactly what’s going on under the hood.

DVWA is awesome!

One challenge stuck out: Weak Session IDs. It stuck out because it seemed too easy, and at this point I should mention that the authors specifically state there are vulnerabilities in DVWA which are not documented.

DVWA contains 4 different levels of difficulty for every kind of vulnerability it showcases: Low, Medium, High and Impossible. Generally, Impossible difficulty level contains code which is relatively secure, that you might expect to find in a real-world environment on a decently secured Web Application. As such, in many cases it means that unless you come up with a completely new attack method or use a 0-day, you are pretty much not going to break it.

Back to Weak Session IDs. If you Google the challenge, you will find a large amount of blog posts similar to this one but with one key difference – I have yet to find anyone who reported having broken Impossible difficulty level or even claimed that it could be broken. It seems that in every case, they assumed that it really was impossible – a couple of blogs even said as much.

The blog just ends without trying to attack Impossible difficulty

I will now explain how I broke Impossible difficulty “Weak Session IDs”. Code will follow soon on Github, as well as a Part 2 to this article which will be much more involved ๐Ÿ˜‰

The Weak Session IDs task is as follows: Navigate to the page, click the button and you will be issued a session cookie. You should grab a couple of these, then analyse the content of the cookie. Then, try to infer what the next cookie will be, and use this knowledge to hijack someone else’s session. Of course, as DVWA is running only on your PC, inaccessible to the Internet at large, the next user is still you – so you test this by opening an incognito tab and (with a tool of your choosing like Burpsuite, OWASP Zap or a script you wrote) you request that page and supply the predicted cookie. In the background, you wrote a script to simulate another user, waiting some random amount of time and then clicking the button to generate their cookie – which you did not receive. You are trying to hijack their session by predicting the next cookie and have the site recognise you as that user. If you are successful, you will see the full page, and not be redirected to the login page.

Weak Session IDs challenge

Spoiler: I’m about to tell you how the first 3 levels of difficulty work. You should give DVWA a try before reading on, it’s great fun and will build your confidence as a hacker quite quickly, because even in a controlled test environment, you are still hacking.

Difficulty: Easy
The cookie is just an integer counting how many cookies you have been issued. So, your first cookie will be 1, then after another click on the button it will be 2, then 3 and so on.

Easy

Difficulty: Medium
The cookie is just a Unix timestamp like 1629591783 – if you’re unfamiliar, a Unix timestamp is the number of seconds elapsed since 01 January 1970 at 00:00 on the clock. This one is just as simple as Low difficulty, but it requires you to think a little bit about how to attack and hijack that session in the background – once you have tried a timestamp that’s in the past and it failed, it obviously won’t start working later, so you end up trying the current second (represented as a Unix timestamp) continuously until your background script decides to generate the next cookie, and suddenly you have logged in as that user!

Medium

Difficulty: High
This is still easy – it’s just an MD5 hash of the same incrementing integer you found on Low difficulty. Cracking this one requires you to (probably) crack the hash using John The Ripper, hashcat or the like. Again, get yourself 5-10 cookies and analyse them (by decrypting the MD5 hash) and you will see they are incrementing integers. Personally I think this one would have been much harder if it was an MD5 hashed Unix timestamp – once you figure out the structure, you just have to hash the integers 1-100 and try them round-robin until your background script gets a cookie in that list, then you try it again and boom – you’re logged in as the other user! Only on that page, though. For the rest of the site, DVWA requires you to be smarter to impersonate another user and in fact the cookie we just cracked is only valid for that page.

High

Notes:
The above seemed quite easy to me, especially in comparison to the other challenges which took a bit of thought. The hint given by DVWA authors indeed tempts you to break Impossible difficulty, and that was all I needed.

Impossible hint… “Feel free to try”

Difficulty: Impossible
First of all I want to try as much as I can before I cheat – that’s the best way to learn. DVWA lets you look at the source code that you are dealing with, but lets you know (by calling it a Spoiler) that this is kind of cheating. You would not be able to view PHP source code on a real, live server and so you should only look once you have exhausted all of your reasonable attack approaches. In the case of Impossible difficulty, all I could do was extract some cookie values, then run hash-identifier on them (included by default in Kali Linux – Windows users might do well to use a website or install this tool manually). This told me it was most likely a SHA-1 hash, so I fired up hashcat and guessed that the content before hashing might be either the incrementing integer from Low difficulty, or a Unix timestamp from Medium difficulty. So, I ran hashcat on the digits 0-100 using SHA-1, and also on the entire space of numbers with 10 digits, because a Unix timestamp has had 10 digits since Sunday, September 9th 2001 at 01:46:39 and will continue to have 10 digits until Saturday, November 20th 2286 at 17:46:39. No dice. I didn’t crack any values, and so I decided to cheat.

I looked at the source code of the challenge, because after all, DVWA is for practice – I’ve never really done this before, so I need to be able to cheat, as long as I cheat honestly and really learn how things work in the process. Below is the code we are trying to beat:

Impossible

When I saw this, a couple of things jumped out at me: first, the static string at the end. This string is in ALL cookies – it never changes. What this means is, although it lengthens the cookie cleartext by 10, thereby making brute-forcing a cookie much more expensive, once you figure it out it can be provided to hashcat and requires 0 guesses. An “all eggs in one basket” kind of approach, and frankly, if you really wanted to, you could pay for some online compute and get a cluster from AWS or DeepOcean to crack two cookies by brute-force, and recognise that it’s probably static. So, in terms of making cracking actually impossible, this is a bad choice.

The second thing is that we are indeed using the trusty Unix timestamp – this is good, because it means the cookie’s content relies on the time when it was generated. The simple incrementing integer from Low difficulty does not have this property and is very much weaker as a result – if I ever figure out (by trivial brute-force) how many times you have been issued a cookie, it is very easy to keep incrementing and testing, thus maintaining access by consistently tracking your cookie-count. A timestamp is better, but still subject to attack: consider a site where every user has a profile page, and on their page a status of either “Online” or something like “Last online x time ago”. I can easily refresh your profile page (via a script) every 60 seconds and log when your status changed from “Last online 18 hours ago” to “Online”, and I know that this changed in the past 60 seconds, so I only have 60 timestamps to try, probably less. That is not a lot of brute-force attempts! Lacking a profile page with a “last online” status, I can still use any other indicator of activity like a post, share, comment, like, change to the profile picture/information etc. As a last resort, on many sites I could still see when you created your account, and even a few million seconds/Unix timestamps really is not that hard for a powerful computer to iterate through. However, it is more likely to set off some IDS alarms and generally be detected.

So, as we can see, these two things (Unix timestamp, then the static string “Impossible”) are appended to the output of a function in PHP called mt_rand(). As it turns out, here the DVWA authors are offering up a problem which saw a lot of fanfare about a decade ago – PHP’s mt_rand() function can indeed be cracked, and there are many tools and whitepapers out there to help with this. It is not new, and this is the final confirmation – they really do want you to hack Impossible difficulty!

Mersenne Twister
As I said, there’s a lot of material out there about cracking PHP’s implementation of Mersenne Twister. It’s a PRNG, meaning Psuedo-Random Number Generator. The docs themselves tell you it’s not cryptographically secure and should not be used for anything related to security – like Session Cookies ๐Ÿ˜‰ One of the reasons is that it’s deterministic – meaning if you give it the same seed (starting) value, it will always produce the same output stream. What’s more, after 624 outputs, mt_rand will reseed itself based on the last seed, again in a deterministic way. This means that if you crack the seed at any point, you know all of the future values – forever! This is true until the PHP instance is restarted, like in the case of a server reboot, but we’ll get to that later.

If you want to learn how to hack, bookmark this tab and then close it: come back once you’ve tried this yourself!

You’ll quickly find via researching PHP’s mt_rand that for the most popular tool, PHP MT Seed, you need to know whether you have the first output, the second output or the nth output. Ideally, you know you have the first output, and the tool will tell you all possible seed values that produce that number as the first output. It does this via brute-forcing mt_rand with every possible seed, then getting some outputs. If you know you have the fifth output, you can tell it to search for seeds where that number is the fifth output but the search is much slower and once you start guessing that your output might be the tenth output, the program starts to get a LOT slower. After about 12-15 skips, it becomes painfully slow and you should not continue with this approach, because brute forcing does not scale well.

I tried to attack the problem for a few days manually: copy-pasting all my cookies out of Burpsuite along with the Date header of the HTTP packet (to convert to a Unix timestamp later) into a text file and then running php_mt_seed by hand. I got frustrated because I couldn’t get the underlying attack to work – making sure I was dealing with the first output but still guessing I may have missed up to 10 anyway. I spent some time using the File Upload and File Inclusion vulnerabilities I already conquered to run custom PHP scripts to confirm or deny certain ideas about how things worked behind the scenes. Still, I couldn’t successfully predict the next cookie value, no matter how careful I was not to trigger mt_rand unknowingly, and restarting the PHP instance every run to make sure I got the first mt_rand output. I was about to give up and move to the next vulnerability type, accepting that maybe it WAS impossible to break this, the way it was implemented.

Then, two things happened. First, after some conclusive testing, I guessed that PHP was just abandoning every seeded mt_rand instance after a request was served. Thus, it was pointless to crack the seed. I then confirmed this was the case by eventually finding this commit, from PHP 7.1.0 – my machine was running 7.4.15, so it had this behaviour. It was exciting to guess such an arcane detail from my testing! I quickly installed PHP 5.6, which according to this post, definitely does not exhibit this behaviour.

The second thing was that I got tired of the manual effort and decided to write a script to automate it before I officially gave up. A few hundred lines of Python later, I had implemented all of my knowledge about the attack and still it failed. So, I delved deeper into how mt_rand is seeded – you can do this manually by calling mt_srand(your_chosen_seed), but if you call mt_rand without doing that first, it will generate a seed by itself using the process id (PID), plus some timestamps, and also involving the output of PHPs LCG PRNG. I thought I might try and crack this LCG thing – the PRNG behind the PRNG – and that led to another discovery.

My PHP instance was running 6 threads. Turns out, 1 process is the root process of PHP, the rest are workers handling requests. If you run webservers you know this – I did not. So, 5 processes were issuing cookies – not just 1.

So, I figured out that I had to grab a stream of cookies, let’s say 10, after restarting the PHP instance (thus reseeding mt_rand, ensuring I get the first outputs). Then I had to transpose that list of 10 cookies into 5 lists of 2 cookies each. I am dealing with 5 seeded mt_rand instances here, so I should now have the first two outputs of each instance. Then, I ran php_mt_seed on the first output of each process, and BOOM – I cracked them all! The subsequent value I received for each process was indeed predicted by manually seeding mt_rand via mt_srand with the predicted seed, and then getting two outputs! Increasing to 25 cookies total, 5 per process, confirmed this: all were the subsequent outputs based on the seed I cracked using php_mt_seed, run with the first output of each process! I could now predict ALL future values used for cookies on that server, until a restart, while guessing the timestamp.

The final step was to achieve the problem statement – I ran a background script, waiting a random amount of time before clicking the button to generate the next mt_rand output within the cookie value. I was trying all the while to submit the next output of mt_rand + the current Unix timestamp + “Impossible” as a SHA-1 hash value. Once it worked, I had finally cracked Weak Session IDs on Impossible difficulty!

But wait. Even though it may be possible to force a restart of a PHP instance on a webserver via various bugs and attacks which I haven’t tried yet, there is still a glaring weakness in this method – I HAVE to have the first ouput of each process to guarantee an easy crack on each seed – and I can only skip up to 10 or so (meaning only 10 other users have received cookies since the last reseed) before php_mt_seed slows to a crawl, and the attack with it. It also assumes that all processes are in sync – that no process gets tied up with a request while the others issue a few cookies each.

I need to be able to jump in ANYWHERE in the output stream, crack it anyway, and then store the state so I can come back any time and catch up with however many cookies have been issued since then, to predict the next cookies. To test this, we will get our random script to click the button a random number of times, then we will start our attack.

To be continued, with some nice hashcat optimisations, server-time synchronisation and much more in Part 2 ๐Ÿ˜‰


One response to “DVWA: Weak Session IDs – Impossible difficulty Part I”

Leave a Reply

Your email address will not be published. Required fields are marked *