Java审计之SQL注入

Java审计之SQL注入

一、什么是SQL注入
定义

​ SQL是操作数据库数据的结构化查询语言,web应用程序的应用数据与后台数据库中的数据产生交互时会采用SQL。而SQL注入(SQL Injection)是当开发未对web应用程序用户可控输入的参数、web表单、cookie等(即注入点)进行规范性校验、过滤和数据清洗,将用户输入的参数以拼接的方式带入了SQL语句中,造成了SQL注入的产生。攻击者通过构造特殊的payload,后端程序以拼接的方式将payload带入SQL语句中,数据库执行拼接的SQL,使得攻击者直接窃取数据库信息、提权,造成信息泄露。因此SQL注入在多年稳居OWASP TOP 10榜首位置。

SQL注入的特点
1、广泛性

​ 任何一个基于SQL语言的数据库都有可能会被攻击,很多的开发人员在编写web应用程序时未对用户可控输入的参数、web表单、cookie等进行规范性校验、过滤和数据清洗,都很容易出现SQL注入漏洞。

2、隐蔽性

​ SQL语句一般都嵌入在HTTP请求中,很难与正常语句进行区分,并且SQL注入的payload变种极多,攻击者通过调整攻击参数可进行绕过。

3、危害大

​ 攻击者通过SQL注入获取到服务器的库名、表名、字段名,从而获取到整个服务器中的数据,对网站用户的数据安全具有极大的威胁。攻击者也可通过获取到的数据,得到后台管理员的密码,从而拿下整个网站。不仅对数据库信息安全造成威胁,对整个数据库系统安全影响极大。

4、操作方便

​ 网上有很多的SQL注入工具,如SQLmap、傀儡SQL注入批量扫描工具等,git上面也有很多人写的注入exp。

二、Java各框架使用不当造成的SQL注入
(一)、JDBC拼接不当造成SQL注入

