准则

SQL 注入

是时候看看 SQL 注入了。在很长一段时间里,SQL 注入是 OWASP 十大漏洞中无可争议的王者,连续多年都是如此。尽管 SQL 注入已经有 20 多年的历史,尽管它在榜单上的排名略有下降,但它仍然是一个非常流行和危险的漏洞。 

作为一种网络安全漏洞,SQL 注入(SQLi)仍然是攻击者最常用的 "黑客 "技术之一,因为它允许攻击者操纵数据库并从中提取关键信息。更令人担忧的是,攻击者可以让自己成为数据库服务器的管理员,并做出一些真正具有破坏性的事情,如破坏数据库、操纵事务、泄露数据,并使其容易受到更多问题的攻击。

让我们快速了解一下它是如何发生的

SQL(或结构化查询语言)是用于与关系数据库通信的语言;它是开发人员、数据库管理员和应用程序用来管理每天产生的海量数据的查询语言。

在应用程序中存在两个上下文:一个是数据上下文,另一个是代码上下文。代码上下文告诉计算机要执行什么,并将其与要处理的数据分开。当攻击者输入的数据被 SQL 解释器误认为是代码时,就会发生 SQL 注入,使攻击者得以从应用程序中收集有价值的信息。 

SQL 注入攻击的影响

SQL 注入对任何网络应用程序都极为有害,而且是许多高知名度漏洞背后的首选技术,因为它能让攻击者在未经授权的情况下访问关键数据。他们可以看到大量信息,从用户名和密码到信用卡详细信息和个人身份号码。 

在获得这些数据后,攻击者可以接管账户、重置密码、进行长时间的网上购物或实施其他(更糟糕的)欺诈行为。 

但 SQLi 最令人担忧的地方可能是,攻击者可以在不被发现的情况下,长期保持一个后门进入系统。可以想象,无论后门打开多久,都会导致重复的数据泄露。这太可怕了。 

让我们来看几个例子,以便更好地理解其实际效果。

SQLi 示例

SQLi 包括各种可应对不同情况的漏洞技术。以下是一些最常见的 SQLi 例子:

技术 说明
检索隐藏数据 利用这种技术,攻击者可以修改任何 SQL 查询,从数据库中收集更多信息。
数据检查 攻击者可以提取有关数据库版本和结构的信息,这有助于他们利用更多信息。这种技术可能因数据库而异。
工会攻击 攻击者可以提取有关数据库版本和结构的信息,这有助于他们利用更多信息。这种技术可能因数据库而异。
盲 SQLi 利用 Blind SQLi,攻击者可以在数据库中实现查询。但问题是,攻击者控制着这个查询,而且它不会在应用程序的响应中返回任何结果。
颠覆应用逻辑 攻击者会干扰或操纵查询,从而破坏应用程序的逻辑。要篡改查询,攻击者可以将 SQL 注释序列"--"和 WHERE 子句结合起来。

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 "库提供了一种通过第二个参数为查询设置参数的方法。这包括一个按名称或索引作为参数绑定到查询的值列表。

await sequelize.query(
    'SELECT * FROM Post WHERE state = $state AND author = $author',
    {
        bind: { state: state, author: author},
        type: QueryTypes.SELECT
    }
);
await sequelize.query(
    'SELECT * FROM Post WHERE state = $1 AND author = $2',
    {
        bind: [state, author],
        type: QueryTypes.SELECT
    }
);