1. 什么是SQL注入(SQLi)
SQL注入是一个web安全漏洞,它允许攻击者干扰应用程序对其数据库的查询。它通常允许攻击者查看他们正常无法检索的数据。这可能包括属于其他用户的数据,或者应用程序本身能够访问的任何其他数据。在许多情况下,攻击者可以修改或删除这些数据,从而导致对应用程序内容或行为的永久改变。
在某些情况下,攻击者可以升级SQL注入攻击,以危及底层服务器或其他后端基础设施,或执行拒绝服务攻击。
2. 成功的SQL注入攻击的影响是什么?
成功的SQL注入攻击可能导致对敏感数据的未授权访问,如密码、信用卡详细信息或个人用户信息。近年来,许多备受瞩目的数据泄露都是SQL注入攻击的结果,导致了声誉受损和监管罚款。在某些情况下,攻击者可以得到进入系统的持久后门,从而导致在很长一段时间内不被发现。
3. SQL注入举例
在不同的情况下会出现各种各样的SQL注入漏洞、攻击和技术。一些常见的SQL注入示例包括:
检索隐藏的数据,您可以在其中修改SQL查询以返回额外的结果。
破坏应用程序逻辑,您可以更改一个查询来干扰应用程序的逻辑。
UNION攻击,您可以从不同的数据库表中检索数据。
检查数据库,从中可以提取关于数据库版本和结构的信息。
SQL盲注,即在应用程序的响应中不返回您控制的查询结果。
4. 检索隐藏数据
考虑一个以不同类别显示产品的购物应用程序。当用户点击Gifts类别时,他们的浏览器会请求URL:
https://insecure-website.com/products?category=Gifts
这会导致应用程序进行SQL查询,从数据库中检索相关产品的详细信息:
SELECT * FROM products WHERE category = 'Gifts' AND released = 1
这个SQL查询请求数据库返回:
所有详细信息(*)
从products表获取数据
分类是Gifts
另外一个条件,and released is 1.
限制条件released = 1用于隐藏未发布的产品。对于未发布的产品,推测released= 0。
应用程序没有任何针对SQL注入攻击的防御,因此攻击者可以构造如下攻击:
https://insecure-website.com/products?category=Gifts'--
SQL查询语句为:
SELECT * FROM products WHERE category = 'Gifts'--' AND released = 1
这里的关键是双破折号--在SQL中是注释器,这意味着--后面部分被解释为注释。这有效地删除了查询的剩余部分,因此不再包含AND released = 1。这意味着将显示所有产品,包括未发布的产品。
更进一步,攻击者可以导致应用程序显示任何类别中的产品,包括他们不知道的类别:
https://insecure-website.com/products?category=Gifts'+OR+1=1--
SQL查询语句为:
SELECT * FROM products WHERE category = 'Gifts' OR 1=1--' AND released = 1
修改后的查询将返回类别为Gifts或1等于1的所有项目。因为1=1总是true,所以查询将返回所有项。
5. 颠覆应用程序逻辑
考虑一个使用用户名和密码登录的应用程序。如果用户提交了用户名wiener和密码bluecase,应用程序通过执行以下SQL查询来检查凭据:
SELECT * FROM users WHERE username = 'wiener' AND password = 'bluecheese'
如果查询返回用户的详细信息,则登录成功。否则,将被拒绝。
在这里,攻击者可以以任意用户登录而不需要密码,只需使用SQL注释 -- ,从查询的WHERE子句中删除密码检查。例如,提交用户名administrator'-- 和一个空密码会得到以下查询:
SELECT * FROM users WHERE username = 'administrator'--' AND password = ''
此查询返回用户名为administrator的用户,并以该用户成功地让攻击者登录。
6. 从其他表中检索数据
在应用程序响应中返回SQL查询结果的情况下,攻击者可以利用SQL注入漏洞从数据库中的其他表检索数据。这是使用UNION关键字完成的,它允许您执行一个额外的SELECT查询,并将结果附加到原始查询。
例如,如果一个应用程序执行以下包含用户输入“Gifts”的查询:
SELECT name, description FROM products WHERE category = 'Gifts'
攻击者可以提交以下输入:
' UNION SELECT username, password FROM users--
这将导致应用程序返回所有用户名和密码以及产品的名称和描述。
7. 查询数据库信息
在初步识别SQL注入漏洞之后,获取关于数据库本身的一些信息通常是有用的。这些信息通常可以为下一步利用铺平道路。
可以查询数据库的详细版本信息。完成此操作的方式取决于数据库类型,因此您可以根据任何技术推断数据库类型。例如,在Oracle上可以执行:
SELECT * FROM v$version
您还可以确定存在哪些数据库表,以及它们包含哪些列。例如,在大多数数据库上,您可以执行以下查询来列出表:
SELECT * FROM information_schema.tables
8. SQL盲注
许多SQL注入实例都是盲注。这意味着应用程序不会在其响应中返回SQL查询的结果或任何数据库错误的详细信息。但是仍然可以利用盲注访问未经授权的数据,所涉及的技术通常更复杂,更难执行。
根据漏洞的性质和所涉及的数据库,可以使用以下技术来利用SQL盲注漏洞:
您可以更改查询的逻辑触发可检测到的返回差异,这取决于单个条件是否为真。这可能涉及到将一个新条件注入到某个布尔逻辑中,或者有条件地触发一个错误,比如被零除的错误。
您可以有条件地触发查询处理中的时间延迟,这样可以根据应用程序响应所需的时间推断条件的真实性。
可以使用OAST技术触发带外网络交互。这个技巧非常强大,尤其在其他技巧不管用的情况下。通常,您可以通过带外通道直接提取数据,例如,将数据放入您控制的域的DNS查找中。
9. 如何检测SQL注入漏洞
可以通过对应用程序中的每个入口点使用一组系统化手动检测SQL注入。这通常包括:
提交单引号字符 ',并寻找错误或其他异常。
提交一些特定的sql语法,先为入口点提供基本(原始)值,然后提交一个不同的值,并在响应中寻找系统差异。
提交布尔条件,如OR 1=1和OR 1=2,并在应用程序的响应中寻找差异。
提交用于在SQL查询中执行时触发时间延迟的payload,并查找响应所需时间的差异。
提交OAST payload,用于在SQL查询执行时触发带外交互,并监视产生的任何交互。
10. 在查询的不同位置进行SQL注入
大多数SQL注入漏洞都出现在SELECT查询的WHERE子句中。这种类型的SQL注入通常被有经验的测试人员很好地理解。
但是SQL注入漏洞原则上可以出现在查询中的任何位置,以及不同的查询类型中。SQL注入最常见的其他地方是:
在UPDATE语句中,更新的值或WHERE子句中。
在INSERT语句中,插入的值。
在SELECT语句中,在表名或列名中。
在SELECT语句中,在ORDER BY子句中。
11. 二阶SQL注入
当应用程序从HTTP请求中获取用户输入,并在处理该请求的过程中,以不安全的方式将输入合并到SQL查询中时,就会出现一阶SQL注入。
在二阶SQL注入(也称为存储SQL注入)中,应用程序从HTTP请求中获取用户输入,并将其存储起来以备将来使用。这通常是通过将输入存储到数据库中来完成的,但是在存储数据的地方没有出现漏洞。之后,当处理不同的HTTP请求时,应用程序检索存储的数据,并以不安全的方式将其合并到SQL查询中。
在开发人员意识到SQL注入漏洞的情况下,经常会出现二阶SQL注入,因此可以安全地将原始数据插入到数据库。当数据稍后被处理时,它被认为是安全的,因为它之前被安全地放入数据库。此时,数据以一种不安全的方式处理,因为开发人员错误地认为它是可信的。
12. 数据库特有因素
SQL语言的一些核心特性在常见数据库以相同的方式实现,而且检测和利用SQL注入漏洞的许多方法在不同类型的数据库上都是相同的。
然而,常见的数据库之间也有许多不同之处。这意味着用于检测和利用SQL注入的一些技术在不同的平台上的工作方式是不同的。例如:
字符串拼接的语法。
注释
批量查询
平台特有的api。
错误信息
13. 如何防御SQL注入
可以通过在查询中使用参数化查询(也称为预编译)而不是字符串拼接来防止大多数SQL注入实例。
以下代码容易受到SQL注入的影响,因为用户的输入直接拼接到查询中:
String query = "SELECT * FROM products WHERE category = '"+ input + "'";
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(query);
这段代码可以很容易地修改以防止SQL注入:
PreparedStatement statement = connection.prepareStatement("SELECT * FROM products WHERE category = ?");
statement.setString(1, input);
ResultSet resultSet = statement.executeQuery();
参数化查询可用于查询中的不可信输入作为数据出现的任何情况,包括where子句和INSERT或UPDATE语句中的值。它们不能用于处理查询中的其他部分中的不可信输入,比如表名或列名,或者ORDER BY子句。在这些位置的不可信输入,应用需要采用不同的方法,例如允许输入值的白名单,或使用不同的逻辑来交付所需的行为。
要使参数化查询有效地防止SQL注入,查询中使用的字符串必须始终是硬编码的常量,决不能包含来自任何来源的任何变量数据。不要试图逐个决定数据项是否受信任,并继续认为在查询中使用字符串拼接是安全的。