Date post: | 15-Jan-2015 |
Category: |
Technology |
Upload: | krzysztof-kotowicz |
View: | 23,397 times |
Download: | 12 times |
Copyright © The OWASP Foundation Permission is granted to copy, distribute and/or modify this document under the terms of the OWASP License.
The OWASP Foundation
OWASP
http://www.owasp.org
SQL injection: complete walkthrough (not only) for PHP developers
Krzysztof Kotowicz
PHP Developer
http://web.eskot.pl
Medycyna Praktyczna
10.03.2010
OWASP 2
Plan
What is SQL injection?
Why is it so dangerous (demo)?
How to defend?
• Prepared statements
• Escaping
• Stored procedures
• Additional methods
Summary
OWASP 3
Discussed databases (RDBMS)
MySQL
Oracle
MS SQL Server
To some extent:
• PostgreSQL
• SQLite
OWASP 4
Discussed PHP projects
PDO – PHP data objects
• Common interface for various RDBMS
Doctrine 1.2
• ORM (Object Relational Mapper) used e.g. in Symfony framework
Propel 1.4
• ORM, like Doctrine
• Used in Symfony
Zend Framework 1.10
• Popular framework MVC for PHP
MDB2 2.4.1 • Database abstraction layer (DBAL)
• Distributed through PEAR
OWASP 5
What is SQL injection?
OWASP 6
SQL injection – short definition
It is a kind of web application attack, where user-supplied input coming from: URL: www.example.com?id=1
Forms: [email protected]
Other elements: e.g. cookie, HTTP headers
is manipulated so that vulnerable application executes SQL commands injected by attacker.
OWASP 7
Example – login form
User logs in without knowing the login nor password
SELECT * FROM users WHERE login = '{$login}' and password_hash = MD5('{$password}') $login = "' or 1=1 -- "; $password = "dowolne"; // zamierzalismy osiagnac to (kod \ dane) SELECT * FROM users WHERE login = '' or 1=1 -- ' and password_hash = MD5('dowolne') // but server interprets it as SELECT * FROM users WHERE login = '' or 1=1 -- ' and password_hash = MD5('anything')
SELECT * FROM users WHERE login = '{$login}' and password_hash = MD5('{$password}') $login = "' or 1=1 -- "; $password = "dowolne"; // you wanted to achieve this (code \ data) SELECT * FROM users WHERE login = '' or 1=1 -- ' and password_hash = MD5('anything')
SELECT * FROM users WHERE login = '{$login}' and password_hash = MD5('{$password}') $login = "' or 1=1 -- "; $password = "anything";
SELECT * FROM users WHERE login = '{$login}' and password_hash = MD5('{$password}')
OWASP 8
Why is it so dangerous?
DEMO
OWASP 9
What are the possible threats?
Unauthorized access to application
Access to whole database / databases on the server
Denial of service
Database modification
Read / write files on server's filesystem
Code execution
OWASP 10
A few facts
Injection vulnerabilities are the 1st on OWASP Top 10 2010 RC
SQLi is responsible for 40–60% cases of data breach [1] [2]
Modern attack techniques are advanced and automated
• Vulnerability is not only in WHERE part
• Sometimes it is enough to break a query
Vulnerabilities are found on a daily basis, even in new applications
OWASP 11
How to defend?
OWASP 12
How to defend against SQL injection?
Source of vulnerability is mixing code with data
Defense methods
Separating code from data
prepared statements
stored procedures
Escaping
SELECT * FROM users WHERE login = 'login'
OWASP 13
How to defend?
Prepared statements
OWASP 14
Prepared statements – how to use?
1. Preparing SQL command (string)
Put placeholders where data should be
2. Send command to server
3. Attach data to command
4. Execute command
5. Fetch results 3, 4, 5 could be repeated...
6. Clear the command
WHERE a = ? ... WHERE a = :col
PREPARE
EXECUTE
OWASP 15
Prepared statements - example
PDO
// prepare command $stmt = $dbh->prepare("INSERT INTO SUMMARIES (name, sum) VALUES (:name, :sum)"); // attach data variables - WITH ITS TYPES! $stmt->bindParam(':name', $name, PDO::PARAM_STR); $stmt->bindParam(':sum', $sum, PDO::PARAM_INT); // bind values $name = 'something'; $value = 1234; // execute command $stmt->execute(); $stmt = null; //free memory
OWASP 16
Prepared statements - advantages
Commands are completely separated from data they operate on
Injection is not possible
Command is compiled only once - potential speedup
$stmt->bindParam(':name', $name, PDO::PARAM_STR); $stmt->bindParam(':sum', $sum, PDO::PARAM_INT); // petla po danych... foreach ($do_bazy as $name => $value) { $stmt->execute(); }
OWASP 17
Prepared statements - caveats
Not all commands may be parametrised
You cannot put parameters everywhere
Just using PS does not enforce using parameters in them
Sometimes they're emulated (it's a good thing!)
-- error SELECT * FROM :table SELECT :function(:column) FROM :view -- not what you expect SELECT * FROM table WHERE :column = 1 SELECT * FROM table GROUP BY :column
OWASP 18
Prepared statements in Doctrine
Uses PDO (emulated for Oracle) and prepared statements
Uses own DQL language instead of SQL
$q = Doctrine_Query::create() ->select('u.id') ->from('User u') ->where('u.login = ?', ‘mylogin'); echo $q->getSqlQuery(); // SELECT u.id AS u__id FROM user u // WHERE (u.login = ?) $users = $q->execute();
OWASP 19
Prepared statements in Doctrine cont.
It can still bite you
Correct this to:
NEVER put input data directly into SQL commands
$q = Doctrine_Query::create() ->update('Account') ->set('amount', 'amount + 200') ->where("id > {$_GET['id']}");
->where("id > ?", (int) $_GET['id']);
OWASP 20
Prepared statements in Propel
Uses PDO, like Doctrine
// through Criteria $c = new Criteria(); $c->add(AuthorPeer::FIRST_NAME, "Karl"); $authors = AuthorPeer::doSelect($c); // through custom SQL (sometimes it's more convenient) $pdo = Propel::getConnection(BookPeer::DATABASE_NAME); $sql = "SELECT * FROM complicated_sql JOIN some_big_join USING something WHERE column = :col)”; $stmt = $pdo->prepare($sql); $stmt->execute(array('col' => 'Bye bye SQLi!');
OWASP 21
Prepared statements in Zend Framework
PDO (+ mysqli + oci8 + sqlsrv)
// prepare + execute $stmt = $db->prepare('INSERT INTO server (key, value) VALUES (:key,:value)'); $stmt->bindParam('key', $k); $stmt->bindParam('value', $v); foreach ($_SERVER as $k => $v) $stmt->execute(); // prepare + execute in one step $stmt = $db->query('SELECT * FROM bugs WHERE reported_by = ? AND bug_status = ?', array('goofy', 'FIXED')); while ($row = $stmt->fetch()) echo $row['bug_description'];
OWASP 22
Prepared statements in MDB2
Based on different database drivers (mysql, oci8, mssql, ...)
Emulates PS, if database doesn't support them
$types = array('integer', 'text', 'text'); $stmt = $mdb2->prepare('INSERT INTO numbers VALUES (:id, :name, :lang)', $types); $data = array('id' => 1, 'name' => 'one', 'lang' => 'en'); $affectedRows = $stmt->execute($data); $stmt->free();
OWASP 23
Prepared statements - summary
They offer very good protection (if used properly)
Easy to use, small changes in code
Good support in frameworks
They have their limits
Sometimes they have to be used with other defense methods
OWASP 24
How to defend?
Escaping data
OWASP 25
Escaping – how does it work?
Data and commands are still kept in a single variable, but we try to separate them inline
Numbers
• Cast to (int) / (float) – don't use is_numeric [1]!
Texts are surrounded with single quotes : '
• If quote is inside the text, you need a way to distinguish it from the ending quote
• Prepend a special character e.g. "\" to a quote
• Escaping rules depend on context!
.. WHERE col = 'TEXT DATA' AND ...
OWASP 26
Escaping – context
addslashes() Returns a string with backslashes before characters that need to be quoted in database queries etc. These characters are single quote ('), double quote ("), backslash (\) and NUL (the NULL byte). / Source: php.net manual /
Are you safe?
$user = addslashes($_GET['u']); $pass = addslashes($_GET['p']); $sql = "SELECT * FROM users WHERE username = '{$user}' AND password = '{$pass}'"; $ret = exec_sql($sql);
OWASP
NO
OWASP 28
Escaping – context cont.
Different RDBMS have different ways of escaping data (it also depends on configuration)
addslashes() works just like MySQL only „by chance”
RBDMS PHP function i've got quotes
PDO $pdo->quote($val, $type) n/a (it depends)
MySQL (mysql) mysql_real_escape_string i\'ve got quotes
MySQL (mysqli) mysqli_real_escape_string i\'ve got quotes
Oracle (oci8) n/d - str_replace() i''ve got quotes
SQLite sqlite_escape_string i''ve got quotes
MS SQL (mssql) n/d - str_replace() i''ve got quotes
PostgreSQL pg_escape_string() i''ve got quotes
OWASP 29
Escaping – context cont.
Don't use addslashes(), use PHP functions for your
RBDMS
Are you safe now?
// SELECT * FROM users WHERE username = // '{$user}' AND password = '{$pass}' $_GET['u'] = "anything'"; $_GET['p'] = " or 1=1 -- "; // MySQL sees it as : SELECT * FROM users WHERE username = 'anything\'' AND password = ' or 1=1 -- ' // SQLite / MS SQL / Oracle / PostgreSQL: SELECT * FROM users WHERE username = 'anything\'' AND password = ' or 1=1 -- '
OWASP
ALMOST
OWASP 31
Escaping gotchas – charsets
Errors discovered in 2006 in PostgreSQL and MySQL [1] [2]
In some multibyte charsets despite escaping you can cause SQL injection
\ is „swallowed” by multibyte character
Example:
• BF 27 [ ¬ ' ] BF 5C 27 [ ¬ \ ' ]
• First 2 bytes are character ¿ in GBK charset
• Server will see ¿'
OWASP 32
Escaping gotchas – charsets
Some Asian charsets are vulnerable
Luckily - not UTF-8!
In PostgreSQL '' escaping was used (instead of \')
In mysql_real_escape_string() escaping is done with respect to current connection charset
• Doesn't always work! [1] [2]
Charset also defines context
OWASP 33
Escaping gotchas – object names
Colum, table, database etc. names
• No common good rule to escape them
• Different reserved words, different maximum name lengths etc.
If you need to get those names from the user - use whitelisting (blacklisting if you really can't do otherwise)
OWASP 34
Escaping gotchas – object names cont.
Example - sorting by column
There's a vuln. in $order, but you can't
escape there
$cat_id = (int) $_GET['cid']; $order = $_GET['column']; $stmt = $pdo->prepare("SELECT * FROM products WHERE cid = :cid ORDER BY $order"); $stmt->bindParam(':cid', $cat_id, PDO::PARAM_INT); if ($stmt->execute()) { ... }
OWASP 35
Escaping gotchas – object names cont.
Whitelisting
Blacklisting
$columns = array( // list of allowed columns 'product_name','cid','price', ); if (!in_array($order, $columns, true)) $order = 'product_name'; // default column
// only a-z and _ $order = preg_replace('/[^a-z_]/', '', $order); // max 40 characters $order = substr($order, 0, 40);
OWASP
Escaping in PDO
PDO::quote($value, $type, $len)
Length and type are sometimes ignored!
• Cast numbers to (int), (float)
• Texts – cut them manually
36
$quoted = $pdo->quote($input, PDO::PARAM_STR, 40);
OWASP 37
Escaping in Doctrine
Careful with Doctrine quote()!
$q = Doctrine_Query::create(); // not like this!!! $quoted = $q->getConnection()->quote($input, 'text'); $q->update('User')->set('username', $quoted); // quote() only changes ' to '' - exploit (MySQL): $input = 'anything\\\' where 1=1 -- '; // escape through PDO - getDbh(): $quoted = $q->getConnection() ->getDbh() ->quote($input, PDO::PARAM_STR); // 'anything \\\\\\\' where 1=1 -- '
OWASP 38
Escaping in Propel
Through PDO::quote()
$pdo = Propel::getConnection(UserPeer::DATABASE_NAME); $c = new Criteria(); $c->add(UserPeer::PASSWORD, "MD5(".UserPeer::PASSWORD.") " ." = " . $pdo->quote($password), Criteria::CUSTOM);
OWASP 39
Escaping in Zend Framework
Functions quote(), quoteInto()
$name = $db->quote("O'Reilly"); // 'O\'Reilly' // simplified escaping for a single value $sql = $db->quoteInto("SELECT * FROM products WHERE product_name = ?", 'any string');
OWASP 40
Escaping in MDB
// quote() function - give type $query = 'INSERT INTO table (id, itemname, saved_time) VALUES (' . $mdb2->quote($id, 'integer') .', ' . $mdb2->quote($name, 'text') .', ' . $mdb2->quote($time, 'timestamp') .')'; $res = $mdb2->exec($query);
quote()
OWASP 41
Escaping - summary
Looks easy - search and replace
Just looks
• You need to know the context (database, charset)
• There are invalid implementations
Encourages invalid practices
• concatenating strings to form a SQL command
• ignoring numeric parameters
Use only if
• You program for a single RDBMS
• There is no other way
OWASP 42
How to defend?
Stored procedures
OWASP 43
Stored procedures
SQL command(s) is moved to database server and stored there under a name
Client executes a procedure with input and output parameters
In output parameters client receives results
Data is formally separated from code
It's NOT enough
OWASP 44
Stored procedures cont.
Example for MS SQL – a vulnerable procedure
It's just like eval()!
CREATE PROCEDURE SP_ProductSearch
@prodname varchar(400)
AS DECLARE @sql nvarchar(4000) SELECT @sql = 'SELECT ProductID, ProductName, Category, Price FROM Product Where ProductName
LIKE ''' + @prodname + '''' EXEC (@sql) ...
OWASP 45
Stored procedures cont.
Same vulnerability in Oracle
CREATE OR REPLACE PROCEDURE SP_ProductSearch(Prodname IN VARCHAR2) AS sqltext VARCHAR2(80); BEGIN sqltext := 'SELECT ProductID, ProductName, Category, Price FROM Product WHERE ProductName LIKE ''' || Prodname || ''''; EXECUTE IMMEDIATE sqltext; ... END;
OWASP 46
Stored procedures – Dynamic SQL
Vulnerability lies in Dynamic SQL
• Data is again mixed with code in one variable
How to defend?
• Separate the code from data
• Escape
OWASP 47
Stored procedures in MS SQL
Separating code from data
• use sp_executesql with parameter list
CREATE PROCEDURE SP_ProductSearch @prodname varchar(400) = NULL AS DECLARE @sql nvarchar(4000) SELECT @sql = N'SELECT ProductID, ProductName, Category, Price FROM Product Where ProductName LIKE @p' EXEC sp_executesql @sql, N'@p varchar(400)', @prodname
OWASP 48
Stored procedures in MS SQL cont.
Escaping character data
Example:
Escape only when you must! (use sp_executesql with parameters)
Object name QUOTENAME(@v)
Text <= 128 chars QUOTENAME(@v,'''')
Text > 128 chars REPLACE(@v,'''','''''')
SET @cmd = N'select * from authors where lname=''' + REPLACE(@lname, '''', '''''') + N''''
OWASP 49
Stored procedures in Oracle
Oracle - use EXECUTE IMMEDIATE .. USING
Escaping - DBMS_ASSERT package
CREATE PROCEDURE SP_ProductSearch @prodname varchar(400) = NULL AS DECLARE @sql nvarchar(4000) SELECT @sql = 'SELECT ProductID, ProductName, Category, Price FROM Product Where ProductName LIKE @prodname' EXEC sp_executesql @sql, N'@prodname varchar(400)', @prodname
CREATE OR REPLACE PROCEDURE SP_ProductSearch(Prodname IN VARCHAR2) AS sqltext VARCHAR2(80); BEGIN sqltext := 'SELECT ProductID, ProductName, Category, Price WHERE ProductName=:p'; EXECUTE IMMEDIATE sqltext USING Prodname; ... END;
OWASP 50
Stored procedures in MySQL
Support for Dynamic SQL only through prepared statements
It's actually harder to make vulnerable procedure
Just use placeholders
OWASP 51
Stored procedures in MySQL cont.
PREPARE / EXECUTE USING / DEALLOCATE PREPARE
DELIMITER $$ CREATE PROCEDURE get_users_like ( IN contains VARCHAR(40)) BEGIN SET @like = CONCAT("%", contains, "%"); SET @sql = "SELECT * FROM users WHERE uname LIKE ?"; PREPARE get_users_stmt from @sql; EXECUTE get_users_stmt USING @like; DEALLOCATE PREPARE get_users_stmt; END$$ DELIMITER ;
OWASP 52
Stored procedures in MySQL cont.
Or, even simpler
Escaping – QUOTE() function
DELIMITER $$ CREATE PROCEDURE get_users_like ( IN contains VARCHAR(40)) BEGIN SET @like = CONCAT("%", contains, "%"); SELECT * FROM users WHERE uname LIKE @like; END$$ DELIMITER ;
OWASP 53
Stored procedures in PHP
Different support level, depending on RDBMS
Common API (e.g. PDO) only for simple calls
• No return from procedure
• Returns scalar value in OUT parameter
Different API (or none at all) for advanced calls
• e.g. cursors, fetching records sets
Almost no support in frameworks
Still some errors...
OWASP 54
Stored procedures in PDO
Calling a procedure
// MySQL $sql = "CALL get_users_like(:contains)"; // MS SQL – EXEC get_users_like :contains $stmt = $pdo->prepare($sql); $ret = $stmt->execute(array('contains' => $input)); foreach($stmt->fetchAll() as $users) { var_dump($users); } unset($s);
OWASP 55
Stored procedures in Doctrine/Propel/Zend Framework
Doctrine - no support (use PDO)
Propel – likewise
Zend Framework – likewise
$pdo = Doctrine_Manager::connection()->getDbh();
$pdo = Propel::getConnection(UserPeer::DATABASE_NAME);
$pdo = $db::getConnection();
OWASP 56
Stored procedures in MDB2
You need to manually escape all parameters!
$mdb2->loadModule('Function'); $multi_query = $mdb2->setOption('multi_query', true); if (!PEAR::isError($multi_query)) { $result = $mdb2->executeStoredProc('get_users_like', array($mdb2->quote($contains, 'text'))); do { while ($row = $result->fetchRow()) { var_dump($row); } } while ($result->nextResult()); }
OWASP 57
Stored procedures - gotchas
Data length
CREATE PROCEDURE change_password @loginname varchar(50), @old varchar(50), @new varchar(50) AS DECLARE @command varchar(120) SET @command= 'UPDATE users SET password=' + QUOTENAME(@new, '''') + ' WHERE loginname=' + QUOTENAME(@loginname, '''') + ' AND password=' + QUOTENAME(@old, '''') EXEC (@command) GO
OWASP 58
Stored procedures - summary
Moving SQL logic to server takes time
Code is not easily ported to other RDBMS
You need to use prepared statements or escaping to write safe stored procedures anyway
If done poorly, you're even more vulnerable
• Both SP code and statement calling SP could be vulnerable
• SP usually has greater permissions than code calling it
Bad support in PHP and frameworks
OWASP 59
Stored procedures - summary
SPs have many advantages outside our scope
Could be used with different clients (Java/.NET + PHP)
Could have better berformance
and many more...
Conclusion:
You can write secure stored procedures, but they usually increase the application cost considerably
It is vital to write stored procedures protected against SQL injection
OWASP 60
How to defend?
Additional methods
OWASP 61
Validation and filtering
Validate all external data
Validate before processing
Filter INPUT - escape OUTPUT
Different validation rules for each parameter - check e.g.
• Type
• Scalar / array
• Min / max values
• Character data length! [1]
OWASP 62
Additional methods
Complementary to all previously mentioned!
Principle of least privilege when connecting to DB
Removing unused functions, accounts, packages shipped with database
Routinely updating the system and database software
Correct PHP and database configuration • magic_quotes_* = false
• display_errors = false
Good database design
OWASP 63
Summary
Pay attention to SQL injection - even a single mistake could cost you!
Prefer complete solutions - e.g. frameworks
Filter and validate all input data
Remeber about data types and lengths
Prefer whitelisting to blacklisting - the latter will fail one day!
Use prepared statements whenever you can
Try to avoid escaping
In stored procedures double check your Dynamic SQL
OWASP 64
Links
Discussed projects
• sqlmap.sourceforge.net
• php.net/manual/en/book.pdo.php
• www.doctrine-project.org
• propel.phpdb.org/trac
• framework.zend.com
• pear.php.net/package/MDB2
About SQL injection
• www.owasp.org/index.php/SQL_Injection
• unixwiz.net/techtips/sql-injection.html
• delicious.com/koto/sql+injection
Hack me
• threats.pl/bezpieczenstwo-aplikacji-internetowych
• tinyurl.com/webgoat
• mavensecurity.com/dojo.php
[email protected] http://blog.kotowicz.net