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