SQL注入

SQL是操作数据库数据的结构化查询语言,网页的应用数据和后台数据库中的数据进行交互时会采用SQL。
而SQL注入是将Web页面的原URL、表单域或数据包输入的参数,修改拼接成SQL语句,传递给Web服务器,进而传给数据库服务器以执行数据库命令。如Web应用程序的开发人员对用户所输入的数据或cookie等内容不进行过滤或验证(即存在注入点)就直接传输给数据库,就可能导致拼接的SQL被执行,获取对数据库的信息以及提权,发生SQL注入攻击。
在java中,操作SQL的主要有以下几种方式:

在java中,操作SQL的主要有以下几种方式:

  1. java.sql.Statement
  2. java.sql.PrepareStatment
  3. 使用第三方ORM框架,MyBatis或者Hibernate

java.sql.Statement

java.sql.statement是最原始的执行SQL的接口,使用它需要手动拼接SQL语句。

  1. String sql = "SELECT * FROM user WHERE id = '" + id + "'"; // 字符串拼接sql语句
  2. Statement statement = connection.createStatement();
  3. statement.execute(sql); // 将sql字符串当作参数,并执行查询,此时存在sql注入问题

这种用法会导致存在sql注入的问题。
将id直接拼接到SQL语句并执行,所以可以构造语句进行注入。

java.sql.PrepareStatement

java.sql.PrepareStatement是一个接口,是对上面的Statement的扩展。
该方法实现了预编译的功能,拥有防护SQL的特性。

1. SQL预编译

  1. 数据库SQL语句编译特性:

数据库接受到sql语句之后,需要词法和语义解析,优化sql语句,制定执行计划。这需要花费一些时间。但是很多情况,我们的一条sql语句可能会反复执行,或者每次执行的时候只有个别的值不同(比如query的where子句值不同,update的set子句值不同,insert的values值不同)。

  1. 减少编译的方法

如果每次都需要经过上面的词法语义解析、语句优化、制定执行计划等,则效率就明显不行了。为了解决上面的问题,于是就有了预编译,预编译语句就是将这类语句中的值用占位符替代,可以视为将sql语句模板化或者说参数化。一次编译、多次运行,省去了解析优化等过程。

  1. 缓存预编译

预编译语句被DB的编译器编译后的执行代码被缓存下来,那么下次调用时只要是相同的预编译语句就不需要编译,只要将参数直接传入编译过的语句执行代码中(相当于一个函数)就会得到执行,并不是所以预编译语句都一定会被缓存,数据库本身会用一种策略(内部机制)。

  1. 预编译的实现方法

预编译是通过PreparedStatement和占位符来实现的。

2. 预编译作用:

  1. 预编译阶段可以优化 sql 的执行

预编译之后的 sql 多数情况下可以直接执行,DBMS 不需要再次编译,越复杂的sql,编译的复杂度将越大,预编译阶段可以合并多次操作为一个操作。可以提升性能。

  1. 防止SQL注入

使用预编译,而其后注入的参数将不会再进行SQL编译。也就是说其后注入进来的参数系统将不会认为它会是一条SQL语句,而默认其是一个参数,参数中的or或者and 等就不是SQL语法保留字了。

使用时,在SQL语句中,用 ? 作为占位符,代替需要传入的参数,然后将该语句传递给数据库,数据库会对这条语句进行预编译。如果要执行这条SQL,只要用特定的 set 方法,将传入的参数设置到SQL语句中的指定位置,然后调用 execute 方法执行这条完整的SQL。

示例如下:

  1. String sql = "SELECT * FROM user WHERE id = ?"; // 预编译语句
  2. PreparedStatement preparedStatement = connection.prepareStatement(sql); // 填入参数
  3. preparedStatement.setString(1,reqStuId); // 设置占位符的值
  4. preparedStatement.executeQuery(); // 执行查询

此时,如果我用之前的请求攻击,执行的SQL会变成 SELECT * FROM user WHERE id = ‘’or 1 #’,可以看到单引号是被转义了,同时参数也被一对单引号包裹,数字型注入也不存在了。

ORDER BY 排序注入

通过占位符传参,不管传递的是什么类型的值,都会被单引号包裹。
而使用 ORDER BY 时,要求传入的是字段名或者是字段位置。
示例如下:

  1. String sql = "SELECT * FROM user ORDER BY " + column;