​ JDBC有两种方式执行SQL语句,一种是PrepareStatement和Statement。PrepareStatement是预编译的,对于批量处理可以大幅度提高效率,也叫JDBC存储过程。Statement在每次执行时都需要进行编译,增大系统开销。理论上PreqareStatement的效率和安全性会比Statement好,但不意味着PreqareStatement不会存在SQL注入。当对PreqareStatement方法使用不当时,仍会存在SQL注入问题。

  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        String sql = "select * from user where id ="+req.getParameter("id");

        PrintWriter out = resp.getWriter();
        out.println("Statement Demo");
        out.println("SQL: "+sql);
        try {
            Statement st = conn.createStatement();
            ResultSet rs = st.executeQuery(sql);

​ 这段代码示例中,使用Statement执行SQL语句。这段代码拼接方式将用户传入的参数"id"带入SQL语句中,创建Statement对象来进行SQL语句的执行。经过拼接后,最终在数据库执行的语句为"select * from user where id=1 or 1=1",改变了程序想要查询"id=1"的语义。

 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        String sql = "select * from user where id ="+req.getParameter("id");

        PrintWriter out = resp.getWriter();
        out.println("prepareStatement Demo");
        out.println("SQL: "+sql);
        try {
            PreparedStatement pst = conn.prepareStatement(sql);
            ResultSet rs = pst.executeQuery();

​ PreqareStatement方法支持使用占位符对变量进行占位,在预编译阶段填入响应的值会构造出完整的SQL语句,避免SQL注入的产生。开发有时为了遍历,会直接采取拼接的方式构造SQL语句,此时进行预编译无法阻止SQL注入。如上方代码所示虽然使用PreqareStatement对SQL进行了预编译,经过拼接后,最终在数据库执行的语句为"select * from user where id =1 or 1=1"

 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        PrintWriter out = resp.getWriter();
        out.println("prepareStatement Demo3");
        String sql = "select * from user where id = ?";
        out.println(sql);
        try {
            PreparedStatement pstt = conn.prepareStatement(sql);
            // 参数已经强制要求是整型
            pstt.setInt(1, Integer.parseInt(req.getParameter("id")));
            ResultSet rs = pstt.executeQuery();
            while (rs.next()){

​ 正确使用PreqareStatement可有效避免SQL注入问题,使用"?“做为占位符时,填入对应字段的值会进行严格的类型检查。将拼接构造SQL语句更改为使用占位符构造SQL语句可有效避免SQL注入。当用户传入 ‘ or 1=1 --+123 时,最终在数据库执行的语句为” select * from user where id = ‘111’ or 1=1 – 123 '"

(二)、框架使用不当造成SQL注入

​ 在实际的开发过程中,JDBC是将SQL语句写在代码块中的,不利于后期的维护。现如今,很多的Java项目或多或少会使用对JDBC进行抽象封装的持久化框架,如Mybatis和Hibernate。通常框架已对SQL注入进行了防御,但开发人员未恰当使用框架的情况下,仍会纯在SQL注入的风险。

1、Mybatis框架

Mybaits框架原理及分析

​ Mybatis是支持定制化SQL、存储过程以及高级映射的优秀持久层框架,主要完成封装JDBC操作和利用反射打通Java类与SQL之间的相互转换。

​ Mybatis的主要涉及目的是让我们对执行SQL语句时对输入输出的数据管理更加方便,方便写出SQL和方便使用SQL执行结果是Mybatis的核心竞争力。

Mybatis的主要成员

	​ configuration		Mybatis所有配置信息保存在Configuration对象中,配置文件中的大部分配置也存储到该类中。
	​ SqlSession		做为Mybatis工作顶层API,表示和数据库交互时的会话,完成必要的数据库增删改查工作。
	​ Executor			Mybatis执行器,是Mybatis调度的核心,负责SQL语句的生成和查询缓存的维护。
	​ StatementHandler	封装JDBC Statement操作,负责对JDBC Statement的操作,如设置参数等。
	​ ParameterHandler	负责对用户传递的参数转换成JDBC Statement所对应的数据类型。
	​ ResultSetHandler	负责将JDBC返回的ResultSet结果集对象转换成List类型的集合。
	​ TypeHandler		负责Java数据类型和JDBC数据类型之间的映射和转换。
	​ MappedStatement	MappedStatement维护一条

        select id, name, age  from user where name = '${name}'
    

​ 当输入"name的值为"WuXie"时,成功查询到结果,Debug的回显如下:

Created connection 507803860.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1e4478d4]
==>  Preparing: select id, name, age from user where name = 'WuXie' 
==> Parameters: 
<==    Columns: id, name, age
<==        Row: 1, WuXie, 38
<==      Total: 1

​ 此时将’name’的值更改为"WuXie’ or 1=1 --+123"时,成功查询到结果,Debug回显值如下:

==>  Preparing: select id, name, age from user where name = 'WuXie' or 1=1 -- 123' 
==> Parameters: 
<==    Columns: id, name, age
<==        Row: 1, WuXie, 38
<==        Row: 2, ZhangQiling, 100
<==        Row: 3, WangPangzi, 40
<==        Row: 4, WuSanxing, 55
<==        Row: 5, XieYuchen, 36
<==        Row: 6, HeiXiazi, 100
<==        Row: 7, LiCu, 18
<==      Total: 7

​ 根据Debug回显值信息可以看到,使用${}方法时,直接将用户输入的值对变量进行了替换,直接将用户输入的值拼接到了SQL中,从而造成了注入。

​ 使用#{}构造的代码如下


​ 当输入"name的值为"WuXie"时,成功查询到结果,Debug的回显如下:

==>  Preparing: select * from user where name = ? 
==> Parameters: WuXie(String)
<==    Columns: id, name, age
<==        Row: 1, WuXie, 38
<==      Total: 1

​ 通过Debug的代码我们可以看到,使用#{}方式会使用’?‘占位符进行预编译,因此不会产生SQL注入问题。使用"WuXie’ or 1=1 --+123"进行验证可以看到,查询不到任何结果。

结论

​ 在底层构建完整的SQL时,#{}方法采用预编译方式构造SQL,避免了SQL注入问题的产生。而${}采用拼接的方式构造SQL,在对用户输入过滤不严格的前提下,此处极易出现SQL注入。

防御方式

​ 在编写Mybatis的映射语句时,尽量采用#{},若不得不使用${},要手工做好过滤工作,防止SQL注入攻击。

2、Hibernate框架

什么是Hibernate

​ Hibernate是一个开放的源代码的对象关系映射框架,它对JDBC进行了非常轻量的对象封装,它将POJO与数据库表建立映射关系,是一个全自动的orm框架,Hibernate可以自动生产SQL语句,自动执行,使得Java程序员可以随心所欲的使用对象编程思想来操纵数据库。Hibernate可以应用在任何使用JDBC的场合,即可以在Java的客户端程序使用们也可以在Servlet/JSP的Web应用中使用,最具有革命意义的是,Hibernate可以在应用EJB 的JavaEE架构中取代CMP,完成数据持久化的重任。

​ Hibernate框架是Java持久化API(JPA)规范的一种实现方式。Hibernate将Java类映射到数据库表中,从Java数据类型的映射到SQL数据类型,采用Hibernate查询(HQL)注入。

​ HQL与SQL的语法类似,但有所不同。受语法影响,HQL注入在实际漏洞上有一定的限制。Hibernate是对持久化的对象进行操作而不是直接对数据库进行操作,因此HQL查询语句由Hibernate引擎进行解析,意味着产生错误信息的不一定是在数据库,还有可能是来自Hibernate引擎。

HQL与SQL的相同/不同

HQL SQL
HQL查询数据库查类和属性 SQL通过表和表的列进行查询
区分大小写,关键字不区分大小写 不区分大小写
都可以取 都可以取别名
?占位符(Hibernate5之后不支持),一般用命名参数,下标从0开始 ?占位符,从顺序1开始计算
:命名参数 没有命名参数 不支持
面向对象的查询语言 面向结构的查询语言

什么是ORM

​ 对象关系映射(Object Relation Mapping,简称ORM,或O/RM,或O/R mapping),是一种程序,用于实现面向对象编程语言里不同类型系统的数据之间的转换。

​ 对象-关系映射,是随着面向对象的软件开发方法发展而产生的。面向对象的开发方法时当今企业级应用开发环境中的主流开发方法,关系数据库是企业级应用环境中永久存放数据的主流数据存储系统。对象和关系数据时业务实体的两种表现形式,业务实体在内存中表现为对象,在数据库中表现为关系数据。内存中的对象之间存在关联和继承关系,而在数据库中,关系数据无法直接表达多对多关联和继承关系。因此,对象-关系映射(ORM)系统一般以中间件的形式存在,主要实现程序对象到关系数据库数据的映射。ORM模型的简单性简化了数据库查询过程。使用ORM查询工具,用户可以访问期望数据,而不必理解数据库的底层结构。

代码解析

 Query query = session.createQuery("from com.demo.bean.User where name = ?1", User.class);

            query.setParameter(1, parameter);
            List user = query.getResultList();
            System.out.println("user------"+user);
            for (Iterator iterator =
                 user.iterator(); iterator.hasNext(); ) {
                User user1 = (User) iterator.next();
                out.println(user1.toString());
            }

​ 这里采用位置参数的HQL参数绑定方式,进行参数绑定。

​ 查看一下输出结果

Hibernate: 
    select
        user0_.id as id1_0_,
        user0_.name as name2_0_ 
    from
        User user0_ 
    where
        user0_.name=?

​ 可以看到这里已经对SQL语句进行了预编译。

​ 先扩展一下HQL参数绑定:

参数绑定的优点:

​ 1、安全性:防止用户恶意输入条件或恶意调用存储过程。

​ 2、提高性能:底层采用JDBC的PreparedStatement预定义SQL功能,后期查询直接从缓存中获取执行。

参数绑定的集中方式:

​ 位置参数(Positional parameter):

String parameter = req.getParameter("name");
Query query = session.createQuery("from com.demo.bean.User where name = ?1", User.class);
query.setParameter(1, parameter);

​ 命名参数(named parameter):

String parameter = req.getParameter("name");
Query query = session.createQuery("from com.demo.bean.User where name = :1", User.class);
query.setParameter(1, parameter);

​ 命名参数列表(named parameter list):

List parameter = Arrays.asList("WuXie","WangPangzi");
Query query = session.createQuery("from com.demo.bean.User where name = :1", User.class);
query.setParameter(1, parameter);

​ 类实例参数(JavaBean):

parameter.setName("WuXie");
Query query = session.createQuery("from com.demo.bean.User where name = :1", User.class);
query.setParameter(1, parameter);

Query接口提供绑定以下类型接口

query.setBinary() 绑定映射类型为binary的参数 

query.setByte() 绑定映射类型为byte的参数 

query.setBoolean() 绑定映射类型为boolean的参数 

query.setBigInteger() 绑定映射类型为integer的参数 

query.setBigDecimal() 绑定映射类型为decimal的参数 

query.setCharacter() 绑定映射类型为character的参数 

query.setCalendar() 绑定映射类型为calendar的参数 

query.setDate() 绑定映射类型为date的参数 

query.setDouble() 绑定映射类型为double的参数 

query.setString() 绑定映射类型为string的参数 

query.setText() 绑定映射类型为text的参数 

query.setTime() 绑定映射类型为time的参数 

query.setTimestamp() 绑定映射类型为timestamp的参数 

以上方法均重载成两种形式,命名绑定和位置绑定 

Hibernate三种特殊绑定参数

​ (1)setEntity():绑定实体

​ (2)setParameter():绑定任意类型参数

​ (3)setProperties():绑定对象属性,参数名必须与实体属性名一致

​ 下面采用拼接的方式进行举例:

            Query query = session.createNativeQuery("select * FROM User where name='" + parameter + "'");

//            query.setParameter(1, parameter);
            List user = query.getResultList();

​ 查看输出内容

Hibernate: 
    select
        * 
    FROM
        User 
    where
        name='WuXie'

​ 可以看到SQL语句将攻击者输入的参数进行了拼接。

(三)、对于SQL注入应该如何防御

​ SQL注入主要成因是未对用户输入进行严格的过滤,并采取不恰当的方式构造SQL语句,有些地方难免需要使用拼接的方式构造,例如SQL语句中的order by后面的参数无法使用预编译赋值,此时就需要开发对用户传入的参数进行校验、数据清洗。

​ 本文记录结合MS08067的《java代码审计 入门篇》学习整理。MS08067团队的各位师傅非常强。

你可能感兴趣的:(java审计记录,安全)