SQL注入漏洞详解:产生原因、攻击演示及解决方案(附实战代码)

SQL注入漏洞详解:产生原因、攻击演示及解决方案(附实战代码)

在Java Web开发中,SQL注入是最常见、最危险的安全漏洞之一。它利用开发者编写SQL语句时的疏忽,通过恶意输入篡改SQL逻辑,从而实现越权登录、数据泄露、甚至数据库被篡改等严重后果。本文将结合实战代码,详细拆解SQL注入漏洞的产生前提、攻击效果、底层原因,以及最有效的解决方案,全程贴合实际开发场景,新手也能轻松理解。

一. 什么是SQL注入?核心定义

SQL注入(SQL Injection),简单来说,就是攻击者通过向应用程序输入恶意的SQL语句片段,利用程序对用户输入的未过滤、未验证,将恶意片段拼接进后台执行的SQL语句中,篡改原有SQL的执行逻辑,达到非法操作数据库的目的。

结合我们的实战场景:在已知用户名的情况下,攻击者无需输入正确密码,只需输入特定的恶意字符串,就能绕过登录验证,成功登录系统——这就是最典型的SQL注入攻击。

二. SQL注入漏洞的产生前提(必看)

SQL注入并非随时随地都能发生,它需要满足两个核心前提,缺一不可,这也是我们后续防范漏洞的关键切入点:

已知关键信息:攻击者需要知道系统中的某个有效用户名(比如本文案例中的“aaa”),这是攻击的基础(如果连用户名都未知,注入攻击的难度会大幅提升)。

后台存在SQL语句拼接:这是漏洞产生的核心前提。后台程序没有对用户输入的用户名、密码进行过滤,直接将用户输入的内容拼接进SQL语句中,导致恶意输入被当作SQL代码执行。

三. SQL注入的攻击效果(实战演示)

我们以登录功能为例,结合提供的代码,演示两种最常见的SQL注入攻击方式,看看攻击者如何在“密码任意输入”的情况下,成功登录系统。

先看后台存在漏洞的登录代码(对应代码中login方法):

/**

* 存在SQL注入漏洞的登录方法

* @param username 用户输入的用户名

* @param password 用户输入的密码

* @return 登录结果

*/

public String login(String username,String password){

Connection conn = null;

Statement stmt = null;

ResultSet rs = null;

try {

// 获取数据库连接

conn = JDBCUtils.getConnection();

// 核心问题:直接拼接用户输入和SQL语句

String sql = "select * from t_user where username = '"+username+"' and password = '"+password+"'";

// 创建Statement对象执行SQL

stmt = conn.createStatement();

rs = stmt.executeQuery(sql);

// 有查询结果则登录成功

if(rs.next()){

return "登录成功...";

}else{

return "登录失败了...";

}

} catch (SQLException e) {

e.printStackTrace();

}finally {

JDBCUtils.close(conn,stmt,rs);

}

return null;

}

攻击方式1:输入恶意字符串 “aaa'or'1=1”(用户名)+ 任意密码

攻击者已知用户名是“aaa”,在登录界面输入:

用户名:aaa'or'1=1

密码:任意字符(比如123456、sfsdfsds等,无需正确)

我们来分析拼接后的SQL语句,就能明白为什么能登录成功:

原SQL模板:select * from t_user where username = '"+username+"' and password = '"+password+"'

拼接后实际执行的SQL:

select * from t_user where username = 'aaa'or'1=1' and password = 'sfsdfsds'

核心分析:

SQL中 '1=1' 是一个永远为“真”的条件(恒成立);

原SQL的逻辑是“用户名等于输入值 并且密码等于输入值”,拼接后变成“用户名等于aaa 或者 1=1(恒真) 并且 密码等于任意值”;

根据SQL的逻辑优先级,“and”优先级高于“or”,1=1(恒真) 并且 密码等于任意值这两个条件结合结果为“假”,'aaa'或者假的结果为真(因为用户名正确),这样就会导致整个WHERE条件最终为“真”;

SQL会查询出t_user表中的所有数据,后台判断有查询结果,就返回“登录成功”——攻击者无需正确密码,成功登录。

攻击方式2:输入恶意字符串 “aaa'-- ”(用户名)+ 任意密码

这是另一种更简洁的注入方式,攻击者输入:

用户名:aaa'-- (注意:-- 后面有一个空格,这是SQL的注释符号)

密码:任意字符

拼接后实际执行的SQL:

select * from t_user where username = 'aaa'-- '' and password = 'sfsdfsdfs'