那么这样依然可能会存在SQL注入的问题,在 Java 中会有两种情况:

  1. //order by不能使用预编译原理
  2. String sql = " SELECT passwd FROM test_table1 WHERE username = ? ";
  3. ps.setString(1, username)
  4. // 会自动给值加上引号。比如假设username=“ls”,那么拼凑成的语句会是
  5. String sql = " SELECT passwd FROM test_table1 WHERE username = 'ls' ";
  6. // 再看order by,order by后一般是接字段名,而字段名是不能带引号的,比如 order by username;
  7. // 如果带上引号成了order by 'username',那username就是一个字符串不是字段名了,这就产生了语法错误。

column 是字符串型
这种情况和 Statement 中描述的一样,是存在注入的。要防御就必须要手动过滤,或者将字段名硬编码到 SQL 语句中。
示例如下:

  1. String column = "id";
  2. String sql ="";
  3. switch(column){ // 根据column来判断需要拼接的sql语句,尽量不要把用户从前端的输入拼接到sql语句中,尽量不信任用户的任何输入。
  4. case "id": sql = "SELECT * FROM user ORDER BY id";
  5. break;
  6. case "username": sql = "SELECT * FROM user ORDER BY username";
  7. break;
  8. ......
  9. }

同样group by也存在同样的问题

MyBatis

MyBatis简介
MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Ordinary Java Object,普通的 Java对象)映射成数据库中的记录。

MyBatis的使用方式主要有俩种,一种是使用注解,直接将SQL语句和方法绑定在一起,如下

  1. package org.mybatis.example;
  2. public interface BlogMapper {
  3. @Select("SELECT * FROM blog WHERE id = #{id}")
  4. Blog selectBlog(int id);
  5. }

这种方式,适合简单的SQL语句,一旦语句长了,注释会变得复杂混乱,维护起来很麻烦,所以它只适合小项目(小项目用的也不多)。
用的最多的是第二种——XML配置,将SQL语句和Java代码分离,有独立的xml文件,描述某个方法会和某个SQL语句绑定。
每一个接口,在资源文件目录中,都有对应的xml。接口中的方法,和xml中id相同的SQL语句关联。
例如,IArticleCateDao 的 list()方法被调用,那么就会找到 ArticleCateMapper.xml中 id等于 list 的方法,执行它的 SQL,然后根据 resultMap 描述的 字段-属性 映射关系,返回相应的实例对象。

这里的 resultMap 具体如下:

  1. <resultMap id="ArticleCateResult" type="ArticleCate">
  2. <id column="id" jdbcType="INTEGER" property="id"/>
  3. <result column="fid" jdbcType="INTEGER" property="fid"/>
  4. <result column="name" jdbcType="VARCHAR" property="name"/>
  5. <result column="status" jdbcType="INTEGER" property="status"/>
  6. <result column="sort" jdbcType="INTEGER" property="sort"/>
  7. </resultMap>

其中,id属性是该映射的名称,type属性代表映射的类。里面有 5 个子元素,id元素映射到ArticleCate的id属性。其它四个result元素中的column属性会映射到对应的property属性。

MyBatis注入问题

${} (不安全的写法)
使用 ${foo} 这样格式的传入参数会直接参与SQL编译,类似字符串拼接的效果,是存在SQL注入漏洞的。所以一般情况下,不会用这种方式绑定参数。

{}(安全的写法)
使用 #{} 做参数绑定时, MyBatis 会将SQL语句进行预编译,避免SQL注入的问题。

MyBatis 预编译模式的实现,在底层同样是依赖于 java.sql.PreparedStatement,所以 PreparedStatement 存在的问题,这里也会存在。
ORDER BY 只能通过 ${} 传递。为了避免SQL注入,需要手动过滤,或者在SQL里硬编码 ORDER BY 的字段名。
此外,还有一种情况 —— LIKE 模糊查询。
示例如下:

  1. <select id="selectStudentByFuzzyQuery" resultMap="studentMap">
  2. SELECT * FROM student WHERE student.stu_name LIKE '%#{stuName}%'
  3. </select>

在这里,MyBatis 会把 %#{stuName}% 作为要查询的参数,数据库会执行 SELECT * FROM student WHERE student.stu_name LIKE ‘%#{stuName}%’,导致查询失败,经过查询失败的原因是这么写经MyBatis转换后(‘%#{name}%’)会变为(‘%?%’),而(‘%?%’)会被看作是一个字符串,所以Java代码在执行找不到用于匹配参数的 ‘?’ ,然后就报错了。

所以这里只能用 ${} 的方式传入。而如果用 ${} 又存在SQL注入的风险,怎么办呢?

最好的方法是,使用数据库自带的 CONCAT ,将 % 和我们用 #{} 传入参数连接起来,这样就既不存在注入的问题,也能满足需求。
示例如下:

  1. <select id="selectStudentByFuzzyQuery" resultMap="studentMap">
  2. SELECT * FROM student WHERE student.stu_name LIKE CONCAT('%',#{stuName},'%')
  3. </select>

Hibernate注入问题

Hibernate简介

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

Hibernate简单架构

  1. 创建持久化类
  2. 创建对象-关系映射文件
  3. 创建Hibernate配置文件
  4. 通过Hibernate API编写访问数据库的代码

image.png

定位框架

直接查看相关的依赖即可:

  1. <!-- 添加Hibernate依赖 -->
  2. <dependency>
  3. <groupId>org.hibernate</groupId>
  4. <artifactId>hibernate-core</artifactId>
  5. <version>3.6.10.Final</version>
  6. </dependency>

或者是相关的配置文件 hibernate.cfg.xml、hibernate.properties、*.hbm.xml。
例如hibernate.cfg.xml:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE hibernate-configuration PUBLIC
  3. "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
  4. "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
  5. <hibernate-configuration>
  6. <session-factory>
  7. <!-- 1. 配置数据库信息 -->
  8. <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
  9. <property name="hibernate.connection.password">ceshi</property>
  10. <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/test</property>
  11. <property name="hibernate.connection.username">root</property>
  12. <property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property>
  13. <!-- 2. 其他配置 -->
  14. <!-- 显示生成的SQL语句 -->
  15. <property name="hibernate.show_sql">true</property>
  16. <property name="format_sql">true</property><!-- 会格式化输出sql语句 -->
  17. <!-- 3. 导入映射文件 -->
  18. <mapping resource="/com/tk/sqlinject/hibernate/User.hbm.xml" />
  19. </session-factory>
  20. </hibernate-configuration>

除了上述方式外,还可以通过hibernate相关的注解来定位,例如

  1. @Column
  2. @Transient
  3. @Table
  4. .....

Hibernate框架下SQL注入漏洞产生场景

Hibernate查询方式主要有get/load主键查询,对象导航查询、HQL查询、Criteria查询、SQLQuery本地SQL查询。审计的方法主要是搜索createQuery()、createSQLQuery、criteria、createNativeQuery(),查看与其相关的上下文,检查是否存在拼接sql。

HQL查询

HQL:hibernate query language,即hibernate提供的面向对象的查询语言。查询的是对象以及对象的属性(区分大小写)。HQL查询并不直接发送给数据库,而是由hibernate引擎对查询进行解析并解释,根据对象-关系映射文件中的映射信息,将其转换为SQL。

跟所有SQL注入的成因一样,使用拼接HQL语句的写法可能会导致SQL注入:

  1. Query<User> query = session.createQuery("from User where name='"+queryString+"'"); // hql拼接

但是HQL注入存在一定的局限性,不能像常规SQL注入一样利用。 其没有联合,没有可用的元数据表等。Hibernate查询语言没有那些在后台数据库中可能存在的功能特性。
举几个例子:

  • 不支持 * 号

    1. Query<User> query = session.createQuery("select * from User");

    Java代审:02_SQL注入 - 图2

  • 不支持Union

    1. Query<User> query = session.createQuery("from User Union select 1,2,3,4,5");

    Java代审:02_SQL注入 - 图3

  • 不支持跨库查表,系统表也不行(未映射的表不可查询)

    1. Query<User> query = session.createQuery("from hibernate.user4");

    Java代审:02_SQL注入 - 图4
    查询mysql的系统表information_schema同样会返回表not mapped:

    1. Query<User> query = session.createQuery("from information_schema.tables");

    Java代审:02_SQL注入 - 图5

  • 表名、列名大小写敏感
    例如映射配置文件中将User Bean映射如下:

    1. <hibernate-mapping>
    2. <class name="com.tk.sqlinject.hibernate.User" table="USER">
    3. <id name="id" type="int">
    4. <column name="ID"/>
    5. <generator class="assigned"/>
    6. </id>
    7. ......
    8. </hibernate-mapping>

那么如果我们并没有考虑大小写,直接查询user的话是会产生not mapped报错的:

  1. Query<User> query = session.createQuery("from user");

Java代审:02_SQL注入 - 图6

HQL无法直接执行原生SQL,及写文件,执行命令等操作。
除了利用万能密码、知道表名列名的情况下进行盲注外,暂时没想到比较好的方法来进行漏洞利用。但是成因跟判断方式跟SQL注入类似,所以也归在一起,审计时候可以注意。

Criterion查询

Hibernate支持Criteria查询(Criteria Query),该查询方式把查询条件封装为一个Criteria对象。在实际应用中,通过构建一个org.hibernate.Criteria实例,然后把具体的查询条件通过Criteria的add()方法加入到Criteria实例中。这样,可以在不使用SQL甚至HQL的情况下进行数据查询。
Hibernate提供了相当多的内置criterion类型(Restrictions子类),例如in、like、eq(相当于where条件查询)等,需要额外注意的是Restrictions.sqlRestriction(SQL限定的查询)
Restrictions.sqlRestriction(sql,value,type)

  1. 是String类型,是SQL语句的条件部分。
  2. 参数是参数值。
  3. 参数是类型。

同样的,若第一个参数使用拼接的方式进行交互的话,会存在SQL注入风险的。举个例子:

  1. List<User> resSql = criteria.add(Restrictions.sqlRestriction("id="+query)).list();
  2. for (User user : resSql) {
  3. System.out.println(user.toString());
  4. }

query是我们用户输入,可以在log打印对应的sql语句,可以看到,倘若我们输入恶意的sql注入payload的话,是可以成功执行的:
Java代审:02_SQL注入 - 图7

SQLQuery本地SQL查询

Hibernate对原生SQL查询的支持和控制是通过SQLQuery接口实现的,这种方式弥补了HQL、Criterion查询的不足,其直接使用sql语句进行查询,在操作和使用上往往更加的自由和灵活,如果使用得当,数据库操作的效率还会得到不同程度的提升。一般复杂的sql都会用到它。
同样的,只要存在sql拼接,就会存在注入问题:

  1. SQLQuery<User> sqlQuery = session.createSQLQuery("select * from User where Name='"+query+"'").addEntity(User.class);

同时因为其直接操作sql,与常见的SQL注入无异,没有特殊的限制。
PS:新版本hibernate已经弃用createSQLQuery(),可使用createNativeQuery()代替。
综上,其实Hibernate框架下的注入挖掘主要还是查看拼接。

Hibernate框架下SQL注入漏洞修复

主要方式是名称绑定,位置绑定(参数化查询)。
下面是具体实例:

HQL查询

  • 名称绑定:

    1. String sqlString = tom;
    2. List<User> res =(List<User>)session.createQuery("select u from User u where name like :name")
    3. .setParameter("name", "%" + sqlString + "%")
    4. .list();
  • 位置绑定:

    1. Query<User> query = session.createQuery("from User where Name=?");
    2. // Hibernate 4.1 之后对于HQL中查询参数的占位符做了改进
    3. Query<User> query = session.createQuery("from User where Name=?0");
    4. // HQL是从0开始的 query.setParameter(0, "tkswifty");
    5. query.setString(0,name);

    Criterion查询

    对于Restrictions.sqlRestriction的情况,应该使用预编译的方式进行SQL查询:

    1. List<User> resSql = criteria.add(Restrictions.sqlRestriction("name=?",query,StandardBasicTypes.STRING)).list();

    同时,在mybatis中提到的例如in、like等查询方式,Criterion查询可以很好的解决预编译的问题:

  • Like

    1. Criteria criteria = session.createCriteria(User.class);
    2. List<User> resLike = criteria.add(Restrictions.like("name", "%test")).list();

    打印log看出交互过程,可以看到熟悉的占位符?:
    Java代审:02_SQL注入 - 图8

  • in

    1. Criteria criteria = session.createCriteria(User.class);
    2. List<User> resIn = criteria.add(Restrictions.in("name", new String[] {"test"})).list();

    Java代审:02_SQL注入 - 图9

    SQLQuery本地SQL查询

    使用占位符预编译方式:

    1. SQLQuery<User> sqlQuery = session.createSQLQuery("select * from User where Name=?0").addEntity(User.class);
    2. sqlQuery.setParameter(0, query);

    打印log看出交互过程,可以看到熟悉的占位符?:
    Java代审:02_SQL注入 - 图10

    其他

    虽然参数化查询是防止SQL注入最便捷有效的一种方式,但是其无法应用于所有场景。当使用执行前不可被占位符?替代的不可信数据来动态构建SQL语句时,必须要对不可信数据进行校验。
    示例如下:

    1. Code oe = new OracleCodec();
    2. String safeExpr = ESAPI.encoder().encodeForSQL(oe, exprString);

同样的,可以在过滤器filter中过滤一些常见的sql注入的敏感字符串,也是安全防护措施审计中不可缺少的一环。