SQL注入攻击包括从客户端向应用程序输入的SQL查询注入或“注入”恶意数据。
一个成功的SQL注入攻击可以做到以下内容:
SQL注入包括普通的SQL注入和盲注。
一般的SQL注入过程中,攻击者输入一段带有攻击性质的内容以后,网页会显示对应的结果,此时攻击者就可以根据结果做出自己的调整。
String query =“SELECT account_balance FROM user_data WHERE user_name =” + request.getParameter(“customerName”);
try{
Statement statement = connection.createStatement(...);
ResultSet results = statement.executeQuery(query);
}
在此例子中如果攻击者在浏览器中将"customerName"参数的值修改成:smith or ‘1’='1。例如:
SELECT account_balance FROM user_data WHERE user_name = smith or 1=1
这样查询语句的意义就变成了从user_data表中返回所有的记录。
有几种不同类型盲注:基于内容和基于时间的SQL注入。
SELECT * from articles where article_id = 4
当我们尝试改变URL的信息为:SELECT * from articles where article_id = 4 AND 1 = 1 它就会被解析成:
SELECT * from articles where article_id = 4 AND 1 = 1
如果浏览器将返回之前使用url为https://my-shop.com?article=4 的相同画面,则知道该网站容易受到SQL盲注攻击。如果浏览器返回未找到的页面或其他内容响应,则知道SQL盲注对其不起作用。
那么我们如何才能真正的利用这一点呢,上面我们只是问了数据一个很基础的问题,但你也可以使用一下方式询问:https://my-shop.com?article=4 AND substring(database_version(),1,1) = 2
大多数情况下,首先查找使用的数据库类型,根据数据库的类型可以找到数据库的系统表,可以枚举数据库中存在的所有表。使用此信息,开始从所有表中获取信息,并且可以转存数据库。请注意,如果正确的设置了数据库内的权限,这中方法可能不起作用(也就是意味着无法使用从Web应用程序连接到数据的用户查询系统表)。
另外一种方法称为基于时间的SQL注入,在这种情况下,你在数据库返回结果之前进行等待。如果你完全盲注的话,你可能需要这个功能,根据发送请求到完全响应时间来判断,例如:
article = 4; sleep(10) --
选项1:使用预编译的语句(参数化查询)
使用带有变量绑定的预编译语句(也称为参数化查询)是所有开发人员应该首先学习如何编写数据库查询的方法。它们比动态查询更易于编写,更易于理解。参数化查询强制开发人员首先定义所有SQL代码,然后将每个参数传递给查询。无论输入是什么,这种编码允许数据库区分代码和数据。
编译好的语句可确保攻击者无法更改查询的意图,即使攻击者插入了SQL命令也是如此。在下面的安全示例中,如果攻击者要输入tom’或’1’='1的userID,则参数化查询将不会受到攻击,而是会查找与字符串完全匹配的用户名tom’或’1 ‘=’ 1。
String custname = request.getParameter("customerName"); // This should REALLY be validated too
// perform input validation to detect attacks
String query = "SELECT account_balance FROM user_data WHERE user_name = ? ";
PreparedStatement pstmt = connection.prepareStatement( query );
pstmt.setString( 1, custname);
ResultSet results = pstmt.executeQuery( );
选项2:使用存储过程
SQL注入可以注入一些并不是安全的存储过程。某些标准存储过程编程构造与使用参数化查询具有相同的效果。大多数存储过程语言的标准要求开发人员只使用自动参数化的参数构建SQL语句。预编译语句和存储过程之间的区别在于,存储过程的SQL代码已定义并存储在数据库本身中,然后从应用程序中调用。这两种技术在防止SQL注入方面具有相同的效果。
在某些情况下,存储过程可能会增加风险。例如,在MS SQL服务器上,您有3个主要的默认角色:db_datareader,db_datawriter和db_owner。在存储过程开始使用之前,DBA会根据要求为webservice的用户提供db_datareader或db_datawriter权限。但是,存储过程需要执行权限,默认情况下不可用的角色。用户管理已集中在一些设置,但仅限于这3个角色,导致所有Web应用程序在db_owner权限下运行,因此存储过程可以正常工作。当然,这意味着如果服务器遭到破坏,攻击者拥有数据库的完全权限,以前他们可能只有读取权限。
下面的代码示例使用CallableStatement(Java的存储过程接口实现)来执行相同的数据库查询。必须在数据库中预定义“sp_getAccountBalance”存储过程,并实现与上面定义的查询相同的功能。
String custname = request.getParameter("customerName"); // This should REALLY be validated
try {
CallableStatement cs = connection.prepareCall("{call sp_getAccountBalance(?)}");
cs.setString(1, custname);
ResultSet results = cs.executeQuery();
// … result set handling
} catch (SQLException se) {
// … logging and error handling
}
白名单验证就是验证用户的输入是否在白名单中,用户请求时,程序会对用户的输入与白名单中的标准进行判断,如果不符合就拒绝用户的输入。白名单时开发人员自己定义的,可以是某个变量允许的输入,也可以是利用正则匹配用户的输入。
以下是表名验证的示例。
String tableName;
switch(PARAM):
case "Value1": tableName = "fooTable";
break;
case "Value2": tableName = "barTable";
break;
...
default : throw new InputValidationException("unexpected value provided for table name");
然后可以将tableName直接附加到SQL查询,因为现在已知它是此查询中表名的合法值和期望值之一。但是由于表名不希望在查询中被使用,通用表验证功能可能会导致数据丢失。
对于像排序顺序这样简单的字符,最好将用户提供的输入转换为布尔值,然后使用该布尔值选择要附加到查询的安全值。这是动态查询创建中非常标准的需求。例如:
public String someMethod(boolean sortOrder) {
String SQLquery = "some SQL ... order by Salary " + (sortOrder ? "ASC" : "DESC");
...
任何时候用户输入都可以转换为非String类型,如日期,数字,布尔值,枚举类型等,然后将其附加到查询中,或者用于选择要附加到查询的值,这可以确保它是这样做是安全的。在所有情况下,即使使用绑定变量也建议将输入验证作为辅助防御。
当上述任何一种方法都不可行时,该技术仅应作为最后的手段使用。此技术是在将用户输入放入查询之前将其转义。每个DBMS都支持一种或多种特定于某些查询的字符转义方案。如果您使用正在使用的数据库的正确转义方案转义所有用户提供的输入,则DBMS不会将该输入与开发人员编写的SQL代码混淆,从而避免任何可能的SQL注入漏洞。
OWASP企业安全API(ESAPI)是一个免费的开源Web应用程序安全控制库,使程序员可以更轻松地编写风险较低的应用程序。ESAPI库旨在使程序员更容易将安全性改进现有应用程序。ESAPI库也是新开发的坚实基础。
有关ESAPI的详细信息,请参见OWASP(https://www.owasp.org/index.php/Category:OWASP_Enterprise_Security_API)。
使用ESAPI数据库编解码器非常简单。Oracle示例如下所示:
ESAPI.encoder().encodeForSQL( new OracleCodec(), queryparam );
因此,如果您在代码中生成了一个现有的动态查询,该查询将转到Oracle,如下所示:
String query = "SELECT user_id FROM user_data WHERE user_name = '" + req.getParameter("userID") + "' and user_password = '" + req.getParameter("pwd") +"'";
try {
Statement statement = connection.createStatement( … );
ResultSet results = statement.executeQuery( query );
}
此时就可以重写第一行查询语句:
Codec ORACLE_CODEC = new OracleCodec();
String query = "SELECT user_id FROM user_data WHERE user_name = '" + ESAPI.encoder().encodeForSQL( ORACLE_CODEC, req.getParameter("userID")) + "' and user_password = '"+ ESAPI.encoder().encodeForSQL( ORACLE_CODEC, req.getParameter("pwd")) +"'";
此时无论输入什么,都可以有效防止SQL注入。
为了获得最大的代码可读性,我们还可以构建自己的OracleEncoder。
Encoder oe = new OracleEncoder();
String query = "SELECT user_id FROM user_data WHERE user_name = '" + oe.encode( req.getParameter("userID")) + "' and user_password = '" + oe.encode( req.getParameter("pwd")) +"'";
使用这种类型的方法,您只需要将每个用户提供的参数封装到ESAPI.encoder().encodeForOracle()调用或您命名为调用的任何内容中。
使用SET DEFINE OFF或SET SCAN OFF确保关闭自动字符替换。如果打开此字符替换,则&字符将被视为SQLPlus变量前缀,可能允许攻击者检索私有数据。
LIKE关键字允许文本扫描搜索。在Oracle中,下划线“_”字符仅匹配一个字符,而“&”符号用于匹配任何字符的零次或多次出现。必须在LIKE子句条件中转义这些字符。例如:
SELECT name FROM emp
WHERE id LIKE '%/_%' ESCAPE '/';
SELECT name FROM emp
WHERE id LIKE '%\%%' ESCAPE '\';
MySQL支持两种转义模式:
1.ANSI_QUOTES SQL模式,以及我们调用的关闭模式;
2.MySQL模式。
ANSI SQL模式:只需用’’(两个单引号)对所有’(单引号)字符进行编码
在MySQL模式下,执行以下操作:
NUL (0x00) --> \0 [This is a zero, not the letter O]
BS (0x08) --> \b
TAB (0x09) --> \t
LF (0x0a) --> \n
CR (0x0d) --> \r
SUB (0x1a) --> \Z
" (0x22) --> \"
% (0x25) --> \%
' (0x27) --> \'
\ (0x5c) --> \\
_ (0x5f) --> \_
所有其他非字母数字字符,ASCII值小于256 - > \ c
其中“c”是原始的非字母数字字符。
转义的一个特殊情况是对从用户接收的整个字符串进行十六进制编码的过程(这可以看作是转义每个字符)。Web应用程序应在将用户输入包含在SQL语句中之前对其进行十六进制编码。SQL语句应该考虑到这一事实,并相应地比较数据。例如,如果我们必须查找匹配sessionID的记录,并且用户将字符串abc123作为会话ID发送,则select语句将为:
SELECT ... FROM session
WHERE hex_encode(sessionID)='616263313233'
(hex_encode应替换为正在使用的数据库的特定工具。)字符串606162313233是从用户接收的字符串的十六进制编码版本(它是用户的ASCII / UTF-8代码的十六进制值序列)数据)。
如果攻击者要传输包含单引号字符的字符串,然后尝试注入SQL代码,则构造的SQL语句将只显示如下:
WHERE hex_encode ( ... ) = '2720 ... '
27是单引号的ASCII代码(十六进制),它与字符串中的任何其他字符一样只是十六进制编码。生成的SQL只能包含数字和字母a到f,并且永远不能包含任何可以启用SQL注入的特殊字符。