核心分析:

SQL中 -- 是注释符号,注释符号后面的所有内容都会被数据库忽略;

拼接后,-- 后面的 “'' and password = 'sfsdfsdfs'” 被全部注释,相当于SQL变成:select * from t_user where username = 'aaa';

只要用户名“aaa”存在,SQL就会查询到对应数据,后台判断登录成功——同样无需正确密码,实现越权登录。

四. SQL注入漏洞的产生原因(底层剖析)

通过上面的演示,我们能明确:SQL注入漏洞的根本原因,就是后台程序对用户输入的内容未做任何过滤和验证,直接将用户输入拼接进SQL语句中。

具体拆解为两点,结合代码更易理解:

SQL语句拼接的弊端:代码中使用 String sql = "select ... where username = '"+username+"' and password = '"+password+"'" 这种拼接方式,用户输入的内容会直接成为SQL语句的一部分。如果用户输入的是恶意SQL片段(比如'or'1=1、-- 等),就会篡改原有SQL的逻辑。

未过滤特殊字符:对于SQL中的特殊字符(比如单引号'、注释符号--、逻辑运算符or/and等),后台程序没有进行转义或过滤,导致这些特殊字符被当作SQL代码的一部分执行,而不是当作普通的用户输入数据。

简单来说:程序把“用户输入的数据”当成了“SQL代码”来执行,这就是SQL注入的本质。

五. SQL注入漏洞的解决方案(终极方案)

解决SQL注入的核心思路是:禁止SQL语句拼接,将用户输入的内容与SQL语句的逻辑分离,让用户输入的内容永远只作为“数据”,而不是“SQL代码”执行。

在Java中,最标准、最有效的解决方案是:使用PreparedStatement接口,它是Statement接口的子接口,核心优势是“预编译SQL语句”,能从根源上杜绝SQL注入。

1. PreparedStatement的核心原理

预编译功能:先将SQL语句的“骨架”(固定逻辑)发送到MySQL服务器端进行编译,编译后的SQL语句格式被固定,无法再被篡改。

占位符替代参数:SQL语句中需要传入用户输入的部分,用 ? 作为占位符(比如 select * from t_user where username = ? and password = ?),而不是直接拼接用户输入。

参数单独传入:编译完成后,再将用户输入的内容作为“参数”,通过专门的方法传入占位符,数据库会自动将参数当作普通数据处理,不会解析成SQL代码。

2. 核心方法(必掌握)

创建PreparedStatement对象:通过Connection接口的 prepareStatement(String sql) 方法创建,参数是带占位符的SQL语句(预编译SQL)。

给占位符赋值:通过PreparedStatement的setXxx()方法给 ? 赋值,核心方法如下:

setString(int parameterIndex, String x):给指定位置的占位符赋值(字符串类型),parameterIndex是占位符的位置(从1开始,不是从0开始)。

setInt(int parameterIndex, int x):给占位符赋值(int类型)。

setObject(int parameterIndex, Object x):通用赋值方法,可适配任意数据类型。

执行SQL语句:

executeQuery():执行查询类SQL(select),无需传入SQL语句(因为已经预编译完成)。

executeUpdate():执行增删改类SQL(insert、update、delete),同样无需传入SQL语句。

实战代码(解决SQL注入,对应login2方法)

/**

* 采用预编译方式,解决SQL注入漏洞的登录方法

* @param username 用户输入的用户名

* @param password 用户输入的密码

* @return 登录结果

*/

public String login2(String username,String password){

Connection conn = null;

PreparedStatement stmt = null; // 预编译SQL执行对象

ResultSet rs = null;

try {

// 1. 获取数据库连接

conn = JDBCUtils.getConnection();

// 2. 编写带占位符的SQL语句(骨架固定)

String sql = "select * from t_user where username = ? and password = ?";

// 3. 预编译SQL语句,创建PreparedStatement对象

stmt = conn.prepareStatement(sql);

// 4. 给占位符赋值(用户输入的内容仅作为数据传入)

stmt.setString(1, username); // 第1个? 赋值为username

stmt.setString(2, password); // 第2个? 赋值为password

// 5. 执行SQL(无需传入SQL语句,已预编译)

rs = stmt.executeQuery();

// 6. 判断登录结果

if(rs.next()){

return "登录成功...";

}else{

return "登录失败了...";

}

} catch (SQLException e) {

e.printStackTrace();

}finally {

// 关闭资源

JDBCUtils.close(conn,stmt,rs);

}

return null;

}

4. 效果验证(彻底杜绝注入)

当我们使用login2方法,再输入之前的恶意字符串时,注入攻击会彻底失效:

输入用户名:aaa'or'1=1,密码:任意字符;

预编译后,SQL的骨架 select * from t_user where username = ? and password = ? 被固定;

用户输入的“aaa'or'1=1”会被当作普通字符串,赋值给第一个占位符,不会被解析成SQL逻辑;

数据库会查询“username等于'aaa'or'1=1'”的用户,而这种用户名并不存在,因此返回“登录失败”,注入攻击失效。

六、核心总结(必背)

SQL注入是什么:攻击者通过恶意输入,篡改后台拼接的SQL语句逻辑,实现越权操作(如无密码登录)。

产生原因:后台程序直接拼接用户输入和SQL语句,未过滤特殊字符,导致用户输入被当作SQL代码执行。

攻击前提:已知有效用户名 + 后台存在SQL拼接。

解决方案:使用PreparedStatement预编译SQL,用?占位符替代参数拼接,将用户输入作为数据传入,从根源杜绝注入。

关键区别:Statement(拼接SQL,有注入漏洞) vs PreparedStatement(预编译+占位符,无注入漏洞),实际开发中必须使用PreparedStatement。

完整代码

import cn.qcby.utils.JdbcUtils;

import java.sql.*;

/**

* 演示SQL注入的问题,漏洞

* 在已知用户名的情况下,通过sql语言关键字,登录系统。

* SQL注入产生原因是SQL语句的拼接,利用SQL关键字(or和and的优先级问题)产生效果。

* 需要解决SQL注入的问题

*

* 解决SQL注入问题,采用SQL语句预编译的方式,把SQL语句中的参数使用 ? 占位符来表示,先把SQL语句编译,格式固定的。

* 再给 ? 传入值,传入任何内容都表示值。数据库会判断SQL执行的结果。

*/

public class JdbcTest4 {

public static void main(String[] args) {

/*

// 模拟登录的功能,有SQL注入的问题*/

// String result = new JdbcTest4().login("aaa'or'1=1", "123");

// System.out.println(result);

String result = new JdbcTest4().login2("aaa'or'1=1", "12346546464");

System.out.println(result);

}

/**

* 采用预编译的方式,解决SQL注入的问题

* @param username

* @param password

* @return

*/

public String login2(String username,String password){

Connection conn = null;

// 预编译执行SQL语句对象

PreparedStatement stmt = null;

ResultSet rs = null;

try {

// 获取到连接

conn = JDBCUtils.getConnection();

// 使用?占位符

String sql = "select * from t_user where username = ? and password = ?";

// 预编译SQL语句,把SQL语句固定

//Statement statement = conn.createStatement();

//statement不能防止sql注入问题 prepareStatement (有占位符)能够防止sql注入问题

stmt = conn.prepareStatement(sql);

// 需要给 ? 设置值

stmt.setString(1,username);

stmt.setString(2,password);

// 执行SQL语句

rs = stmt.executeQuery();

// 遍历数据

if(rs.next()){

// 表示存在数据,如果存在,说明用户名和密码编写正确

return "登录成功...";

}else{

return "登录失败了...";

}

} catch (SQLException e) {

e.printStackTrace();

}finally {

JDBCUtils.close(conn,stmt,rs);

}

return null;

}

/**

* 模拟登录的功能,通过用户名和密码从数据库中查询

* @param username

* @param password

* @return

*/

public String login(String username,String password){

Connection conn = null;

Statement stmt = null;

ResultSet rs = null;

try {

// 获取到连接

conn = JDBCUtils.getConnection();

// 编写SQL语句的拼接 '1=1' true password = '1234sdfsce' false true and false 整体上false,前面的用户名'aaa'or false,用户名正确,所以即使密码错误也能登录

// String sql = "select * from t_user where username = 'aaa' or '1=1' and password = '1234sdfsce'";

// String sql = "select * from t_user where username = 'aaa' or false";

String sql = "select * from t_user where username = '"+username+"' and password = '"+password+"'";

// 执行sql

stmt = conn.createStatement();

// 执行

rs = stmt.executeQuery(sql);

// 遍历数据

if(rs.next()){

// 表示存在数据,如果存在,说明用户名和密码编写正确

return "登录成功...";

}else{

return "登录失败了...";

}

} catch (SQLException e) {

e.printStackTrace();

}finally {

JDBCUtils.close(conn,stmt,rs);

}

return null;

}

}

相关探索