23

Java安全编码之SQL注入

 3 years ago
source link: http://netsecurity.51cto.com/art/202008/624974.htm
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

随着互联网的发展,Java语言在金融服务业、电子商务、大数据技术等方面的应用极其广泛。Java安全编码规范早已成为SDL中不可或缺的一部分。本文以Java项目广泛采用的两个框架Hibernate和MyBatis 为例来介绍,如何在编码过程中避免SQL注入的几种编码方法,包括对预编译的深度解析,以及对预编译理解的几个“误区”进行了解释。

备注,本文是Java语言安全编码会是系列文章的第一篇。

1. 框架介绍

目前Hibernate和MyBatis为java项目广泛采用的两个框架。由于Hibernate使用方便,以前的项目采用Hibernate非常的广泛,但是后面由于Hibernate的侵入式特性,后面慢慢被MyBatis所取代 。下面我们会以SpringBoot为基础,分别搭建Hibernate和MyBatis的漏洞环境。

2. 配置说明

SpringBoot采用2.3.1.RELEASE,MySQL版本为5.7.20。数据库有一张表user_tbl。数据如下:

b642fa36c85584e6c443ebca488b78ca.jpeg-wh_651x-s_2954724511.jpeg

3. Hibernate

Hibernate 是一个开放源代码的对象关系映射框架,它对 JDBC 进行了非常轻量级的对象封装,是一个全自动的 ORM 框架。Hibernate 自动生成 SQL 语句,自动执行。

(1) 环境搭建

结构如下,ctl为控制层,service为服务层,dao为持久层。为了方便没有按照标准的接口实现,我们只关注漏洞的部分。

72607da6ecff19adbcd2df9e2ba46e61.jpeg-wh_600x-s_171528797.jpeg

Beans下User.java对用为user_tbl表结构。

0830f4a1085868e2a76b7b881e83f0d6.jpeg

我们使用/inject 接口,p为接受外部的参数,来查询User的列表,使用fastjson来格化式输出。

c320484eea68a4f9eb7c3aab049e07ca.jpeg-wh_600x-s_978127606.jpeg

我们回到dao层。

1)SQL注入

SQL注入我们使用字符串拼接方式:

38945e64981742716fba00ad5074b72d.jpeg-wh_600x-s_464872052.jpeg

访问http://localhost:8080/inject?p=m 直接用SQLMap跑一下:

581478606bba1db555cc8bdff5b1d95b.jpeg

很容易就注入出数据来了。

2)HQL注入

HQL(Hibernate Query Language)是Hibernate专门用于查询数据的语句,有别于SQL,HQL 更接近于面向对象的思维方式。表名就是对应我们上面的entity配置的。HQL注入利用比SQL注入利用难度大,比如一般程序员不会对系统表进行映射,那么通过系统表获取属性的几乎不可能的,同时由于HQL对于复杂的语句支持比较差,对攻击者来说需要花费更多时间去构造可用的payload,更多详细的语法可以参考:

https://docs.huihoo.com/Hibernate/reference-v3_zh-cn/queryhql.html

89f5c094f5b010360df0c2a767883d9f.jpeg-wh_600x-s_1057009310.jpeg

3)预编译

我们使用setParameter的方式,也就是我们熟知的预编译的方式。

Query query = (Query) this.entityManager.createQuery("from User u where u.userName like :userName ",User.class);  
query.setParameter("userName","%"+username+"%"); 

访问http://localhost:8080/inject?p=m后得到正常结果。

f931f2f739a6ad926748788f8c0909e1.jpeg-wh_600x-s_1452676077.jpeg

执行注入语句:

http://localhost:8080/inject?p=m’ or ‘1’ like ‘1 返回为空。

9d7e5877958ec3e5bc9e94e610a0ad53.jpeg

我们来看看setParameter的方式到底对我们的SQL语句做了什么。我们将断点打至Loader.class的bindPreparedStatement。发现通过预编译后,SQL变为了:

select user0_.id as id1_0_, user0_.password as password2_0_, user0_.username as username3_0_ from user_tbl user0_ where user0_.username like '%'' or ''1'' like ''1%', 

然后交给hikari处理。发现将我们的单引号变成了两个单引号,也就是说把传入的数据变为字符串。

e8eb7380d40fedcdb9174b890f0c9e2c.jpeg-wh_600x-s_1818787161.jpeg

将断点断至mysql-connector-java(也就是我们熟知的JDBC驱动包)的ClientPreparedQueryBindings.setString.这里就是参数设置的地方。

86fb02c9e91585719550fa2c663ba82e.jpeg-wh_600x-s_3008588886.jpeg

看一下算法:

String parameterAsString = x; 
 
            boolean needsQuoted = true; 
 
            if (this.isLoadDataQuery || this.isEscapeNeededForString(x, stringLength)) { 
 
                needsQuoted = false; 
 
                StringBuilder buf = new StringBuilder((int)((double)x.length() * 1.1D)); 
 
                buf.append('\''); 
 
                for(int i = 0; i < stringLength; ++i) { 
 
                    char c = x.charAt(i); 
 
                    switch(c) { 
 
                    case '\u0000': 
 
                        buf.append('\\'); 
 
                        buf.append('0'); 
 
                        break; 
 
                    case '\n': 
 
                        buf.append('\\'); 
 
                        buf.append('n'); 
 
                        break; 
 
                    case '\r': 
 
                        buf.append('\\'); 
 
                        buf.append('r'); 
 
                        break; 
 
                    case '\u001a': 
 
                        buf.append('\\'); 
 
                        buf.append('Z'); 
 
                        break; 
 
                    case '"': 
 
                        if (this.session.getServerSession().useAnsiQuotedIdentifiers()) { 
 
                            buf.append('\\'); 
 
                        } 
 
                        buf.append('"'); 
 
                        break; 
 
                    case '\'': 
 
                        buf.append('\''); 
 
                        buf.append('\''); 
 
                        break; 
 
                    case '\\': 
 
                        buf.append('\\'); 
 
                        buf.append('\\'); 
 
                        break; 
 
                    case '¥': 
 
                    case '₩': 
 
                        if (this.charsetEncoder != null) { 
 
                            CharBuffer cbuf = CharBuffer.allocate(1); 
 
                            ByteBuffer bbuf = ByteBuffer.allocate(1); 
 
                            cbuf.put(c); 
 
                            cbuf.position(0); 
 
                            this.charsetEncoder.encode(cbuf, bbuf, true); 
 
                            if (bbuf.get(0) == 92) { 
 
                                buf.append('\\'); 
 
                            } 
 
                        } 
 
                        buf.append(c); 
 
                        break; 
 
                    default: 
 
                        buf.append(c); 
 
                    } 
 
                } 
 
                buf.append('\''); 

可以看到mysql-connector-java主要是将将我们’转为了’’,对于转义的\会变为\\,比如对于这种SQL:

SELECT user0_.id AS id1_0_,user0_. PASSWORD AS password2_0_,user0_.username AS username3_0_ 
 
FROM user_tbl user0_ WHERE user0_.username LIKE '%\' or username = 0x6d #%' 

也会变为:

SELECT user0_.id AS id1_0_,user0_. PASSWORD AS password2_0_,user0_.username AS username3_0_ 
 
FROM user_tbl user0_ WHERE user0_.username LIKE '%\\'' or username = 0x6d #%' 

有人会说那我们使用select * from user_tbl where id = 1 and user() = 0x726f6f74406c6f63616c686f7374 这种类似的语句,全程没有jdbc里面的危险字符是不是就可以绕过了?mysql-connector-java里面有个非常巧妙的点是,他会根据你传入的类型判断。比如传入的为int类型。就会走setInt。传入的为string就会走setString。所以这段语句还是会被select * from user_tbl where id = 1 ‘and user() = 0x726f6f74406c6f63616c686f7374’

我们看到SQL预编译的算法也是非常简单。

4. MyBatis

MyBatis是一流的持久性框架,支持自定义SQL,存储过程和高级映射。MyBatis可以使用简单的XML或注释进行配置。现在目前国内大部分公司都是采用的MyBatis框架。

(1) 环境搭建:

下面为我们项目目录结构:

d78ff725330cf4328b501dbdfde0f9cb.jpeg-wh_600x-s_268201178.jpeg

(2) 使用#{}的方式

#{}也就是我们熟知的预编译方式。

d1633882882c297ec7343c08b02e2def.jpeg-wh_600x-s_3471468643.jpeg

访问http://localhost:8080/getList?p=m 后正常的返回:

80cc237d2829e465dbda662383587ab8.jpeg-wh_600x-s_2207843173.jpeg

使用http://localhost:8080/getList?p=m' or ‘1’ like ‘1

结果返回为空。不存在注入。

我们将断点断在PreparedStatementLogger的invoke方法上面,其实这里就是一个代理方法。这里我们看到完整的SQL语句。

e79d83f18fb51e44c9bbbbe813e89dbd.jpeg-wh_600x-s_262196082.jpeg

同样我们将断点断在:ClientPreparedQueryBindings.setString同样会进去

95a407f11176a21acb9ea0c5b0704f55.jpeg-wh_600x-s_198807512.jpeg

Hibernate和MyBatis的预编译机制是一样的。

(3) 使用${}的方式

${}的方式也就是MyBatis的字符串连接方式。

e101639fdd8761366c083012977ed2f7.jpeg-wh_600x-s_2307604988.jpeg

使用SQLMap很容易就能跑出数据:

ff7e573b45bc77f6aaae01961a60bb79.jpeg

(4) 关于OrderBy

之前有听人说Order By后面的语句是不会参与预编译?这句话是错误的。Order By也是会参与预编译的。从我们上面的jdbc的setString算法可以看到,是因为setString会在参数的前后加上’’,变成字符串。导致Order By失去了原本的意义。只能说是预编译方式的Order By不适用而已。所以对于这种Order By的防御的话建议是直接写死在代码里面。对于Order By方式的注入我们可以通过返回数据的顺序的不同来获取数据。

37542c4018c1896f3fd87b99c758be78.jpeg-wh_600x-s_2985698202.jpeg

(5) 关于useServerPrepStmts

其实在只有JDBC在开启了useServerPrepStmts=true的情况下才算是真正的预编译。但是如果是字符串的拼接方式,预编译是没有效果的。从MySQL的查询日志就可以开看到。可以看到Prepare的语句。一样是存在SQL注入的。

1f74c938486a28b92865f72e5fee4d06.jpeg-wh_600x-s_4201726750.jpeg

我们使用占位符的方式:

f990eec4c2513b029371b9d2fdcf6f71.jpeg-wh_600x-s_2905454461.jpeg

上面的语句就不存在SQL注入了。

我想这就是JDBC默认为啥不开启useServerPrepStmts=true的原因吧。

5. 总结

在能使用预编译的情况下我们应该要使用预编译。在不能使用预编译的情况下,可以对特定类型做规范,比如传数字的需要规范为Integer,Long等。这样会在进入数据库前会提前抛出异常。或者使用Spring的AOP机制,添加一个前置的fitler,对有害的字符清洗或者过滤。但是这样有点笼统,会对全局参数进行清洗。

还有一种比较好的方式是,通过注解的方式,这样会比较方便,可复用性也很好。对不能进行预编译的参数加上过滤有害字符的注解。我们就不在这里做代码的实现,网上有很多可以参考的教程。可以使用Apache Jakarta Commons提供的很多方便的方法来过滤有害字符。

【责任编辑:赵宁宁 TEL:(010)68476606】


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK