SQL 注入
是时候看看 SQL 注入了。在很长一段时间里,SQL 注入是 OWASP 十大漏洞中无可争议的王者,连续多年都是如此。尽管 SQL 注入已经有 20 多年的历史,尽管它在榜单上的排名略有下降,但它仍然是一个非常流行和危险的漏洞。
作为一种网络安全漏洞,SQL 注入(SQLi)仍然是攻击者最常用的 "黑客 "技术之一,因为它允许攻击者操纵数据库并从中提取关键信息。更令人担忧的是,攻击者可以让自己成为数据库服务器的管理员,并做出一些真正具有破坏性的事情,如破坏数据库、操纵事务、泄露数据,并使其容易受到更多问题的攻击。
让我们快速了解一下它是如何发生的
SQL(或结构化查询语言)是用于与关系数据库通信的语言;它是开发人员、数据库管理员和应用程序用来管理每天产生的海量数据的查询语言。
在应用程序中存在两个上下文:一个是数据上下文,另一个是代码上下文。代码上下文告诉计算机要执行什么,并将其与要处理的数据分开。当攻击者输入的数据被 SQL 解释器误认为是代码时,就会发生 SQL 注入,使攻击者得以从应用程序中收集有价值的信息。
SQL 注入攻击的影响
SQL 注入对任何网络应用程序都极为有害,而且是许多高知名度漏洞背后的首选技术,因为它能让攻击者在未经授权的情况下访问关键数据。他们可以看到大量信息,从用户名和密码到信用卡详细信息和个人身份号码。
在获得这些数据后,攻击者可以接管账户、重置密码、进行长时间的网上购物或实施其他(更糟糕的)欺诈行为。
但 SQLi 最令人担忧的地方可能是,攻击者可以在不被发现的情况下,长期保持一个后门进入系统。可以想象,无论后门打开多久,都会导致重复的数据泄露。这太可怕了。
让我们来看几个例子,以便更好地理解其实际效果。
SQLi 示例
SQLi 包括各种可应对不同情况的漏洞技术。以下是一些最常见的 SQLi 例子:
SQLi 类型
好了,现在让我们来看看三种不同的 SQLi 类型。
带内 SQLi
这是最常见、最简单、最有效的 SQL 注入类型之一。在这种类型的攻击中,攻击和检索结果都使用相同的通信渠道。
以下是两种带内 SQLi 攻击:
- 基于联合的 SQLi- 基于联合的攻击利用联合运算符组合两个或多个 SQL 查询(如 SELECT 语句),以获取所需的信息,并在 HTTP GET 响应中得到结果。
- 基于错误的SQLi - 攻击者利用数据库的错误信息来了解其结构。在这种攻击中,攻击者可能会发送错误请求或执行操作,使服务器显示错误信息,从而接收数据库信息。因此,开发人员必须避免在实时环境中发送错误或日志信息;相反,应限制访问权限来存储这些信息。
推理 SQLi
推理或盲目 SQLi 攻击更为复杂,需要更多时间来利用。此外,攻击者不会立即得到攻击结果,这也是盲目攻击的特点。
攻击者通过 HTTP 请求向数据库服务器发送有效载荷,重组用户的数据库,然后观察应用程序的响应和行为,以确定攻击是否成功。
这是两种推理型 SQLi 攻击:
- 基于布尔值的盲 SQLi- 在这种攻击中,向数据库发送查询以获取布尔值(真或假)结果,攻击者观察 HTTP 响应以预测布尔值结果。
- 基于时间的盲 SQLi- 在这种攻击中,攻击者向数据库发送查询,让数据库等待几秒钟再发送响应,攻击者根据 HTTP 请求的响应时间判断查询结果。
带外 SQLi
这是一种较为罕见的 SQLi 攻击类型,取决于数据库服务器启用的功能。它发生在攻击者无法真正使用其他攻击类型的情况下。
例如,如果他们无法使用相同的通信渠道进行带内攻击,或者 HTTP 响应不够清晰,他们无法计算出查询结果。
此外,这种攻击并不常见,因为它严重依赖数据库服务器向攻击者发送 HTTP 或 DNS 请求所需数据的能力。
如何防御 SQLi
值得庆幸的是,SQL 注入如此古老而常见,但也有办法防止它的发生。使用这些预防技术不仅是一种良好的编码习惯,还能真正提高企业的 SQLi 安全性。
有多种方法可以确保数据库服务器免受此类攻击,如输入验证、使用网络应用程序防火墙(WAF)、确保数据库安全、使用第三方安全团队或系统以及编写防错 SQL 查询。
让我们来看一个在 Python 中采用上述安全措施之一来防止 SQL 注入的例子。
Python 示例
在本例中,攻击者将使用基于布尔的盲 SQL 注入从系统中获取重要信息。
Python易受攻击
假设数据库中有一个名为 "sample_data "的表。该表存储了应用程序用户的用户名和密码。
现在,允许用户通过以下命令从该数据库表中查找值:
import mysql.connector
db = mysql.connector.connect
#Bad Practice(不良做法)。避免这样做!
(host="localhost", user="newuser", passwd="pass", db="sample")
cur = db.cursor()
name = raw_input('Enter Name: ')
cur.execute("SELECT * FROM sample_data WHERE Name = '%s';" % name) for row in cur.fetchall(): print(row)
db.close()
SQL 注入
在这里,如果用户在搜索中输入一个名字,例如 Alicia,那么输出结果就不会有问题。
但是,如果用户输入类似 Alicia'; DROP TABLE sample_data;这样的内容,就会对数据库产生重大影响。
蟒蛇补救
应将 SQL 语句更改为以下内容,以防止攻击发生:
cur.execute("SELECT * FROM sample_data WHERE Name = %s;", (name,))
现在,即使用户试图注入任何 SQL 查询,系统也会将用户输入的内容视为字符串,并只将用户输入的内容视为名称的值。
这一简单改动就能防止未来查询中的恶意活动,并确保系统免受用户输入攻击。
Java 示例
在本例中,我们还将使用一个名为 "sample_data "的数据库表来存储应用程序的用户数据。
一个基本的登录页面需要用户名和密码,而作为 servlet(LoginServlet)的 java 文件会根据数据库验证用户名和密码,以便进行登录操作。
Java:易受攻击的示例
系统使用数据库中的 "sample_data "表,允许用户将其凭据作为输入执行登录操作。
在 LoginServlet 文件中有一个用于登录操作的查询,即
//Bad Example. Do not use string concatenation.
String query = "select * from sample_data where username='" + username + "' and password = '" + password + "'";
Connection conn = null;
Statement stmt = null;
try {
conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/user", "root", "root");
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(query);
if (rs.next()) {
// Login Successful if match is found
success = true;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
stmt.close();
conn.close();
} catch (Exception e) {}
}
if (success) {
response.sendRedirect("home.html");
} else {
response.sendRedirect("login.html?error=1");
}
}
以下是用户登录的查询:
select * from sample_data where username='username' and password ='password'.
SQL 注入
如果输入有效,系统就能完美运行。例如,我们再假设用户名是 Alicia,密码是 secret。
系统将根据这些凭据返回用户的数据。然而,攻击者可以使用 Postman 和 cURL 操作用户请求,进行 SQL 注入。
例如,黑客可以发送一个假用户名(Alicia)和密码'或'1'='1'。
在这种情况下,用户名和密码将不匹配,但条件'1'='1'将始终为真,因此登录操作将成功。
爪哇预防
为防止这种情况,我们需要修改 LoginValidation 代码,并使用 PreparedStatement 代替 Statement 执行查询。这一修改将防止在查询中连接用户名和密码,并将其视为设置器数据,以避免 SQL 注入。
以下是 LoginValidation 的修改代码:
String query = "select * from sample_data where username=? and password = ?";
Connection conn = null;
PreparedStatement stmt = null;
try {
conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/user", "root", "root");
stmt = conn.prepareStatement(query);
stmt.setString(1, username);
stmt.setString(2, password);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
success = true;
}
rs.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
stmt.close();
conn.close();
} catch (Exception e) {
}
}
在这种情况下,PreparedStatement、设置器和底层 JDBC API 将处理用户输入,防止 SQL 注入。
实例
现在,我们再来看几个不同语言的示例,以便更好地理解其实际效果。
C# - 不安全
由于使用了 `FromRawSql`,此示例并不安全。该方法不绑定参数,也不试图转义参数。因此,应尽量避免使用此方法。
var blogs = context.Posts
.FromRawSql("SELECT * FROM Posts WHERE state = {0} AND author = {1}", state, author)
.ToList();
C# - 安全
这个示例的安全性得益于 `FromSqlInterpolated`,它获取内插值并将其参数化。
虽然这通常是安全的,但它有可能与不安全的 `FromRawSql`非常相似。
var blogs = context.Posts
.FromSqlInterpolated($"SELECT * FROM Posts WHERE state = {state} AND author = {author}")
.ToList();
Java - 安全:Hibernate - 命名查询 + 本地查询
Hibernate 通过 "原生查询 "和 "命名查询 "提供了两种以安全方式构造查询的方法。这两种方法都允许指定参数的位置。
@NamedNativeQuery(
name = "find_post_by_state_and_author",
query =
"SELECT * " +
"FROM Post " +
"WHERE state = :state" +
" AND author = :author",
resultClass = Post.class)
java
List<Post> posts = session.createNativeQuery(
"SELECT * " +
"FROM Post " +
"WHERE state = :state" +
" AND author = :author" )
.addEntity(Post.class)
.setParameter("state", state)
.setParameter("author", author)
.list();
Java - 安全:jplq
通过在 jplq 资源库接口上注解 "Query "属性,它们可以有多种形式,并被参数化。
@Query("SELECT p FROM Post p WHERE u.state = ?1 and u.author = ?2")
Post findPostByStateAndAuthor(String state, int author);
@Query("SELECT p FROM Post p WHERE u.state = :state and u.author = :author")
User findPostByStateAndAuthor(@Param("state") String state, @Param("author") int author);
Javascript - 安全:pg
在使用 `pg` 库时,`query` 方法允许通过第二个参数提供参数值来进行参数化。
const { posts }= await db.query('SELECT * FROM Post WHERE state = $1 AND author = $2', [state, author])
Javascript - 安全:序列化
sequelize "库提供了一种通过第二个参数为查询设置参数的方法。这包括一个按名称或索引作为参数绑定到查询的值列表。