In continuing our VERT Vuln School series on SQL Injection vulnerabilities, we’re going to take a look at how attackers can leverage this vulnerability to steal and exfilitrate data.
Once we views bob’s account balance page, we notice that there’s another input-field that might be of interest, the GET variable “cc”. A common way to test for SQL injection vulnerabilities is to insert a single-quote into a field and observe the result from the webserver:
Because the Bank of VERT website is configured to display errors, we see an explicit MySQL error message. In this case, injecting a single-quote broke the syntax of the query. Such an error message, if caused by malformed user-input, almost always indicates a SQL injection vulnerability. Let’s inject some syntactically-correct pieces of SQL and observe the behavior of the application:
By injecting 8507692614026575' and '1'='1 we no longer see the error message. Let’s see what happens if we inject 8507692614026575' and '1'='2
The balance has changed, and is now displayed as 0. How and why did this happen? Let’s again take a look at some of the code to understand this. // if acount type is set, get balance from database if(isset($_GET['cc'])) { // show list of accounts $query = "SELECT amount from balance where credit_card_number='" .$_GET['cc']. "'"; // send query $res = mysql_query($query) or die (mysql_error()); $amount = 0; // if $res is false, query failed if($res) { // retrieve result set $row = mysql_fetch_assoc($res); if(isset($row['amount'])) { $amount = $row['amount']; } } } This snippet of code shows how the web application uses the credit card number to return the balance associated with it. As mentioned previously, the important thing to note is how the query is crafted: $query = "SELECT amount from balance where credit_card_number='" .$_GET['cc']. "'"; Just as before, we see that user input, in this case the GET variable cc ($_GET['cc']) is being inserted into the query without any sort of sanitization. This is bad. Let’s take a look at how the balance is changed by injecting our syntactically-correct snippets of SQL: $query = "SELECT amount from balance where credit_card_number='8507692614026575' and '1'='1'"; We see that by injecting the first single-quote (following the account number), we’re effectively breaking out of the string and adding an additional condition to the where-clause. In this case, our condition is and '1'='1 (note that the last single-quote is left out since it is already being supplied by the hard-coded query in the code). After the injection, because the overall query is now syntactically-correct and is overall true, the DBMS returns all results where the credit_card_number = 8507692614026575. Conversely, when our condition is changed to and '1'='2 $query = "SELECT amount from balance where credit_card_number='8507692614026575' and '1'='2'"; We’ve effectively made it such that the overall true/false-ness of the query to false. How? Even though the credit card number is correct, by adding the condition and '1'='2 the query will never result to true, because 1 does not equal 2. Therefore, the query, when supplied to the DBMS, will not return any results. Because of the way our code is designed, $amount is instantiated as 0, and is therefore displayed as the amount when the query fails to return any results. Now that we have a clear understanding of how certain injected queries affect a vulnerable input, let’s move on to exfilitrating data. This next part requires some basic knowledge of SQL and will gloss over a few details, but should be rather straightforward to follow. Assume that up to this point, the attacker did not have access to the code, and was able to infer the structure of the backend SQL-queries based on how the web application responds to certain injected snippets of SQL. The first step is to find out how many columns there are in the current table. This can be achieved by a simple trick:
The “order-by” trick is commonly used by attackers to infer the number of values returned by the back-end SELECT statement. This is important to know, because when we issue our UNION SELECT statement (to select other data in other tables), we need to correctly align with the number of columns returned by the first SQL query. By incrementing and decrementing the column number in the order-by clause, the attacker is able to determine the number of columns whenever the order-by number no longer generates an error message:
We see that order by 1 -- - does not return an error. This means that there is 1 column returned by the existing SELECT statement. Note that the -- - portion of the injection is a SQL comment; it effectively comments out the rest of the line so that we no longer have to ensure the end of the query is syntactically-correct. Now that we know number of columns returned by the current SELECT statement, we’re ready to perform our UNION SELECT:
Wait, what? We see that we do not see the result of the UNION SELECT, even though we have the number of columns correctly aligned. Recall that the web application is displaying the first column from the first row of the returned results. In order to make the first column not return any results, let’s get rid of the account number and replace it with a value that likely does not exist (such as x) to effectively make the results of our UNION SELECT be the first row of the returned results:
That’s much better. We see that the 1 from UNION SELECT 1 is being reflected onto the page. We are effectively viewing the output of our injected SQL query. From here, it is homestretch in terms of stealing data. Let’s see what else we can find out:
@@hostname is a MySQL shorthand for returning the system’s hostname. In this case, it’s ubuntu. Let’s find out the name of the database we’re connected to:
The name of the database we’re connected to is bankofvert. Using this information, let’s see what tables exist in this database:
We see that there are three tables, account, balance, and users. Notice that we’re querying the information_schema database, which is essentially a database which contains metadata of all the databases and tables in the current MySQL installation. Table users and account sound like they would contain interesting data. However, before we can view the contents, we need the names of the columns in each table:
The users table contains column names username and password. Let’s look at the column names for account:
account contains credit card numbers, CVV values, and social security numbers. Now that we know the table names and column names, let’s put the final nail in the coffin and exfilitrate data from both of these tables:
Using MySQL’s group_concat(), we aggregate the rows of the results into a single string (since the web application is only displaying a single row of the result-set). We can see the 4 users of the Bank of VERT, and their corresponding passwords. Let’s get each of their private financial and personal information from account:
By examining SQL injection from the perspective of an attacker, we can get a better understanding of just how serious these vulnerabilities can be. This concludes our first look into the classic SQL injection vulnerability. In the next portion of this series, we will learn about blind SQL injection, as well as leveraging SQL injection vulnerabilities to gain system-level access. For those that would like to try out SQL injection first hand at home (and not in your production environments), the source code for the Bank of VERT is available here. Title image courtesy of ShutterStock
Mastering Security Configuration Management
Master Security Configuration Management with Tripwire's guide on best practices. This resource explores SCM's role in modern cybersecurity, reducing the attack surface, and achieving compliance with regulations. Gain practical insights for using SCM effectively in various environments.