SQL注入
SQL是操作数据库数据的结构化查询语言,网页的应用数据和后台数据库中的数据进行交互时会采用SQL。
而SQL注入是将Web页面的原URL、表单域或数据包输入的参数,修改拼接成SQL语句,传递给Web服务器,进而传给数据库服务器以执行数据库命令。如Web应用程序的开发人员对用户所输入的数据或cookie等内容不进行过滤或验证(即存在注入点)就直接传输给数据库,就可能导致拼接的SQL被执行,获取对数据库的信息以及提权,发生SQL注入攻击。
在java中,操作SQL的主要有以下几种方式:
在java中,操作SQL的主要有以下几种方式:
- java.sql.Statement
- java.sql.PrepareStatment
- 使用第三方ORM框架,MyBatis或者Hibernate
java.sql.Statement
java.sql.statement是最原始的执行SQL的接口,使用它需要手动拼接SQL语句。
String sql = "SELECT * FROM user WHERE id = '" + id + "'"; // 字符串拼接sql语句
Statement statement = connection.createStatement();
statement.execute(sql); // 将sql字符串当作参数,并执行查询,此时存在sql注入问题
这种用法会导致存在sql注入的问题。
将id直接拼接到SQL语句并执行,所以可以构造语句进行注入。
java.sql.PrepareStatement
java.sql.PrepareStatement是一个接口,是对上面的Statement的扩展。
该方法实现了预编译的功能,拥有防护SQL的特性。
1. SQL预编译
- 数据库SQL语句编译特性:
数据库接受到sql语句之后,需要词法和语义解析,优化sql语句,制定执行计划。这需要花费一些时间。但是很多情况,我们的一条sql语句可能会反复执行,或者每次执行的时候只有个别的值不同(比如query的where子句值不同,update的set子句值不同,insert的values值不同)。
- 减少编译的方法
如果每次都需要经过上面的词法语义解析、语句优化、制定执行计划等,则效率就明显不行了。为了解决上面的问题,于是就有了预编译,预编译语句就是将这类语句中的值用占位符替代,可以视为将sql语句模板化或者说参数化。一次编译、多次运行,省去了解析优化等过程。
- 缓存预编译
预编译语句被DB的编译器编译后的执行代码被缓存下来,那么下次调用时只要是相同的预编译语句就不需要编译,只要将参数直接传入编译过的语句执行代码中(相当于一个函数)就会得到执行,并不是所以预编译语句都一定会被缓存,数据库本身会用一种策略(内部机制)。
- 预编译的实现方法
预编译是通过PreparedStatement和占位符来实现的。
2. 预编译作用:
- 预编译阶段可以优化 sql 的执行
预编译之后的 sql 多数情况下可以直接执行,DBMS 不需要再次编译,越复杂的sql,编译的复杂度将越大,预编译阶段可以合并多次操作为一个操作。可以提升性能。
- 防止SQL注入
使用预编译,而其后注入的参数将不会再进行SQL编译。也就是说其后注入进来的参数系统将不会认为它会是一条SQL语句,而默认其是一个参数,参数中的or或者and 等就不是SQL语法保留字了。
使用时,在SQL语句中,用 ? 作为占位符,代替需要传入的参数,然后将该语句传递给数据库,数据库会对这条语句进行预编译。如果要执行这条SQL,只要用特定的 set 方法,将传入的参数设置到SQL语句中的指定位置,然后调用 execute 方法执行这条完整的SQL。
示例如下:
String sql = "SELECT * FROM user WHERE id = ?"; // 预编译语句
PreparedStatement preparedStatement = connection.prepareStatement(sql); // 填入参数
preparedStatement.setString(1,reqStuId); // 设置占位符的值
preparedStatement.executeQuery(); // 执行查询
此时,如果我用之前的请求攻击,执行的SQL会变成 SELECT * FROM user WHERE id = ‘’or 1 #’,可以看到单引号是被转义了,同时参数也被一对单引号包裹,数字型注入也不存在了。
ORDER BY 排序注入
通过占位符传参,不管传递的是什么类型的值,都会被单引号包裹。
而使用 ORDER BY 时,要求传入的是字段名或者是字段位置。
示例如下:
String sql = "SELECT * FROM user ORDER BY " + column;
那么这样依然可能会存在SQL注入的问题,在 Java 中会有两种情况:
//order by不能使用预编译原理
String sql = " SELECT passwd FROM test_table1 WHERE username = ? ";
ps.setString(1, username)
// 会自动给值加上引号。比如假设username=“ls”,那么拼凑成的语句会是
String sql = " SELECT passwd FROM test_table1 WHERE username = 'ls' ";
// 再看order by,order by后一般是接字段名,而字段名是不能带引号的,比如 order by username;
// 如果带上引号成了order by 'username',那username就是一个字符串不是字段名了,这就产生了语法错误。
column 是字符串型
这种情况和 Statement 中描述的一样,是存在注入的。要防御就必须要手动过滤,或者将字段名硬编码到 SQL 语句中。
示例如下:
String column = "id";
String sql ="";
switch(column){ // 根据column来判断需要拼接的sql语句,尽量不要把用户从前端的输入拼接到sql语句中,尽量不信任用户的任何输入。
case "id": sql = "SELECT * FROM user ORDER BY id";
break;
case "username": sql = "SELECT * FROM user ORDER BY username";
break;
......
}
MyBatis
MyBatis简介
MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Ordinary Java Object,普通的 Java对象)映射成数据库中的记录。
MyBatis的使用方式主要有俩种,一种是使用注解,直接将SQL语句和方法绑定在一起,如下
package org.mybatis.example;
public interface BlogMapper {
@Select("SELECT * FROM blog WHERE id = #{id}")
Blog selectBlog(int id);
}
这种方式,适合简单的SQL语句,一旦语句长了,注释会变得复杂混乱,维护起来很麻烦,所以它只适合小项目(小项目用的也不多)。
用的最多的是第二种——XML配置,将SQL语句和Java代码分离,有独立的xml文件,描述某个方法会和某个SQL语句绑定。
每一个接口,在资源文件目录中,都有对应的xml。接口中的方法,和xml中id相同的SQL语句关联。
例如,IArticleCateDao 的 list()方法被调用,那么就会找到 ArticleCateMapper.xml中 id等于 list 的方法,执行它的 SQL,然后根据 resultMap 描述的 字段-属性 映射关系,返回相应的实例对象。
这里的 resultMap 具体如下:
<resultMap id="ArticleCateResult" type="ArticleCate">
<id column="id" jdbcType="INTEGER" property="id"/>
<result column="fid" jdbcType="INTEGER" property="fid"/>
<result column="name" jdbcType="VARCHAR" property="name"/>
<result column="status" jdbcType="INTEGER" property="status"/>
<result column="sort" jdbcType="INTEGER" property="sort"/>
</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 模糊查询。
示例如下:
<select id="selectStudentByFuzzyQuery" resultMap="studentMap">
SELECT * FROM student WHERE student.stu_name LIKE '%#{stuName}%'
</select>
在这里,MyBatis 会把 %#{stuName}% 作为要查询的参数,数据库会执行 SELECT * FROM student WHERE student.stu_name LIKE ‘%#{stuName}%’,导致查询失败,经过查询失败的原因是这么写经MyBatis转换后(‘%#{name}%’)会变为(‘%?%’),而(‘%?%’)会被看作是一个字符串,所以Java代码在执行找不到用于匹配参数的 ‘?’ ,然后就报错了。
所以这里只能用 ${} 的方式传入。而如果用 ${} 又存在SQL注入的风险,怎么办呢?
最好的方法是,使用数据库自带的 CONCAT ,将 % 和我们用 #{} 传入参数连接起来,这样就既不存在注入的问题,也能满足需求。
示例如下:
<select id="selectStudentByFuzzyQuery" resultMap="studentMap">
SELECT * FROM student WHERE student.stu_name LIKE CONCAT('%',#{stuName},'%')
</select>
Hibernate注入问题
Hibernate简介
Hibernate是一个开放源代码的对象关系映射框架,它对JDBC进行了非常轻量级的对象封装,它将POJO与数据库表建立映射关系,是一个全自动的orm框架,hibernate可以自动生成SQL语句,自动执行,使得Java程序员可以随心所欲的使用对象编程思维来操纵数据库。 Hibernate可以应用在任何使用JDBC的场合,既可以在Java的客户端程序使用,也可以在Servlet/JSP的Web应用中使用,最具革命意义的是,Hibernate可以在应用EJB的JavaEE架构中取代CMP,完成数据持久化的重任。起通常与 Struts、Spring 一起搭配使用,也就是我们熟知的 SSH 框架。
Hibernate简单架构
- 创建持久化类
- 创建对象-关系映射文件
- 创建Hibernate配置文件
- 通过Hibernate API编写访问数据库的代码
定位框架
直接查看相关的依赖即可:
<!-- 添加Hibernate依赖 -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>3.6.10.Final</version>
</dependency>
或者是相关的配置文件 hibernate.cfg.xml、hibernate.properties、*.hbm.xml。
例如hibernate.cfg.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- 1. 配置数据库信息 -->
<property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="hibernate.connection.password">ceshi</property>
<property name="hibernate.connection.url">jdbc:mysql://localhost:3306/test</property>
<property name="hibernate.connection.username">root</property>
<property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property>
<!-- 2. 其他配置 -->
<!-- 显示生成的SQL语句 -->
<property name="hibernate.show_sql">true</property>
<property name="format_sql">true</property><!-- 会格式化输出sql语句 -->
<!-- 3. 导入映射文件 -->
<mapping resource="/com/tk/sqlinject/hibernate/User.hbm.xml" />
</session-factory>
</hibernate-configuration>
除了上述方式外,还可以通过hibernate相关的注解来定位,例如
@Column
@Transient
@Table
.....
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注入:
Query<User> query = session.createQuery("from User where name='"+queryString+"'"); // hql拼接
但是HQL注入存在一定的局限性,不能像常规SQL注入一样利用。 其没有联合,没有可用的元数据表等。Hibernate查询语言没有那些在后台数据库中可能存在的功能特性。
举几个例子:
不支持 * 号
Query<User> query = session.createQuery("select * from User");
不支持Union
Query<User> query = session.createQuery("from User Union select 1,2,3,4,5");
不支持跨库查表,系统表也不行(未映射的表不可查询)
Query<User> query = session.createQuery("from hibernate.user4");
查询mysql的系统表information_schema同样会返回表not mapped:Query<User> query = session.createQuery("from information_schema.tables");
表名、列名大小写敏感
例如映射配置文件中将User Bean映射如下:<hibernate-mapping>
<class name="com.tk.sqlinject.hibernate.User" table="USER">
<id name="id" type="int">
<column name="ID"/>
<generator class="assigned"/>
</id>
......
</hibernate-mapping>
那么如果我们并没有考虑大小写,直接查询user的话是会产生not mapped报错的:
Query<User> query = session.createQuery("from user");
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)
- 是String类型,是SQL语句的条件部分。
- 参数是参数值。
- 参数是类型。
同样的,若第一个参数使用拼接的方式进行交互的话,会存在SQL注入风险的。举个例子:
List<User> resSql = criteria.add(Restrictions.sqlRestriction("id="+query)).list();
for (User user : resSql) {
System.out.println(user.toString());
}
query是我们用户输入,可以在log打印对应的sql语句,可以看到,倘若我们输入恶意的sql注入payload的话,是可以成功执行的:
SQLQuery本地SQL查询
Hibernate对原生SQL查询的支持和控制是通过SQLQuery接口实现的,这种方式弥补了HQL、Criterion查询的不足,其直接使用sql语句进行查询,在操作和使用上往往更加的自由和灵活,如果使用得当,数据库操作的效率还会得到不同程度的提升。一般复杂的sql都会用到它。
同样的,只要存在sql拼接,就会存在注入问题:
SQLQuery<User> sqlQuery = session.createSQLQuery("select * from User where Name='"+query+"'").addEntity(User.class);
同时因为其直接操作sql,与常见的SQL注入无异,没有特殊的限制。
PS:新版本hibernate已经弃用createSQLQuery(),可使用createNativeQuery()代替。
综上,其实Hibernate框架下的注入挖掘主要还是查看拼接。
Hibernate框架下SQL注入漏洞修复
主要方式是名称绑定,位置绑定(参数化查询)。
下面是具体实例:
HQL查询
名称绑定:
String sqlString = tom;
List<User> res =(List<User>)session.createQuery("select u from User u where name like :name")
.setParameter("name", "%" + sqlString + "%")
.list();
位置绑定:
Query<User> query = session.createQuery("from User where Name=?");
// Hibernate 4.1 之后对于HQL中查询参数的占位符做了改进
Query<User> query = session.createQuery("from User where Name=?0");
// HQL是从0开始的 query.setParameter(0, "tkswifty");
query.setString(0,name);
Criterion查询
对于Restrictions.sqlRestriction的情况,应该使用预编译的方式进行SQL查询:
List<User> resSql = criteria.add(Restrictions.sqlRestriction("name=?",query,StandardBasicTypes.STRING)).list();
同时,在mybatis中提到的例如in、like等查询方式,Criterion查询可以很好的解决预编译的问题:
Like
Criteria criteria = session.createCriteria(User.class);
List<User> resLike = criteria.add(Restrictions.like("name", "%test")).list();
打印log看出交互过程,可以看到熟悉的占位符?:
in
Criteria criteria = session.createCriteria(User.class);
List<User> resIn = criteria.add(Restrictions.in("name", new String[] {"test"})).list();
SQLQuery本地SQL查询
使用占位符预编译方式:
SQLQuery<User> sqlQuery = session.createSQLQuery("select * from User where Name=?0").addEntity(User.class);
sqlQuery.setParameter(0, query);
其他
虽然参数化查询是防止SQL注入最便捷有效的一种方式,但是其无法应用于所有场景。当使用执行前不可被占位符?替代的不可信数据来动态构建SQL语句时,必须要对不可信数据进行校验。
示例如下:Code oe = new OracleCodec();
String safeExpr = ESAPI.encoder().encodeForSQL(oe, exprString);
同样的,可以在过滤器filter中过滤一些常见的sql注入的敏感字符串,也是安全防护措施审计中不可缺少的一环。