创建连接对象使用CreateStatement()
Statement stmt = null;
try {
stmt = conn.createStatement( );
. . .
}
catch (SQLException e) {
. . .
}
finally {
stmt.close();
}
JDBC使用Statement是不安全的,需要程序员做好过滤,所以一般使用JDBC的程序员会更喜欢使用PrepareStatement做预编译,预编译不仅提高了程序执行的效率,还提高了安全性。
使用Statement
来操作数据库比较常见的场景是直接使用+
拼接SQL语句,如下图
@GetMapping("/vul1")
public String vul1(String id) {
StringBuilder result = new StringBuilder();
try {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection conn = DriverManager.getConnection(db_url, db_user, db_pass);
Statement stmt = conn.createStatement();
String sql = "select * from users where id = '" + id + "'";
log.info("[vul] 执行SQL语句: " + sql);
ResultSet rs = stmt.executeQuery(sql);
while (rs.next()) {
String res_name = rs.getString("user");
String res_pass = rs.getString("pass");
String info = String.format("查询结果 %s: %s", res_name, res_pass);
result.append(info);
}
rs.close();
stmt.close();
conn.close();
return result.toString();
} catch (Exception e) {
// 输出错误,用于报错注入
return e.toString();
}
}
例如String id
获取的参数为1,那么此时的SQL语句为
select * from users where id = '1'
因为这里并未对String id
参数进行过滤,所以传入'and updatexml(1,concat(0x7e,(SELECT user()),0x7e),1)--%20+
就会造成SQL注入,那么此时的语句变成下面这样
select * from users where id = ''and updatexml(1,concat(0x7e,(SELECT user()),0x7e),1)--%20+'
可以看到传入的单引号闭合了前面的单引号,并且最后使用–+注释原来的单引号,这样就导致了SQL注入。
Tips: 不过这里做一个思考,Java是一个强类型的语言,那么在使用
id
来代表参数,那么大概率接收的是一个int
类型的值,我认为如果站在java开发的角度上想,这里如果定义为int id
是不是就不会造成注入了呢?
手动修改一下这里的代码,然后启动
很显然,这里报错了,因为是类型转换的问题,我们传入的Str没办法转成int,所以这里及时存在SQL注入,在这种类型的限制下,也没有办法进行SQL语句拼接
PrepareStatement与Statement的区别对SQL语句进行预编译处理,预编译的好处除了在一定程度上防止SQL注入之外,还减少了SQL语句的编译次数,有效的提高了性能,而SQL注入只对编译过程有破坏作用,执行阶段只是把输入串作为数据处理,不需要再对SQL语句进行解析,因此解决了注入问题。
因为SQL语句编译阶段是进行词法分析、语法分析、语义分析等过程的,也就是说编译过程识别了关键字、执行逻辑之类的东西,编译结束了这条SQL语句能干什么就定了。而在编译之后加入注入的部分,就已经没办法改变执行逻辑了,这部分就只能是相当于输入字符串被处理
String sql = "select * from users where id = ?";
PreparedStatement st = conn.prepareStatement(sql);
st.setString(1, id);
ResultSet rs = st.executeQuery();
1、prepareStatement会先初始化SQL,先把这个SQL提交到数据库中进行预处理,多次使用可提高效率,而Statement不会初始化,没有预处理,每次都是从0开始执行SQL。 2、prepareStatement可以在SQL中用?替换变量,而createStatement不支持 ? 替换变量,只能在sql中拼接参数 3、在使用功能上的区别 如果想要删除三条数据 对于createStatement,需要写三条语句
String sql = "delete from category where id = 2" ;
String sql = "delete from category where id = 3" ;
String sql = "delete from category where id = 7" ;
而prepareStatement,通过set不同数据只需要生成一次执行计划,可以重用
String sql = "delete from category where id = ?" ;
总结 Statement每次执行sql语句,数据库都要执行sql语句的编译,最好用于仅执行一次查询并返回结果的情形,效率高于PreparedStatement.但存在sql注入风险。PreparedStatement是预编译执行的。在执行可变参数的一条SQL时,PreparedStatement要比Statement的效率高,因为DBMS预编译一条SQL当然会比多次编译一条SQL的效率高。安全性更好,有效防止SQL注入的问题。对于多次重复执行的语句,使用prepareStatement,因为数据库会对sql语句进行预编译,下次执行相同的sql语句时,数据库端不会再进行预编译了,而直接用数据库的缓冲区,提高数据访问的效率(但尽量采用使用?号的方式传递参数),如果sql语句只执行一次,以后不再复用。
原理是采用了预编译的方法,先将SQL语句中可被客户端控制的参数集进行编译,生成对应的临时变量集,再使用对应的设置方法,为临时变量集里面的元素进行赋值,赋值函数setString()
,会对传入的参数进行强制类型检查和安全检查,所以就避免了SQL注入的产生。下面具体分析。
Statement之所以会被sql注入是因为SQL语句结构发生了变化。比如:
SQL = “SELECT * FROM users WHERE (name = ‘” + userName + “’) and (pw = ‘”+ passWord +”’);”
//如果恶意填入
userName = “1’ OR ‘1’=’1”;
passWord = “1’ OR ‘1’=’1”;
//将变成
SQL = SELECT * FROM users WHERE (name = ‘1’ OR ‘1’=’1’) and (pw = ‘1’ OR ‘1’=’1’);
//相当于
SQL = SELECT * FROM users;
Sql
而Preparement样式为
SELECT * FROM users WHERE userName=? and passWord=?
Sql
该SQL语句会在得到用户的输入之前先用数据库进行预编译,这样的话不管用户输入什么用户名和密码的判断始终都是并的逻辑关系,防止了SQL注入。
简单总结,参数化能防注入的原因在于,语句是语句,参数是参数,参数的值并不是语句的一部分,数据库只按语句的语义跑。
public String vul2(String id) {
StringBuilder result = new StringBuilder();
try {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection conn = DriverManager.getConnection(db_url, db_user, db_pass);
String sql = "select * from users where id = " + id;
log.info("[vul] 执行SQL语句: " + sql);
PreparedStatement st = conn.prepareStatement(sql);
ResultSet rs = st.executeQuery();
while (rs.next()) {
String res_name = rs.getString("user");
String res_pass = rs.getString("pass");
String info = String.format("查询结果%n %s: %s%n", res_name, res_pass);
result.append(info);
}
rs.close();
st.close();
conn.close();
return result.toString();
} catch (Exception e) {
return e.toString();
}
}
在代码中可以看到,这里使用了prepareStatement对SQL语句进行预编译处理,但是还是使用+号拼接的方式拼接了前端传入的参数,没有使用?号做占位符,这样就导致了prepareStatement预编译处理防止SQL注入失效了。
在控制台中可以看到打印的SQL语句
Tips: 来看看使用?号占位符的时候SQL语句的样子
不难发现使用?号占位符之后,传入的参数还是会在’’中间,因为传入的只会当做字符串作为解析
新增一个方法,改一下代码
public String safe_test(String id) {
StringBuilder result = new StringBuilder();
try {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection conn = DriverManager.getConnection(db_url, db_user, db_pass);
String sql_1 = "where user like '%" + id + "%'";
String sql = "select * from users " + sql_1;
System.out.println("拼接后语句为: " + sql);
PreparedStatement st = conn.prepareStatement(sql);
log.info("[safe] 执行SQL语句: " + st);
ResultSet rs = st.executeQuery();
result.append("成功");
rs.close();
st.close();
conn.close();
return result.toString();
} catch (Exception e) {
return e.toString();
}
}
然后构造参数请求接口
GET /SQLI/JDBC/test1?id=1%25'+or+sleep(3)%23 HTTP/1.1
Host: 192.168.0.35:8888
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:103.0) Gecko/20100101 Firefox/103.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: JSESSIONID=F445CA5F0CF6B1BE279BC0FA438873E6
在in当中使用拼接而不使用占位符做预编译的原因是因为很多时候无法确定Ids里含有多少个对象 例如下面的语句
select * from users where id in (2,3);
# 使用占位符就需要
select * from users where id in (?, ?);
# 那么有多个语句呢?如何动态的更新?
稍微改一下代码
首先为什么预编译无法防止order by注入,因为order by的子域后面需要加上字段名或者字段位置,但是字段名是不能带引号的,否则会被认为是一个字符串,但是使用PreapareStatement会强制给参数加上单引号
但是如果给order by加上单引号,可以发现order by的排序功能失效了
这里有三个对照组可以参考,所以在使用order by语句的时候必须拼接Statement,所以如果使用order by的话,需要对传入的参数进行过滤。
Mybatis下有两种传参方式,分别是${}以及#{}, 其区别是
1、传入的参数在SQL中显示为字符串(当成一个字符串),会对自动传入的数据加一个双引号。
例:使用以下SQL
select id,name,age from student where id =#{id}
当我们传递的参数id为 “1” 时,上述 sql 的解析为:
select id,name,age from student where id ="1"
$传入的参数在SqL中直接显示为传入的值 例:使用以下SQL
select id,name,age from student where id =${id}
当我们传递的参数id为 “1” 时,上述 sql 的解析为:
select id,name,age from student where id =1
1、号作用相当于是字符串拼接相当于使用StringBuffer的append方法将{username}
1、在sql语句中,如果要接收传递过来的变量的值的话,必须使用#。因为使用#是通过PreparedStement接口来操作,可以防止sql注入,并且在多次执行sql语句时可以提高效率。
2、只是简单的字符串拼接而已,所以要特别小心sql注入问题。对于sql语句中非变量部分,那就可以使用,比如方式一般用于传入数据库对象(如传入表名)。例如:
select * from `${tableName}$`
对于不同的表执行统一的查询操作时,就可以使用$来完成。
例:MyBatis排序时使用order by 动态参数时需要注意,用$而不是#。
#
号select * from users order by id;
select * from users order by 'id';
最直观的两条SQL语句就可以证明为什么不能使用#号
这里很明显能够发现,当加上单引号之后,排序就失效了,这是为什么呢?
因为表名不允许使用引号,直接引用就报错,但是使用#号又会给表名加上单引号,导致报错,所以推荐使用$号
【底层实现原理】在框架底层,是JDBC中的PreparedStatement类在起作用,PreparedStatement是我们很熟悉的Statement的子类,它的对象包含了编译好的SQL语句。这种“准备好”的方式不仅能提高安全性,而且在多次执行同一个SQL时,能够提高效率。原因是SQL已编译好,再次执行时无需再编译。
随后访问接口构造报错函数
try catch堆栈信息
由于使用 #{} 会将对象转成字符串,形成 order by “user” desc 造成错误,因此很多研发会采用${}来解决,从而造成SQL注入
orderby在Mybatis如下
请求接口查看SQL执行的语句
然后手动执行一下SQL语句就知道差异是什么了
可以看到,在使用单引号加上order by排序的字段之后,orderby的排序功能直接失效,那么预编译处理的参数传进去之后肯定是有单引号的,故此会造成SQL注入
Hibernate是一个开源的对象关系映射(ORM:Object Relation Mapping)框架,对JDBC进行了非常轻量级的对象封装,采用映射元数据(配置文件)来描述对象-关系的映射细节,是一个全自动的orm框架。hibernate可以自动生成SQL语句,自动执行,使得Java程序员可以随心所欲的使用对象编程思维来操纵数据库。
ORM是一种思想
Hibernate查询方式主要有get/load主键查询,对象导航查询、HQL查询、Criteria查询、SQLQuery本地SQL查询。审计的方法主要是搜索createQuery()、createSQLQuery、criteria、createNativeQuery(),查看与其相关的上下文,检查是否存在拼接sql。
整体目录结构
package com.uzju.hsql;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.Table;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Parameter;
@Entity
@Table(name = "ADDRESS")
public class Address {
@Id
@Column(name = "emp_id", unique = true, nullable = false)
@GeneratedValue(generator = "gen")
@GenericGenerator(name = "gen", strategy = "foreign",
parameters = { @Parameter(name = "property", value = "employee") })
private long id;
@Column(name = "address_line1")
private String addressLine1;
@Column(name = "zipcode")
private String zipcode;
@Column(name = "city")
private String city;
@OneToOne
@PrimaryKeyJoinColumn private Employee employee;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getAddressLine1() {
return addressLine1;
}
public void setAddressLine1(String addressLine1) {
this.addressLine1 = addressLine1;
}
public String getZipcode() {
return zipcode;
}
public void setZipcode(String zipcode) {
this.zipcode = zipcode;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public Employee getEmployee() {
return employee;
}
public void setEmployee(Employee employee) {
this.employee = employee;
}
}
package com.uzju.hsql;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import org.hibernate.annotations.Cascade;
@Entity
@Table(name = "EMPLOYEE")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "emp_id")
private long id;
@Column(name = "emp_name")
private String name;
@Column(name = "emp_salary")
private double salary;
@OneToOne(mappedBy = "employee")
@Cascade(value = org.hibernate.annotations.CascadeType.ALL)
private Address address;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
}
package com.uzju.hsql;
import org.hibernate.SessionFactory;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cfg.Configuration;
import org.hibernate.service.ServiceRegistry;
public class HibernateUtil {
private static SessionFactory sessionFactory;
private static SessionFactory buildSessionFactory() {
try {
// Create the SessionFactory from hibernate.cfg.xml
Configuration configuration = new Configuration();
configuration.configure("hibernate.cfg.xml");
System.out.println("Hibernate Configuration loaded");
ServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder().applySettings(configuration.getProperties()).build();
System.out.println("Hibernate serviceRegistry created");
SessionFactory sessionFactory = configuration.buildSessionFactory(serviceRegistry);
return sessionFactory;
}
catch (Throwable ex) {
System.err.println("Initial SessionFactory creation failed." + ex);
ex.printStackTrace();
throw new ExceptionInInitializerError(ex);
}
}
public static SessionFactory getSessionFactory() {
if(sessionFactory == null) sessionFactory = buildSessionFactory();
return sessionFactory;
}
}
package com.uzju.hsql;
import java.util.Arrays;
import java.util.List;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
public class HQLExamples {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
//Prep work
SessionFactory sessionFactory = HibernateUtil.getSessionFactory();
Session session = sessionFactory.getCurrentSession();
//HQL example - Get All Employees
Transaction tx = session.beginTransaction();
Query query = session.createQuery("from Employee");
List<Employee> empList = query.list();
for(Employee emp : empList){
System.out.println("List of Employees::"+emp.getId()+","+emp.getAddress().getCity());
}
//HQL example - Get Employee with id
query = session.createQuery("from Employee where id= :id");
query.setLong("id", 3);
Employee emp = (Employee) query.uniqueResult();
System.out.println("Employee Name="+emp.getName()+", City="+emp.getAddress().getCity());
// //HQL pagination example
// query = session.createQuery("from Employee");
// query.setFirstResult(0); //starts with 0
// query.setFetchSize(2);
// empList = query.list();
// for(Employee emp4 : empList){
// System.out.println("Paginated Employees::"+emp4.getId()+","+emp4.getAddress().getCity());
// }
//
// //HQL Aggregate function examples
// query = session.createQuery("select sum(salary) from Employee");
// double sumSalary = (Double) query.uniqueResult();
// System.out.println("Sum of all Salaries= "+sumSalary);
//
// //HQL join examples
// query = session.createQuery("select e.name, a.city from Employee e "
// + "INNER JOIN e.address a");
// List<Object[]> list = query.list();
// for(Object[] arr : list){
// System.out.println(Arrays.toString(arr));
// }
//
// //HQL group by and like example
// query = session.createQuery("select e.name, sum(e.salary), count(e)"
// + " from Employee e where e.name like '%i%' group by e.name");
// List<Object[]> groupList = query.list();
// for(Object[] arr : groupList){
// System.out.println(Arrays.toString(arr));
// }
//
// //HQL order by example
// query = session.createQuery("from Employee e order by e.id desc");
// empList = query.list();
// for(Employee emp3 : empList){
// System.out.println("ID Desc Order Employee::"+emp3.getId()+","+emp3.getAddress().getCity());
// }
//rolling back to save the test data tx.rollback();
//closing hibernate resources
sessionFactory.close();
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"https://hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory> <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="hibernate.connection.password">root</property>
<property name="hibernate.connection.url">jdbc:mysql://localhost/test</property>
<property name="hibernate.connection.username">root</property>
<property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property>
<property name="hibernate.current_session_context_class">thread</property>
<property name="hibernate.show_sql">true</property>
<mapping class="com.uzju.hsql.Employee"/>
<mapping class="com.uzju.hsql.Address"/>
</session-factory></hibernate-configuration>
String SQLInject = "1";
String SQL = "from Employee where id= " + SQLInject;
List<Employee> emp = session.createQuery(SQL).list();
for (Employee employee : emp){
System.out.println(employee.getName());
System.out.println("=====");
}
String SQLInject = "1 or 1 = 1";
String SQL = "from Employee where id= " + SQLInject;
Hibernate对原生SQL查询的支持和控制是通过SQLQuery接口实现的,这种方式弥补了HQL、Criterion查询的不足,其直接使用sql语句进行查询,在操作和使用上往往更加的自由和灵活,如果使用得当,数据库操作的效率还会得到不同程度的提升。一般复杂的sql都会用到它。
该方法与常规的SQL注入没什么区别,存在注入点直接拼接就可以造成注入,无条件限制。
新版本hibernate已经弃用createSQLQuery(),可使用createNativeQuery()代替。
当查询数据时,人们往往需要设置查询条件。在SQL或HQL语句中,查询条件常常放在where子句中。此外,Hibernate还支持Criteria查询(Criteria Query),这种查询方式把查询条件封装为一个Criteria对象。在实际应用中,使用Session的createCriteria()方法构建一个org.hibernate.Criteria实例,然后把具体的查询条件通过Criteria的add()方法加入到Criteria实例中。这样,程序员可以不使用SQL甚至HQL的情况下进行数据查询。
参数绑定优点: (1)安全性
防止用户恶意输入条件和恶意调用存储过程 (2)提高性能
底层采用JDBC的PreparedStatement预定义sql功能,后期查询直接从缓存中获取执行
在HQL语句中定义命名参数要用”:”开头
1 Query query=session.createQuery(“from User user where user.name=:username and user.age=:userage ”);
2 query.setString(“username”,name);
3 query.setInteger(“userage”,age);
上面代码中用:username和:userage分别定义了命名参数,然后用Query接口的setXXX()方法设定名参数值,setXXX()方法包含两个参数,分别是命名参数名称和命名参数实际值。
在HQL查询语句中用”?”来定义参数位置,形式如下:
Query query=session.createQuery(“from User user where user.name=? and user.age =? ”);
query.setString(0,name);
query.setInteger(1,age);
同样使用setXXX()方法设定绑定参数,只不过这时setXXX()方法的第一个参数代表绑定参数在HQL语句中出现的位置编号(由0开始编号),第二个参数仍然代表参数实际值。
注:在实际开发中,提倡使用按名称绑定命名参数,因为这不但可以提供非常好的程序可读性,而且也提高了程序的易维护性,因为当查询参数的位置发生改变时,按名称邦定名参 数的方式中是不需要调整程 序代码的。
在Hibernate的HQL查询中可以通过setParameter()方法邦定任意类型的参数,如下代码:
String hql=”from User user where user.name=:customername ”;
Query query=session.createQuery(hql);
query.setParameter(“customername”,name,Hibernate.STRING);
如上面代码所示,setParameter()方法包含三个参数,分别是命名参数名称,命名参数实际值,以及命名参数映射类型。对于某些参数类型setParameter()方法可以根据参数值的Java类型,猜测出对应的映射类型,因此这时不需要显示写出映射类型,像上面的例子,可以直接这样写:
query.setParameter(“customername”,name);但是对于一些类型就必须写明映射类型,比如java.util.Date类型,因为它会对应Hibernate的多种映射类型,比如Hibernate.DATA或者Hibernate.TIMESTAMP。
在Hibernate中可以使用setProperties()方法,将命名参数与一个对象的属性值绑定在一起,如下程序代码:
Customer customer=new Customer();
customer.setName(“pansl”);
customer.setAge(80);
Query query=session.createQuery(“from Customer c where c.name=:name and c.age=:age ”);
query.setProperties(customer);
setProperties()方法会自动将customer对象实例的属性值匹配到命名参数上,但是要求命名参数名称必须要与实体对象相应的属性同名。
它会把命名参数与一个持久化对象相关联,如下面代码所示:
Customer customer=(Customer)session.load(Customer.class,”1”);
Query query=session.createQuery(“from Order order where order.customer=:customer ”);
query. setEntity(“customer”,customer);
List list=query.list();
上面的代码会生成类似如下的SQL语句:
Select * from order where customer_ID='1';
参考 2、https://b1ngz.github.io/java-sql-injection-note/ 3、https://zhuanlan.zhihu.com/p/134037462 4、https://zhuanlan.zhihu.com/p/42841510 6、https://www.cnblogs.com/zsh-blogs/p/10574381.html 7、https://zhuanlan.zhihu.com/p/42841510 8、https://c0d3p1ut0s.github.io/MyBatis%E6%A1%86%E6%9E%B6%E4%B8%AD%E5%B8%B8%E8%A7%81%E7%9A%84SQL%E6%B3%A8%E5%85%A5/ 9、https://c0d3p1ut0s.github.io/%E7%AE%80%E5%8D%95%E8%AF%B4%E8%AF%B4MySQL-Prepared-Statement/ 10、https://www.dineshonjava.com/hibernate/understanding-parameter-binding-and-sql/ 11、https://www.digitalocean.com/community/tutorials/hibernate-query-language-hql-example-tutorial 12、https://blog.csdn.net/u011721501/article/details/43918203 13、Java代码审计之SQL注入——Hibernate框架-SecIN (sec-in.com) 14、https://blog.csdn.net/qq_36908872/article/details/103523165