
注入式SQL
是时候谈谈SQL注入了。它曾长期稳居OWASP十大漏洞榜首,这一霸主地位持续了多年。尽管这种漏洞已存在近二十年,且在该榜单中的排名略有下滑,但它依然是极其普遍且危险的漏洞。
作为一种网络安全漏洞,SQL注入(SQLi)仍是攻击者最常用的"入侵"技术之一,因为它能让攻击者操控数据库并提取关键信息。 更令人担忧的是,攻击者可冒充数据库服务器管理员实施破坏性操作,例如摧毁数据库、篡改交易记录、泄露敏感数据,并使系统暴露于其他安全风险之中。
让我们看看事情是如何发展的
SQL(结构化查询语言)是用于与关系型数据库通信的语言;它是开发人员、数据库管理员和应用程序管理每日生成海量数据时所使用的查询语言。
在应用程序中存在两种上下文:一种用于数据,另一种用于代码。代码上下文指示计算机应执行哪些操作,并将其与待处理的数据分离。 当攻击者输入的数据被SQL解释器错误地当作代码处理时,就会发生SQL注入,从而使其能够从应用程序中获取有价值的信息。
SQL注入攻击的影响
SQL注入对任何Web应用都可能极其危险,它曾是众多备受瞩目的安全漏洞背后的首选攻击手段,因为它能让攻击者未经授权就获取关键数据。攻击者可窃取大量敏感信息,包括用户名和密码、信用卡详细信息以及个人身份号码。
在获取这些数据后,攻击者可接管账户、重置密码、进行持续性网络购物,或实施其他(更为恶劣的)欺诈行为。
但SQL注入最令人担忧之处或许在于:若攻击者未被察觉,便能在系统中长期维持后门通道。正如您所想象的,只要后门保持开启状态,数据泄露就会持续发生。这实在令人毛骨悚然。
让我们通过几个实例来更深入地了解其运作方式。
SQL注入示例
SQL注入包含多种漏洞利用技术,可应对不同场景。以下是一些最常见的SQL注入示例:
SQL注入类型
好,现在我们来看三种不同的SQL注入类型。
SQLi 带内
这是最常见、最简单且最有效的SQL注入类型之一。此类攻击中,攻击者与获取结果均通过同一通信通道进行。
两种类型的带内SQL注入攻击如下:
- 基于UNION的SQL注入——该攻击利用UNION运算符组合两个或多个SQL查询(如SELECT语句),以获取所需信息并返回HTTP GET响应。
- 基于错误的SQL注入—— 攻击者利用数据库的错误信息来理解其结构。 在此类攻击中,攻击者可发送虚假请求或执行特定操作,诱使服务器显示错误信息,从而获取数据库中的信息。 因此开发人员必须避免在生产环境中直接输出错误信息或记录日志,相关信息应存储在受限访问的区域中。
推断性SQL
推断式或盲注SQL注入攻击更为复杂,其实施过程可能耗时更长。此外,攻击者无法立即获得攻击结果,这使得该攻击具有盲注特性。
攻击者通过HTTP请求向数据库服务器发送有效负载,以重构用户数据库,随后观察应用程序的响应及行为,从而判断攻击是否成功。
存在两种类型的推断式SQL注入攻击:
- 基于布尔值的盲注SQL注入——此类攻击中,攻击者向数据库发送查询以获取布尔值结果(真或假),并通过观察HTTP响应来推测布尔值结果。
- 基于时间的盲注SQL注入——在此攻击中,攻击者向数据库发送请求,使其延迟数秒再返回响应,攻击者通过HTTP请求的响应时间来评估查询结果。
SQL注入(非内联)
这是一种较为罕见的SQL注入攻击类型,其成功与否取决于数据库服务器启用的功能特性。当攻击者无法有效利用其他攻击方式时,此类攻击便可能发生。
例如当攻击者无法使用相同通信通道进行同频段攻击,或HTTP响应不够清晰以致无法计算查询结果时。
此外,由于该攻击高度依赖数据库服务器能否执行HTTP或DNS查询以向攻击者发送所需数据,故实际应用中并不常见。
如何防御SQL注入攻击
幸运的是,由于SQL注入的危害性由来已久且普遍存在,我们已具备多种防范手段。采用此类预防技术不仅是良好的编码习惯,更能有效增强组织在SQL注入攻击方面的防御能力。
保护数据库服务器免受此类攻击的方法有多种,例如:输入验证、部署Web应用防火墙(WAF)、加强数据库安全、借助第三方安全团队或系统,以及编写无漏洞的SQL查询语句。
让我们看一个在Python中使用上述安全措施之一来防止SQL注入的示例。
Python示例
在此示例中,攻击者将使用布尔型盲SQL注入来获取系统中的重要信息。
Python:存在漏洞
假设数据库中存在一个名为「sample_data」的表。该表存储了应用程序用户的用户名和密码。
现在允许用户使用以下命令在此数据库表中查找值:
导入 mysql.connector
数据库 = mysql.connector.connect
注意:#Bad。请避免这样做!这仅用于学习。
(host="localhost", user="newuser", passwd="pass", db="sample")
cur = db.cursor()
name = raw_input('请输入姓名:')
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;这样的内容,将对数据库造成重大影响。
Python:修复
为防止攻击发生,SQL指令需按以下方式修改:
cur.execute("SELECT * FROM SAMPLE_DATA WHERE Name = %s;", (name,))
从现在起,系统将把用户输入视为字符串处理,即使用户试图注入SQL查询,系统也会仅将用户输入视为名称的值。
此项简单修改可阻止后续请求中的恶意活动,并保护系统免受用户输入攻击。
Java示例
在此示例中,我们还将使用名为「sample_data」的数据库表,该表存储应用程序的用户数据。
一个基本的登录页面使用用户名和密码,并通过Java文件(即LoginServlet)将其与数据库进行验证,从而实现登录操作。
Java:易受攻击的示例
通过使用数据库中的「sample_data」表,系统允许用户使用其凭据作为输入执行连接操作。
LoginServlet文件包含一个适用于登录操作的请求,即:
//Mauvais exemple. N'utilisez pas la concaténation de chaînes.
String query = « select * from sample_data where username=' » + username + « 'and password =' » + password + « '» ;
Connexion conn = null ;
Déclaration stmt = null ;
essayez {
conn = DriverManager.getConnection (« jdbc:mysql : //127.0.0. 1:3306 /user », « root ») ;
stmt = conn.createStatement () ;
ResultSet rs = STMT.ExecuteQuery (requête) ;
si (rs.next ()) {
//Connexion réussie si une correspondance est trouvée
succès = vrai ;
}
} catch (Exception e) {
e. printStackTrace () ;
} enfin {
essayez {
stmt.fermer () ;
conn.close () ;
} catch (Exception e) {}
}
si (succès) {
response.sendRedirect (» home.html «) ;
} autre {
Response.sendRedirect (» login.html ? erreur = 1") ;
}
}
以下是用户登录请求:
在 sample_data 中选择 *,其中 用户名 = 用户名 = 用户名 且 密码 = 密码
注入式SQL
如果输入有效,系统将完美运行。例如,假设用户名仍是Alicia,密码为secret。
系统将返回包含这些凭据的用户数据。然而,攻击者可利用Postman和cURL工具篡改用户请求以实施SQL注入攻击。
例如,黑客可以发送一个虚构的用户名(Alicia)和密码“or '1'='1'”。
在这种情况下,用户名和密码将不匹配,但条件“1'='1”仍将成立,从而使登录操作成功。
Java:预防
出于预防目的,我们需要修改LoginValidation代码,将执行查询时使用的Statement替换为PreparedStatement。此项修改将阻止在查询中拼接用户名和密码,并将其作为参数处理,从而避免SQL注入攻击。
以下是修改后的LoginValidation代码:
String query = « select * from sample_data where username= ? et mot de passe = ? » ;
Connexion conn = null ;
PreparedStatement stmt = null ;
essayez {
conn = DriverManager.getConnection (« jdbc:mysql : //127.0.0. 1:3306 /user », « root ») ;
stmt = Conn.PrepareStatement (requête) ;
STMT.setString (1, nom d'utilisateur) ;
STMT.setString (2, mot de passe) ;
ResultSet rs = STMT.executeQuery () ;
si (rs.next ()) {
succès = vrai ;
}
rs.fermer () ;
} catch (Exception e) {
e. printStackTrace () ;
} enfin {
essayez {
stmt.fermer () ;
conn.close () ;
} catch (Exception e) {
}
}
在这种情况下,PreparedStatement、设置器和底层的JDBC API将负责处理用户输入,并防止SQL注入。

示例
接下来我们将考察其他语言中的若干示例,以便更深入地理解其在实际应用中的表现。
C# - 非安全
此示例因使用FromRawSQL方法而不安全。该方法既不绑定参数也不进行转义处理。因此,必须绝对避免使用此方法。
var blogs = context.POSTS
.fromRawSQL (« SÉLECTIONNEZ * PARMI LES ARTICLES OÙ state = {0} ET author = {1} », state, author)
.toList () ;
C# - 安全
此示例通过FromSQLInterpolated实现安全处理,该方法将插值后的值进行参数化处理。
尽管这通常是安全的,但它可能与不安全的FromRawSQL非常相似。
var blogs = context.POSTS
.fromSQLInterpolated ($"SÉLECTIONNEZ * PARMI LES ARTICLES OÙ state = {state} ET author = {author} »)
.toList () ;
Java - 安全:Hibernate - 命名查询 + 本地查询
Hibernate 提供了两种安全构建查询的方法:通过其“原生查询”和“命名查询”。这两种方法都允许指定参数的位置。
@NamedNativeQuery (
name = « find_post_by_state_and_author »,
requête =
« SÉLECTIONNEZ *" +
« DE LA POSTE » +
« WHERE state =:state » +
« ET auteur = : auteur »,
Classe de résultats = Post.class)
java
Liste des <Post>messages = Session.createNativeQuery (
« SÉLECTIONNEZ *" +
« DE LA POSTE » +
« WHERE state =:state » +
« ET auteur = : auteur »)
.Ajouter une entité (Post.class)
.setParameter (« état », état)
.setParameter (« auteur », auteur)
.liste () ;
Java - 安全:jplq
通过在jplq存储库接口上注解`Query`属性,它们可以呈现多种形式并支持参数化配置。
@Query("SELECT p FROM MESSAGE p WHERE u.state = ?1 AND u.author = ?2 IN")
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 MESSAGE WHERE STATE = 1$ AND AUTHOR = 2$ », [state, author])
JavaScript - 安全:Sequelize
Sequelize库提供了一种通过第二个参数设置查询的方式,该参数用于接收查询参数。这包括一个用于将值绑定到查询作为参数的列表,绑定方式可以是按名称或按索引。