My first exploit: a site that allows setting any user’s password

I recently found an interesting vulnerability that allowed any user of a particular website to set any other user’s password.

That’s a big one, right?

It was a bit of fun and I thought might make for an interesting article. You’re about to find out.

Disclaimer: I’m a long way from being a security expert and this was my first foray into SQL injection, so please forgive all naivety found below.

Disclosure: I’m not going to disclose the website in question. Not because I’ve reported it to the site owner and am bound by secrecy, but because I’m going to keep the vulnerability for myself. (If you find the site in question, please do me a favour and keep your mouth shut.)

You know how sometimes you open up DevTools for a site and aimlessly peruse their minified code and network requests, and before you know it the sun has set and your cat is cold? Well, I was doing that on the user profile page of a website and noticed that when I ticked ‘receive notifications’ on and off, it sent a network request like:

/api/users?email=no

And I thought: I wonder if they’re doing anything foolish? Maybe I’ll give this SQL injection thing a try?

So I Googled “xkcd little bobby tables” to remind myself how to do SQL injection, cracked my knuckles — which I did not enjoy — and got to work.

In Chrome’s network tab, I copied the request (Copy > Copy as fetch) and pasted the result into a snippet so I could keep replaying the request, which looked something like this:

The rest of this post is about fiddling with that body string — it’s the vehicle for sending instructions to the server.

On a somewhat unrelated note, there’s a new season of Grand Designs out.

OK, first I tried to change my surname by setting some value in a column called lastName — just guessing at a column name:

Nothing interesting happened. Then I tried the same with last_name and finally took a stab at surname and boom: the page updated and my surname changed to ‘testing’.

This was very exciting. I had always assumed that SQL injection was the stuff of textbook legend, and that no one actually had code out in the wild that put user input directly into SQL statements.

(Recently I’ve been approaching many things in life with Sturgeon’s law in mind: “90% of everything is crap”. I’ve come to realise that to assume that all things are done well is to miss many an opportunity. I think it’s this newfound lack of faith in humanity that gave me the confidence to even bother trying all this.)

For the uninitiated, let me explain what the above outcome reveals.

I means that something like this is happening on the server:

I’m pretty sure their server is PHP, but I don’t know PHP so you’re getting JavaScript examples. Also, I don’t actually know anything concrete about the SQL query. I have no idea if the table is called user or users or user_table and it doesn’t matter.

If my user ID is 1234 and I send email=no then the resulting SQL is:

And if I replace no with the exact string no', surname = 'testing then the resulting SQL is valid-but-sneaky:

Remember that I’m sending these requests from a DevTools snippet while on the profile page of a website. So from here on in, you can think of the ‘surname’ field on that page (the actual HTML <input> element) as a little stdout that I can write to by setting a value in the surname column of the database for my user account.

The next thing I wondered was, can I copy data from another column into the surname column?

I have no idea what I’m doing, and don’t know SQL very well, and don’t know what database they’re using, so there’s about 20 minutes of Googling between each of these steps, and another 20 of head scratching as I repeatedly put my ' marks in the wrong place. I’m surprised I didn’t wreck the whole database in the process.

Copying data from one column to another was a bit trickier, because the query I wanted was this (with a guess there was going to be a password column):

Note the lack of quote marks around ‘password’ (I mean in the code, not in this sentence). As you’ll recall, the super-advanced query-builder must have looked like this…

… which means I’d have a stray quote mark if I tried to pass in no', surname = password and the resulting string would not be a valid SQL query. Instead, I needed the string that I injected to become the whole second half of the query, and for everything after it to be ignored. Specifically, I needed to provide my own WHERE, and a ; to end the SQL statement, and a comment # so that everything after it is ignored. Yes, I am explaining this terribly.

The new string I sent was:

The resulting string that will be sent to the database would then be something like:

Note that the database will ignore WHERE id = '1234' since that comes after the comment #. (Seems like not allowing comments in SQL queries would be a good way to protect against sloppy code).

I had hoped to see my plain text password — P@ssword1 — appear in the surname field, but instead I got 00fcdde26dd77af7858a52e3913e6f3330a32b31.

This was disappointing but unsurprising, and I carried on with my plan to copy my password hash into another user’s password column.

Let me clarify for the uninitiated: when you set up an account somewhere, and send your brand new password of ‘P@ssword1’, it’s going to get turned into a hash — like 00fcdde26dd77af7858a52e3913e6f3330a32b31 — and stored in the database. No one can look at that hash and work out what your password is (or so the story goes).

