SQL是操作数据库数据的结构化查询语言,web应用程序的应用数据与后台数据库中的数据产生交互时会采用SQL。而SQL注入(SQL Injection)是当开发未对web应用程序用户可控输入的参数、web表单、cookie等(即注入点)进行规范性校验、过滤和数据清洗,将用户输入的参数以拼接的方式带入了SQL语句中,造成了SQL注入的产生。攻击者通过构造特殊的payload,后端程序以拼接的方式将payload带入SQL语句中,数据库执行拼接的SQL,使得攻击者直接窃取数据库信息、提权,造成信息泄露。因此SQL注入在多年稳居OWASP TOP 10榜首位置。
任何一个基于SQL语言的数据库都有可能会被攻击,很多的开发人员在编写web应用程序时未对用户可控输入的参数、web表单、cookie等进行规范性校验、过滤和数据清洗,都很容易出现SQL注入漏洞。
SQL语句一般都嵌入在HTTP请求中,很难与正常语句进行区分,并且SQL注入的payload变种极多,攻击者通过调整攻击参数可进行绕过。
攻击者通过SQL注入获取到服务器的库名、表名、字段名,从而获取到整个服务器中的数据,对网站用户的数据安全具有极大的威胁。攻击者也可通过获取到的数据,得到后台管理员的密码,从而拿下整个网站。不仅对数据库信息安全造成威胁,对整个数据库系统安全影响极大。
网上有很多的SQL注入工具,如SQLmap、傀儡SQL注入批量扫描工具等,git上面也有很多人写的注入exp。
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 '"
在实际的开发过程中,JDBC是将SQL语句写在代码块中的,不利于后期的维护。现如今,很多的Java项目或多或少会使用对JDBC进行抽象封装的持久化框架,如Mybatis和Hibernate。通常框架已对SQL注入进行了防御,但开发人员未恰当使用框架的情况下,仍会纯在SQL注入的风险。
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维护一条
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pjGVATHZ-1628248437780)(1.png)]
(图片来自《深入理解mybatis原理》 MyBatis的架构设计以及实例分析)
Mybatis框架的思想是将SQL语句编入配置文件中,避免SQL语句在代码中大量出现,方便后续对SQL语句的修改与配置。正确使用Mybatis框架可以有效的阻止SQL注入的产生。
$与#的区别
#{}:占位符号(在对数据解析时会自动添加’ ')
${}:SQL拼接符号(替换结果,不会添加’ ',like和order by 后面使用,存在SQL注入问题,需手动添加过滤)
两者使用
一般来说能用#{}的都用#{},首先是为性能考虑,相同的预编译SQL可以重复使用,其次${}在预编译之前已经将变量进行替换,容易产生SQL注入问题。
表名、字段名和order by后的做为变量时必须使用${},主要原因是表名、字段名和order by 后是字符串,使用SQL占位符替换字符串时会带上’ ',这样会是SQL报错。
代码解析
使用${}构造的代码如下
当输入"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语句中的order by后面的参数无法使用预编译赋值,此时就需要开发对用户传入的参数进行校验、数据清洗。
本文记录结合MS08067的《java代码审计 入门篇》学习整理。MS08067团队的各位师傅非常强。