SQL注入是十大 Web 应用程序漏洞之一。简单来说,SQL注入是指通过用户输入的数据在查询中注入/插入SQL代码。它可以发生在任何使用关系数据库(如 Oracle、MySQL、PostgreSQL 和 SQL Server)的应用程序中。
为了执行 SQL 注入,恶意用户首先尝试在应用程序中找到可以嵌入 SQL 代码和数据的位置。它可以是登录页面任何网络应用程序或任何其他地方。因此,当应用程序收到嵌入 SQL 代码的数据时,SQL 代码将与应用程序查询一起执行。
- 恶意用户可以获得对您的应用程序的未经授权的访问并窃取数据。
- 他们可以更改、删除数据库中的数据并关闭您的应用程序。
- 黑客还可以通过执行数据库特定的系统命令来控制运行数据库服务器的系统。
假设我们有一个名为tbluser它存储应用程序用户的数据。这userId是表的主列。我们的应用程序具有功能,可让您通过以下方式获取信息userId.userId 的值是从用户请求中接收的。
让我们看一下下面的示例代码。
String userId = {get data from end user};
String sqlQuery = "select * from tbluser where userId = " + userId;
当使用有效数据(即 userId 值 132)执行上述查询时,它将如下所示。
输入数据:132
执行的查询:从 tbluser 中选择 *,其中 userId=132
结果:查询将返回 userId 132 的用户的数据。在这种情况下没有发生 SQL 注入。
黑客可以使用 Postman、cURL 等工具更改用户请求,将 SQL 代码作为数据发送,从而绕过任何 UI 端验证。
输入数据:2 or 1=1
执行的查询:从 tbluser 中选择 *,其中 userId=2 或 1=1
结果:现在上面的查询有两个带有 SQL OR 表达式的条件。
-
userId=2:这部分将匹配 userId 值为“2”的表行。
-
1=1:这部分将始终被评估为正确。所以查询将返回表的所有行。
我们来看看 SQL 注入的四种类型。
上面的例子是一个基于布尔的SQL注入的例子。它使用计算结果为 true 或 false 的布尔表达式。它可用于从数据库获取附加信息。例如;
输入数据:2或1=1
SQL 查询:从 tbl_employee 中选择名字、姓氏,其中 empId=2 or 1=1
SQL 联合运算符将来自两个不同查询的数据与相同列数组合在一起。在本例中,联合运算符用于从其他表中获取数据。
输入数据:2 union从tbluser中选择用户名、密码
查询:从 tbl_employee 中选择名字、姓氏,其中 empId=2 union从tbluser中选择用户名、密码
通过使用基于联合的 SQL 注入,攻击者可以获得用户凭据。
In 基于时间的 SQL 注入,在查询中注入特殊函数,可以将执行暂停指定的时间。这种攻击会降低数据库服务器的速度。它可能会影响数据库服务器的性能,从而降低您的应用程序的性能。例如,在 MySQL 中:
输入数据:2 + 睡眠(5)
Query: 从 tbl_employee 中选择 emp_id、first_name、last_name,其中 empId=2 + SLEEP(5)
在上面的示例中,查询执行将暂停 5 秒。
在此变体中,攻击者尝试从数据库获取错误代码和消息等信息。攻击者注入语法不正确的SQL,因此数据库服务器将返回错误代码和消息,可用于获取数据库和系统信息。
我们将使用一个简单的Java网络应用程序演示 SQL 注入。我们有登录.html,这是一个基本的登录页面,它从用户那里获取用户名和密码并将其提交到登录Servlet.
LoginServlet 从请求中获取用户名和密码,并根据数据库值验证它们。如果身份验证成功,Servlet 会将用户重定向到主页,否则将返回错误。
登录.html Code:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Sql Injection Demo</title>
</head>
<body>
<form name="frmLogin" method="POST" action="https://localhost:8080/Web1/LoginServlet">
<table>
<tr>
<td>Username</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>Password</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td colspan="2"><button type="submit">Login</button></td>
</tr>
</table>
</form>
</body>
</html>
LoginServlet.java Code:
package com.journaldev.examples;
import java.io.IOException;
import java.sql.*;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
@WebServlet("/LoginServlet")
public class LoginServlet extends HttpServlet {
static {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (Exception e) {}
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
boolean success = false;
String username = request.getParameter("username");
String password = request.getParameter("password");
// Unsafe query which uses string concatenation
String query = "select * from tbluser 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");
}
}
}
数据库查询[MySQL]:
create database user;
create table tbluser(username varchar(32) primary key, password varchar(32));
insert into tbluser (username,password) values ('john','secret');
insert into tbluser (username,password) values ('mike','pass10');
输入用户名: john
输入用户名: secret
Query:从 tbluser 中选择 *,其中用户名 =“john”,密码 =“secret”
Result:用户名和密码存在于数据库中,因此认证成功。用户将被重定向到主页。
输入用户名: dummy
输入密码: ' 或 '1'='1
Query:从 tbluser 中选择 *,其中用户名=‘dummy’且密码 =‘’或‘1’=‘1’
Result:输入的用户名和密码在数据库中不存在,但验证成功。Why?
这是由于 SQL 注入,因为我们输入了 ' 或 '1'='1 作为密码。查询中有3个条件。
-
用户名=‘虚拟’:它将被评估为 false,因为表中没有用户名为 dummy 的用户。
-
密码=‘’:由于表中没有空密码,因此它将被评估为 false。
-
‘1’=‘1’:它将被评估为 true,因为这是静态字符串比较。
现在结合所有 3 个条件,即假与假或真=>最终结果将是true.
在上面的场景中,我们使用了布尔表达式来执行SQL注入。还有一些其他方法可以进行 SQL 注入。在下一节中,我们将了解在 Java 应用程序中防止 SQL 注入的方法。
最简单的解决方案是使用准备好的声明代替陈述执行查询。
我们不是将用户名和密码连接到查询中,而是通过PreparedStatement 的 setter 方法提供它们进行查询。
现在,从请求接收到的用户名和密码值仅被视为数据,因此不会发生 SQL 注入。
我们来看看修改后的servlet代码。
String query = "select * from tbluser 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()) {
// Login Successful if match is found
success = true;
}
rs.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
stmt.close();
conn.close();
} catch (Exception e) {
}
}
让我们了解一下这个案例中发生了什么。
Query: 从 tbluser 中选择 *,其中用户名=?和密码=?
上述查询中的问号(?)称为位置参数。上面的查询中有 2 个位置参数。我们不会连接用户名和密码来查询。我们使用中可用的方法准备好的声明提供用户输入。
我们通过使用设置了第一个参数stmt.setString(1, username)
和第二个参数使用stmt.setString(2, password)
。底层 JDBC API 负责清理这些值以避免 SQL 注入。
- 在查询中使用数据之前先验证数据。
- 不要使用常用词作为表名或列名。例如,许多应用程序使用 tbluser 或 tblaccount 来存储用户数据。电子邮件、名字、姓氏是常见的列名称。
- 不要直接连接数据(作为用户输入接收)来创建 SQL 查询。
- 使用类似的框架休眠 and Spring数据JPA用于应用程序的数据层。
- 在查询中使用位置参数。如果您使用普通JDBC,然后使用PreparedStatement执行查询。
- 通过权限和授权限制应用程序对数据库的访问。
- 不要向最终用户返回敏感的错误代码和消息。
- 进行适当的代码审查,以免开发人员意外编写不安全的 SQL 代码。
- 使用类似的工具SQLMap查找并修复应用程序中的 SQL 注入漏洞。
这就是 Java SQL 注入的全部内容,我希望这里没有遗漏任何重要的内容。
您可以从下面的链接下载示例 Java Web 应用程序项目。
SQL注入Java项目