The next time you go to log in, you’ll type ‘Password@1’, the server will hash that again, and check that the hash of the password you just sent matches the hash of the password in the database, thereby confirming that the passwords match, without ever storing the password.

This means that if I want to set someone else’s password to ‘P@ssword1’, I must set the value in the password column for that user to 00fcdde26dd77af7858a52e3913e6f3330a32b31.

Easy peasy.

So I opened up a different browser, set up a new user with a different email address and first checked that I could set data for that user. I updated the body prop to:

I fired that off, refreshed the page of the other user and holy crap it worked! The other user’s surname was now “WOOT!!” (my grandmother’s maiden name. Old granny woot, we called her).

So now I tried to set the password for the other user:

And guess what?!?!?

It didn’t work and I was now locked out of my second account.

Turns out I was doing two things wrong, and both took a few hours to work out. Security experts reading this already know what those two things were, and are probably yelling at the screen right now about this fool writing of his ‘exploits’ that are on page one of “Hacking for Babies”.

Aaaaaanyway, I eventually Googled “password hash” and noticed that many hashes were longer than my 00fcdde26dd77af7858a52e3913e6f3330a32b31, so I wondered if it was getting truncated somewhere. I tried typing a whole bunch of text into the ‘surname’ field on the screen and found that it was limited to 40 characters. (Good on them for having a maxlength attribute on their <input> to match the limitation in the database.)

So, I was looking at only the first 40 characters of what might be a much longer hash. I Googled “sql substring” and was soon pointing this puppy at the server:

I started at 30 to confirm that the first 10 characters overlapped with the last 10 characters of 00fcdde26dd77af7858a52e3913e6f3330a32b31. Or the last 9 overlapped. Or 11.

I think when I die and go to hell, I will be forced to watch all of my off-by-one mistakes in slow-mo for all eternity. Including close-ups of my scrunched up face as I repeatedly realise my incessant ineptitude.

Back on earth: the characters did overlap, and with my substrings combined I now had a 64-character password hash. I once more tried to copy this over to user two:

And guess what?!?!

Well you already guessed what because I said I got two things wrong.

I still couldn’t log in as the other user, but I was nearing the end (information it would have been nice to know at the time).

I eventually Googled “best practices database password” and learned/was reminded about a thing called a ‘salt’.

Using a salt means that when you create a hash for ‘P@ssword1’ for one user, you get a different result to when you do it for a second user (using a different salt). So of course having the same password hash doesn’t work for both users — the salt is different.

This seems clever, but also pretty daft. All the examples just had an extra column in a user table called salt. Wouldn’t that mean I just have to copy across two columns instead of one? Isn’t that like having a second padlock that takes the same key?

I changed my query to try and get the value from a column I hoped was called ‘salt’ into the surname column:

I got more jumbled characters as a surname, a good sign. Again, I used SUBSTRING to get what turned out to be a 64-character salt.

I was all set, I had the hash of my password, and the salt that was used to create it, I just needed to copy those both across to the other user. So I sent my final network request for the evening:

It worked! Hallelujah, praise baby Jesus! I could now log into my second user account with the same password as my own account.

Isn’t that nuts?

There was lots of trial and error here, but when I pick a real target user, I will start by getting their salt and hash, and writing those down somewhere. Then I’ll set their salty hash to mine as described above, log in, then immediately set them back to their original values. I only need their password changed for a split second while the login process happens, so I can almost certainly go undetected.

That’s in theory, of course. I would never do such a thing.

You may be wondering if this is a made up story like how I harvested your passwords and credit cards.

It isn’t. A few small details have been changed to protect the guilty, but otherwise all the steps outlined are as they happened. And of course I have actually disclosed this to them.

But I can’t help but wonder if this was just beginner’s luck. This is literally the first site I’ve ever tried to SQL inject, and they had everything neatly laid out for me like I was taking a hacking for babies exam.

Admittedly the site in question is a small fish, with not many users (34,718), and it’s a paid service, so not much of a target for idle hackers. But still, it stuns me that this is possible.

Anyway, I’m hooked on this whole security thing now. It combines my two favourite things: writing code and misbehaving. So, having Googled “information security salaries Australia” I think I’ve found my next career.

Hey thanks for reading! If you have a site with crappy security, why not let me know in the comments and I’ll, um, go in and fix it for you.

I like web stuff.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store