文章前言
近期在对公司协同源代码进行审计的过程中发现一个很奇怪的点就是很多研发人员认为对于JDBC类的SQL语句防注入只需要使用PreparedStatement即可实现对SQL注入的有效防御,而不管被带入PreparedStatement的SQL语句之前是否有过拼接,这一点使得我PreparedStatement对SQL语句处理过程产生了好奇心以及对JDBC类SQL注入有效防御方法探索验证的冲动
注入简介
SQL(Structured Query Language)是具有数据操纵和数据定义等多种功能的数据库语言,当我们在与动态网站进行交互(例如:点击、搜索、登录等)的过程中其实后端都有涉及到与之相对应的增删改查操作(INSERT、DELETE、UPDATE、SELECT),而由于数据的增删改查操作都少不了对操作数据对象的检索且查询条件大多数情况下用户都可以掌控(除去少数初始化或者写死的SQL等特殊场景外),故而当用户可以直接控制SQL语句时便可以直接向SQL语句中注入恶意查询语句并实现对SQL语句的重构(需要使SQL语句合法可执行),使得SQL语句在后端执行并查询攻击者想要查询的数据信息,从而产生SQL注入,用一言以蔽之即为:攻击者通过对参数进行测试挖掘可控的注入点,之后将恶意SQL语句插入并完成对SQL语句的重构后在后段被带入数据库查询获取攻击者想要的数据库信息
执行语句
Statement
Statement是Java执行数据库操作的一个重要接口,用于在已经建立数据库连接的基础上向数据库发送要执行的SQL语句,Statement对象主要用于执行不带参数的简单SQL语句,Statement主要通过executeUpdate方法向数据库发送增、删、改的SQL语句,executeUpdate执行完后将会返回一个整数(即增删改语句导致了数据库几行数据发生了变化),常见的增删改查示例如下:
CRAU操作- create
使用executeUpdate(String sql)方法完成数据添加操作:
Statement st = conn.createStatement();
String sql = \\\"insert into user(...) values(...)\\\";
int num = st.executeUpdate(sql);
if(num>0){
System.out.println(\\\"添加成功!\\\");
}
CRUD操作- delete
使用executeUpdate(String sql)方法完成数据删除操作:
Statement st = conn.createStatement();
String sql =\\\"delete from user where id=101\\\";
int num = st.executeUpdate(sql);
if(num>0){
System.out.println(\\\"删除成功!\\\");
}
CRUD操作- update
使用executeUpdate(String sql)方法完成数据修改操作:
Statement st = conn.createStatement();
String sql =\\\"update user set set name =\\\'al1ex\\\' where name=\\\'liuwei\\\'\\\";
int num = st.executeUpdate(sql);
if(num>0){
System.out.println(\\\"修改成功!\\\");
}
CRUD操作- read
使用executeQuery(String sql)方法完成数据查询操作:
Statement st = conn.createStatement();
String sql =\\\"select * from user where id=101\\\";
ResultSet rs = st.executUpdate(sql);
while(rs.next()){
......
}
PreparedStatement
PreparedStatement是java.sql包中的接口,它继承了Statement并与之在两方面有所不同:
-
PreparedStatement实例包含已编译的SQL语句,包含于PreparedStatement对象中的SQL语句可具有一个或多个IN参数,IN参数的值在SQL语句创建时未被指定,相反的该语句为每个IN参数保留一个问号(\\”?\\”)作为占位符,每个问号的值必须在该语句执行之前通过适当的setXXX方法来提供
-
PreparedStatement继承了Statement的所有功能,另外它还添加了一整套方法用于设置发送给数据库以取代IN参数占位符的值,同时三种方法executeQuery和execute、executeUpdate已被更改并使之不再需要参数,这些方法的Statement形式(接受SQL语句参数的形式)不应该用于PreparedStatement对象
环境搭建
数据库类
使用phpstudy来提供MySQL服务并创建一个数据库和数据表
编写一下mysql.sql文件并导入
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
create table admin(
name varchar(32) not null unique,
pwd varchar(32) not null default \\\'\\\'
)character set utf8;
insert into admin values(\\\'al1ex\\\',\\\'123456\\\');
测试工程
新建一个工程:
模拟用户登录操作:
package com.al1ex;
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;
import java.util.Scanner;
public class Student {
public static void main(String[] args) throws IOException, ClassNotFoundException, SQLException {
Scanner scanner = new Scanner(System.in);
//让用户输入管理员用户名和密码
System.out.print(\\\"请输入管理员名字:\\\");
/**
* 说明:如果希望看到注入效果,这里需要用nextLine,用next不行
* next():当接收到空格或者单引号(\\\')就表示结束,我们输入用户名可能空格,那么就是报错
* 而nextLine()回车才算结束
*/
String admin_name=scanner.nextLine();
System.out.print(\\\"请输入管理员密码:\\\");
String admin_pwd=scanner.nextLine();
Properties properties = new Properties();
properties.load(new FileInputStream(\\\"C:\\\\\\\\Users\\\\\\\\Al1ex\\\\\\\\Desktop\\\\\\\\SqlInjection\\\\\\\\src\\\\\\\\jdbc.properties\\\"));
//获取相关的值
String user = properties.getProperty(\\\"user\\\");
String password = properties.getProperty(\\\"password\\\");
String driver = properties.getProperty(\\\"driver\\\");
String url = properties.getProperty(\\\"url\\\");
//1.注册驱动
Class.forName(driver);
//2.得到连接
Connection connection = DriverManager.getConnection(url, user, password);
//3.得到Statement
Statement statement = connection.createStatement();
//4.组织SQL
String sql = \\\"select name,pwd from admin where name=\\\'\\\" +admin_name+\\\"\\\' and pwd=\\\'\\\"+admin_pwd+\\\"\\\' \\\";
//执行给定的SQL语句,该语句返回单个 ResultSet对象,类似于返回一张表
ResultSet resultSet = statement.executeQuery(sql);
if (resultSet.next()){//如果查询到一条记录,则说明该管理员存在
System.out.println(\\\"恭喜,登陆成功!\\\");
}else{
System.out.println(\\\"对不起,登陆失败\\\");
}
resultSet.close();
connection.close();
}
}
配置文件
导入com.mysql.jdbc.Driver并进行配置:
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/sqlinjection?characterEncoding=UTF-8&useSSL=false
user=al1ex
password=al11ex
注入防御
正常演示
从上面的测试工程中我们看到此时我们需要在控制台输入用户名以及用户密码之后后段会获取我们输入的数据并将其作为参数构造SQL语句,之后执行SQL语句检索后段数据库中是否有与之相匹配的用户名和用户密码(此处需要用户名和用户密码完全匹配),如果有则查询的结果自然不为空,从而提示\\”登录成功\\”,如果用户名和用户密码不匹配或者用户名不存在等其他情况则会导致数据库查无此信息,最终使得查询结果为空,从而提示\\”登录失败\\”,下面是我们的一个正常登录测试:
A、正确用户名和正确用户密码
请输入管理员名字:al1ex
请输入管理员密码:123456
select name,pwd from admin where name=\\\'al1ex\\\' and pwd=\\\'123456\\\'
恭喜,登陆成功!
B、正确用户名和错误的用户密码
注入演示
上面的工程代码中我们可以看到用户名和用户密码属于用户可控且直接拼接,之后直接使用statement.executeQuery执行SQL语句中间不带任何过滤处理和预编译操作,故而存在SQL注入,简易演示如下:
请输入管理员名字:admin
请输入管理员密码:1234\\\' or \\\'1\\\'=\\\'1
select name,pwd from admin where name=\\\'admin\\\' and pwd=\\\'1234\\\' or \\\'1\\\'=\\\'1\\\'
恭喜,登陆成功!
有效防御
在这里我们使用预编译进行SQL注入防御,需要注意的是这里我们使用了\\”?\\”占位符,同时使用prepareStatement进行了预编译操作
package com.al1ex;
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;
import java.util.Scanner;
public class Student2 {
public static void main(String[] args) throws IOException, ClassNotFoundException, SQLException {
Scanner scanner = new Scanner(System.in);
//让用户输入管理员用户名和密码
System.out.print(\\\"请输入管理员名字:\\\");
/**
* 说明:如果希望看到注入效果,这里需要用nextLine,用next不行
* next():当接收到空格或者单引号(\\\')就表示结束,我们输入用户名可能空格,那么就是报错
* 而nextLine()回车才算结束
*/
String admin_name=scanner.nextLine();
System.out.print(\\\"请输入管理员密码:\\\");
String admin_pwd=scanner.nextLine();
Properties properties = new Properties();
properties.load(new FileInputStream(\\\"C:\\\\\\\\Users\\\\\\\\Al1ex\\\\\\\\Desktop\\\\\\\\SqlInjection\\\\\\\\src\\\\\\\\jdbc.properties\\\"));
//获取相关的值
String user = properties.getProperty(\\\"user\\\");
String password = properties.getProperty(\\\"password\\\");
String driver = properties.getProperty(\\\"driver\\\");
String url = properties.getProperty(\\\"url\\\");
//1.注册驱动
Class.forName(driver);
//2.得到连接
Connection connection = DriverManager.getConnection(url, user, password);
//3.得到PreparedStatement
//3.1 组织sql,sql语句的?就相当于占位符。
String sql = \\\"select name,pwd from admin where name=? and pwd=?\\\";
//3.2 preparedStatement对象是实现了PreparedStatement接口的实现类的对象
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//3.3 给?赋值
preparedStatement.setString(1,admin_name);
preparedStatement.setString(2,admin_pwd);
//4.执行select语句
PreparedStatement pst = null;
System.out.println(sql);
ResultSet rs = preparedStatement.executeQuery();
if (rs.next()){//如果查询到一条记录,则说明该管理员存在
System.out.println(\\\"恭喜,登陆成功!\\\");
}else{
System.out.println(\\\"对不起,登陆失败\\\");
}
preparedStatement.close();
connection.close();
}
}
从下面的执行结果中我们可以看到该解决方法可以有效的解决SQL注入问题,同时也可以看到即便是在实现参数绑定之后,我们输出的SQL语句依旧使用的?占位符
请输入管理员名字:admin
请输入管理员密码:1234\\\' or \\\'1\\\'=\\\'1
select name,pwd from admin where name=? and pwd=?
对不起,登陆失败
无效防御
下面演示的是笔者在代码审计的过程中发现的不少开发人员对SQL注入防御手法,从下面可以看到这里依旧对参数进行拼接操作,之后直接将拼接后的SQL语句丢进prepareStatement进行执行,以为通过此类方法就可以直接解决SQL注入问题,其实并非如此,具体测试代码如下:
package com.al1ex;
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;
import java.util.Scanner;
public class Student3 {
public static void main(String[] args) throws IOException, ClassNotFoundException, SQLException {
Scanner scanner = new Scanner(System.in);
//让用户输入管理员用户名和密码
System.out.print(\\\"请输入管理员名字:\\\");
/**
* 说明:如果希望看到注入效果,这里需要用nextLine,用next不行
* next():当接收到空格或者单引号(\\\')就表示结束,我们输入用户名可能空格,那么就是报错
* 而nextLine()回车才算结束
*/
String admin_name=scanner.nextLine();
System.out.print(\\\"请输入管理员密码:\\\");
String admin_pwd=scanner.nextLine();
Properties properties = new Properties();
properties.load(new FileInputStream(\\\"C:\\\\\\\\Users\\\\\\\\Al1ex\\\\\\\\Desktop\\\\\\\\SqlInjection\\\\\\\\src\\\\\\\\jdbc.properties\\\"));
//获取相关的值
String user = properties.getProperty(\\\"user\\\");
String password = properties.getProperty(\\\"password\\\");
String driver = properties.getProperty(\\\"driver\\\");
String url = properties.getProperty(\\\"url\\\");
//1.注册驱动
Class.forName(driver);
//2.得到连接
Connection connection = DriverManager.getConnection(url, user, password);
//3.得到Statement
Statement statement = connection.createStatement();
//4.组织SQL
String sql = \\\"select name,pwd from admin where name=\\\'\\\" +admin_name+\\\"\\\' and pwd=\\\'\\\"+admin_pwd+\\\"\\\' \\\";
//执行给定的SQL语句,该语句返回单个 ResultSet对象,类似于返回一张表
PreparedStatement pst = null;
ResultSet rs = null;
System.out.println(sql);
pst = connection.prepareStatement(sql);
rs = pst.executeQuery();
if (rs.next()){//如果查询到一条记录,则说明该管理员存在
System.out.println(\\\"恭喜,登陆成功!\\\");
}else{
System.out.println(\\\"对不起,登陆失败\\\");
}
rs.close();
connection.close();
}
}
从下面的回显结果我们可以看到成功实现SQL注入:
请输入管理员名字:admin
请输入管理员密码:1234\\\' or \\\'1\\\'=\\\'1
select name,pwd from admin where name=\\\'admin\\\' and pwd=\\\'1234\\\' or \\\'1\\\'=\\\'1\\\'
恭喜,登陆成功!
调试分析
调试分析只需要在Student2中下一个断点即可,有兴趣的可以去进行调试一下,这里不再赘述~
下面是PreparedStatement处理参数的简易流程:
文末小结
预编译防止SQL注入的根本在于使用预编译后语句是语句,参数是参数,但是要注意预编译防止SQL注入的关键点在于正确使用预编译(参数占位、参数绑定、语句执行等),并不是使用了prepareStatement就可以防止SQL注入
原创文章,作者:七芒星实验室,如若转载,请注明出处:https://www.sudun.com/ask/34331.html