什么是JDBC
Java 数据库连接 ( JDBC ) 是编程语言Java的应用程序编程接口(API) ,它定义了客户端如何访问数据库。它是一种基于 Java 的数据访问技术,用于 Java 数据库连接。它是Oracle Corporation的Java Standard Edition平台的一部分。它提供了查询和更新数据库中数据的方法,并且面向关系数据库。JDBC 到ODBC(开放式数据库连接)桥允许连接到Java 虚拟机(JVM) 主机环境中任何 ODBC 可访问的数据源。
什么是API
应用程序接口(英语:Application Programming Interface[1]),缩写为API[2],是一种计算接口,它定义多个软件中介之间的交互,以及可以进行的调用(call)或请求(request)的种类,如何进行调用或发出请求,应使用的数据格式,应遵循的惯例等。它还可以提供扩展机制,以便用户可以通过各种方式对现有功能进行不同程度的扩展[3]。一个API可以是完全定制的,针对某个组件的,也可以是基于行业标准设计的以确保互操作性。通过信息隐藏,API实现了模块化编程,从而允许用户实现独立地使用接口。
注入思路
直接使用 JDBC 的场景,如果代码中存在拼接 SQL 语句,那么很有可能会产生注入
环境搭建
首先使用idea搭建一个可供测试的环境,如果读者不太熟悉idea的使用可参考此教程,教程图文并茂,较为详细
我们通过模拟用户登录,来验证JDBC注入
java环境源代码
```
package com.jbdc_demo;
import java.sql.*;
import java.util.Scanner;
public class TestLogin {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入用户名:");
String userName = scanner.nextLine();
System.out.print("请输入密码:");
String password = scanner.nextLine();
//1.加载驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2.获取连接对象,连接数据库
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/temp?useUnicode=true&characterEncoding=utf8", "root", "root");
//3.创建执行SQL语句的对象
Statement statement = connection.createStatement();
//4.编写SQL语句,并执行SQL语句
String sql = "select * from user where username = '" + userName + "' and password = '" + password + "'";
ResultSet resultSet = statement.executeQuery(sql);
//5.处理结果
if (resultSet.next()) {//通过参数,查到一行数据,就提示用户的登陆成功!
System.out.println("登录成功!");
} else {
System.out.println("登录失败!");
}
//6.释放资源
resultSet.close();
statement.close();
connection.close();
}
}
```
数据库
```
创建数据库
CREATE DATABASE temp CHARACTER SET utf8;
使用该数据库
USE temp;
创建用户表
CREATE TABLE user (
id INT PRIMARY KEY auto_increment,
username CHARACTER(20) NOT NULL,
password CHARACTER(20) NOT NULL,
phone CHARACTER(11)
) charset = utf8;
初始化(插入)表中数据
INSERT INTO user (username, password, phone) VALUES ('ziph', '123456', '16688889999');
INSERT INTO user (username, password, phone) VALUES ('zhangsan', '123456', '16644445555');
```
搭建好的数据库
运行java程序
先输入正确的数据
再替换为注入语句
这次我们没有使用正确的语句,但是却登录成功了
为什么会登录成功
- 因为用户拼接的语句被当做命令执行了
Java代码分析
首先分析一下Java代码
前面的数据库连接就不再赘述了,直接分析关键语句
```
String sql = "select * from user where username = '" + userName + "' and password = '" + password + "'";
ResultSet resultSet = statement.executeQuery(sql);
if (resultSet.next()) {//通过参数,查到一行数据,就提示用户的登陆成功!
System.out.println("登录成功!");
} else {
System.out.println("登录失败!");
}
```
- 可以看到,这里对用户的输入没有任何的过滤,只要可以拼凑,把前面的单引号闭合,注释掉后面的部分就可以尽情地执行自己的sql语句
- sql语句查询结束之后,会返回查询结果,而resultset.next是默认指向第一行的前面,每调用一次就会向下挪动一位,当初次调用之后,就会挪动到查询结果的第一行,这个时候布尔值是ture的。所以就会println登录成功
sql语句分析
```
//输入的数据如下
请输入用户名:llll' or 1=1 ;#
请输入密码:llll
//真正构造的sql语句
"select * from user where username = 'llll' or 1=1 ;
' and password = 'llll'";
这个时候真正有效的就是
select * from user where username = 'llll' or 1=1 ;
后面的“' and password = 'llll'";”已经被注释掉
```
- 真正有效的部分是select * from user where username = 'llll' or 1=1 ;
- 后面的“' and password = 'llll'";”已经被#注释掉
- or运算符——如果第一个条件和第二个条件中只要有一个成立,则 OR 运算符显示一条记录。
- 在"where username = 'llll' or 1=1 "这样的筛选语句中,username='llll'和1=1是属于同一级的,也就是说,这两个语句有一个是正确的就可以正常的查询**
- 而显然的是,表中没有username=llll的字段,所以这个时候语句为select * from user where 1=1 ;
- 但是表中也没有字段为1的,所以就有语句select * from user
1=1在SQL查询中的作用
where 1=1 是为了避免where 关键字后面的第一个词错误(and 或者 or)而导致语法错误。
and的例子
```
String sql="select * from table_name where 1=1";
if( conditon 1) {
sql=sql+" and var2=value2";
}
if(conditon 2) {
sql=sql+" and var3=value3";
}
```
or的例子
select * from user where username = 'llll' or 1=1 ;
1=1为什么可以查询所有数据
因为表中没有为1的字段,所以等同于查询所有数据
resultset.next
简单理解一下,resultset.next返回的是一个布尔值,且resultset就像一个指针一样,指向第一行之前,每调用一次向下挪动一词
=============================分割线============================
那么,如何避免SQL注入问题?
- 把用户的输入只作为参数运行即可
PreparedStatement
我们先来了解一下PreparedStatement
PreparedStatement是什么
PreparedStatement 是一个特殊的Statement对象
PreparedStatement的特点
- 简化Statement中的操作
- 提高执行语句的性能
- 可读性和可维护性更好
- 安全性更好。
Statement 和 PreparedStatement之间的关系和区别
- 关系:PreparedStatement继承自Statement,都是接口
- 区别:PreparedStatement可以使用占位符,是预编译的,批处理比Statement效率高
如何使用PreparedStatement
使用PreparedStatement主要有两点,一个是参数标记,另一个是动态参数绑定
参数标记
//3.创建执行sql语句的对象
String sql = "select * from user where username = ? and password = ?";
//预编译SQL语句————提前把SQL语句编译为字符串,其中用到了转义字符防止个别符号注入SQL
PreparedStatement preparedStatement = connection.prepareStatement(sql);
动态参数绑定
//为占位符下标赋值
preparedStatement.setString(1, username);
preparedStatement.setString(2, password);
接着,祭出代码,接住实际的结果来理解
java代码
```
package com.jbdc_demo;
import java.sql.*;
import java.util.Scanner;
public class TestSafeLogin {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入用户名:");
String username = scanner.nextLine();
System.out.print("请输入密码:");
String password = scanner.nextLine();
//1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2.获取连接对象
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/temp?useUnicode=true&characterEncoding=utf8","root", "root");
//3.创建执行sql语句的对象
String sql = "select * from user where username = ? and password = ?";
//预编译SQL语句————提前把SQL语句编译为字符串,其中用到了转义字符防止个别符号注入SQL
PreparedStatement preparedStatement = connection.prepareStatement(sql);
/**
* 查看预编译后的SQL语句字符串
* 此查看不计为jdbc的开发步骤中
*/
System.out.println(preparedStatement);
//为占位符下标赋值
preparedStatement.setString(1, username);
preparedStatement.setString(2, password);
/**
* 查看赋值后SQL语句的字符串
* 此查看不计为jdbc的开发步骤中
*/
System.out.println(preparedStatement);
//4.执行SQL语句————此时executeQuery()不需要再传入参数
ResultSet resultSet = preparedStatement.executeQuery();
//5.处理结果
if (resultSet.next()) {//通过参数,查到一行数据,提示用户登录成功!
System.out.println("登陆成功!");
} else {
System.out.println("登录失败!");
}
//6.释放资源
resultSet.close();
preparedStatement.close();
connection.close();
}
}
```
再次进行测试
先看一下实际运行情况
同样的注入语句,这里失败了,是因为用户的输入被作为参数了,没有被当时做指令来执行。
那么为什么这个时候就失败了?
先分析一下语句的作用
当使用PreparedStatement的时候,程序会对sql语句进行预编译,
//3.创建执行sql语句的对象
String sql = "select * from user where username = ? and password = ?";
//预编译SQL语句————提前把SQL语句编译为字符串,其中用到了转义字符防止个别符号注入SQL
PreparedStatement preparedStatement = connection.prepareStatement(sql);
用户未输入数据的时候,sql语句是这样的
select * from user where username = ** NOT SPECIFIED ** and password = ** NOT SPECIFIED **
其中, NOT SPECIFIED 就是之前占位符(?)的作用所在
输入数据之后,便有sql语句如下
select * from user where username = 'lisi' and password = '123456'
现在回到我们的案例当中来,根据以上我们可以得知,PreparedStatement会把用户的输入作为一个参数来处理
也就是说,尽管我们可以拼接了一个语句,但是在程序看来,那就是一个参数而已
如下:
select * from user where username = 'llll '' or 1=1 ;#' and password = 'llll'
到这里可以清楚的知道,是因为把用户的输入作为参数以及对特殊字符进行处理来达到的预防sql注入的目的。
PreparedStatement是如何避免注入问题的?
1、占位符、预处理
2、对特殊字符进行处理
通过占位符来达到用户的输入只作为数据,恪守代码和数据分离的原则,又通过对用户输入的特殊字符转义或者补全等方式来规避注入。
参考及扩展:
How does a PreparedStatement avoid or prevent SQL injection?
How to Fix SQL Injection Using Java PreparedStatement & CallableStatement
一、mimikatz简介 mimikatz是法国人Gentil Kiwi编写的一款windows平台下的神器,它具备很多功能,其中最亮的功能是直接从lsass.exe进程里获取windows处于active状态账号的明文密码或hash散列值。 mimikatz…
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论