• Java中的SQL注入简易分析


    JDBC

    什么是JDBC?JDBC是Java DataBase Connectivity的缩写,它是Java程序访问数据库的标准接口。

    使用Java程序访问数据库时,Java代码并不是直接通过TCP连接去访问数据库,而是通过JDBC接口来访问,而JDBC接口则通过JDBC驱动来实现真正对数据库的访问。

    ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
    
    │  ┌───────────────┐  │
       │   Java App    │
    │  └───────────────┘  │
               │
    │          ▼          │
       ┌───────────────┐
    │  │JDBC Interface │<─┼─── JDK
       └───────────────┘
    │          │          │
               ▼
    │  ┌───────────────┐  │
       │  JDBC Driver  │<───── Vendor
    │  └───────────────┘  │
               │
    └ ─ ─ ─ ─ ─│─ ─ ─ ─ ─ ┘
               ▼
       ┌───────────────┐
       │   Database    │
       └───────────────┘
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    通过SQLInjection.java代码对于JDBC中对于数据的调用使用预编译和不使用预编译两种情况进行分析

    不使用占位符拼接

    Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
    Statement stmt = conn.createStatement();
    String sql = "select name from students where id =" + value;
    ResultSet rs = stmt.executeQuery(sql);
    
    • 1
    • 2
    • 3
    • 4

    不使用占位符拼接的关键代码如上,先通过Connection提供的createStatement()方法创建一个stmt对象,用于执行一个查询.然后执行stmt对象提供的executeQuery(sql)传入我们构造的SQL语句并获得返回的结果集,使用ResultSet来引用结果集.

    而在执行的关键代码中,先是把sql语句传入Statementlmpl.class当中的ResultSet executeQuery()方法中,在locallyScopedConn.execSQL()中执行SQL并将执行结果放入到this.result
    请添加图片描述

    请添加图片描述

    通过追踪execSQL()方法可以追溯到Connectionlmpl.class文件,我们可以看到在将sql语句接收到方法中后,将语句交由MysqlIO来执行.
    请添加图片描述

    请添加图片描述

    查看sqlQueryDirect()方法,通过拼装发送包信息,最后通过Buffer resultPacket = this.sendCommand(3, (String)null, queryPacket, false, (String)null, 0);中的sendCommand()方法将其发送出去
    请添加图片描述

    请添加图片描述

    纵观在Statementlmpl.class当中的ResultSet executeQuery()方法中只是将我们的sql语句进行一步步的传递,大部分只进行了功能上的校验,在最后发送到数据库进行执行,通过names列表可以看到数据库中所有的名字都被读取了出来.
    请添加图片描述

    使用占位符(PreparedStatement)

    String sql = "select name from students where id =?";
    PreparedStatement preparedStatement = conn.prepareStatement(sql);
    preparedStatement.setString(1, value);
    ResultSet resultSet = preparedStatement.executeQuery();
    
    • 1
    • 2
    • 3
    • 4

    使用PreparedStatement预编译方法,对于要传递的id的值先使用?进行占位,并且把数据连同sql本身传给数据库,以此保证每次传给数据库的SQL语句是相同的,只是占位符的数据不同.

    我们传递数据1 or 1=1进行测试,以此来学习在PrepareStatement对于我们传入的数据的处理过程.

    PrepareStatement的开启与关闭情况

    在对prepareStatement()方法进行调试的时候,我们需要了解一个关于预编译的知识点.

    预编译功能跟MySQL版本及 MySQL Connector/J(JDBC驱动)版本都有关,首先MySQL服务端是在4.1版本之后才开始支持预编译的,之后的版本都默认支持预编译,并且预编译还与 MySQL Connector/J(JDBC驱动)的版本有关, Connector/J 5.0.5之前的版本默认支持预编译, Connector/J 5.0.5之后的版本默认不支持预编译, 所以我们用的Connector/J 5.0.5驱动以后版本的话默认都是没有打开预编译的 (如果需要打开预编译,需要配置 useServerPrepStmts 参数)

    因为我的测试环境为5.1.47,所以目前版本的预编译默认是关闭的
    请添加图片描述

    所以我们运行代码,在初始状态下的mysql查询日志是这样的
    请添加图片描述

    而在数据库链接中加入useServerPrepStmts=true后mysql的查询日志为

    请添加图片描述

    我们可以看到查询比之前多了一条Prepare数据,表示着预编译开启成功

    我们回到代码调试中,当代码执行到PreparedStatement preparedStatement = conn.prepareStatement(sql);时,我们通过断点调试可以进入到ConnectionImpl,javaprepareStatement()方法当中.
    请添加图片描述

    当我们没有设置useServerPrepStmts=true时,在prepareStatement()方法当中useServerPreparedStmts属性为false,直接跳过当前代码块进入到最后的else代码块
    请添加图片描述

    请添加图片描述

    最后并不会向数据库提交SQL预编译请求

    而我们设置useServerPrepStmts=true后,再次调试代码,会发现useServerPreparedStmts属性为true,最后向数据库提交SQL预编译请求
    请添加图片描述

    请添加图片描述

    我们继续调试代码到ResultSet resultSet = preparedStatement.executeQuery(),进入setString()方法

    在没有开启预编译的情况下,会进入PreparedStatement.class,在其中的isEscapeNeededForString()方法中对于用户输入的数据中的非法字符进行转义,最后交由数据库端进行运行.

    请添加图片描述

    而开启了预编译的情况下,会进入ServerPreparedStatement.class,最后数据交由mysql端进行转义处理

    在预编译情况下对于order by,like的危害

    order by

    ORDER BY关键字用于按升序或降序对结果集进行排序。

    order by后一般接字段名,而字段名是不能带引号的,比如order by id,如果使用预编译,id在预编译的过程中会被setString()方法自动加引号,而如果带上引号之后就成了order by 'id',现在id就是一个字符串而不是一个字段名了,会产生语法错误.

    请添加图片描述

    请添加图片描述

    可以看到拼接后的sql语句中order by的参数就是字符串,我们在数据库中运行查看
    请添加图片描述

    请添加图片描述

    可以看出经过引号包裹的语句没有起作用

    所以在开发过程中,不能参数化的位置,不管怎么拼接,最终都是和使用"+"号拼接字符串的功效一样:拼成了sql语句但没有防sql注入的效果.

    我们可以通过构造if语句来对order by以后的语句进行构造进行SQL注入
    请添加图片描述

    所以需要对order by参数进行特殊的过滤

    like

    在使用like时,通过平常的sql语句进行构造select * from students where name like '%?%'会报错,所以有时我们看到的代码中会出现拼接形态的like语句,此时就很有可能出现sql注入漏洞
    请添加图片描述

    正确的like预编译构造方法如下图所示,需要在setString()方法中将%构造出来
    请添加图片描述

    Mybatis

    Mybatis解析执行过程

    引用一下先知社区R17a大佬的过程图:
    请添加图片描述

    以查询SQL分析,主要步骤如下:
    1.SqlSession创建过程:SqlSessionFactoryBuilder().build(inputStream)创建一个SqlSession,创建的时候会进行配置文件解析生成Configuration属性实例,解析时会将mapper解析成MapperStatement加到Configuration中,MapperStatement是执行SQL的必要准备,SqlSource是MapperStatement的属性,实例化前会先创建动态和非动态SqlSource即DynamicSqlSourceRawSqlSourceDynamicSqlSource对应解析$以及动态标签如foreach,RawSqlSource创建时解析#并将#{}换成占位符?

    2.执行准备过程:DefaultSqlSession.selectOne()执行sql(如果是从接口getMapper方式执行,首先会从MapperProxy动态代理获取DefaultSqlSession执行方法selectxxx|update|delete|insert),首先从Configuration获取MapperStatement,执行executor.query()。executor执行的第一步会先通过MapperStatement.getBoundSql()获取SQL,此时如果MapperStatement.SqlSource是动态即DynamicSqlSource,会先解析其中的动态标签比如${}会换成具体传入的参数值进行拼接,获取到SQL之后调用executor.doQuery(),如果存在预编译首先会调用JDBC处理预编译的SQL,最终通过PreparedStatementHandler调用JDBC执行SQL;

    3.JDBC执行SQL并返回结果集

    调试代码(${}和#{}使用的不同)

    在通过mybatis数据操作的过程中,在XMLScriptBuilder.parseScriptNode()处会因为${}#{}使用的不同执行不同的方法

    请添加图片描述

    在进入parseScriptNode()后,先通过parseDynamicTags()方法中的TextSqlNode.isDynamic()判断是否存在${}标志来区分动态和非动态SqlSource

    请添加图片描述

    TextSqlNode.isDynamic()首先会通过DynamicCheckerTokenParser()中的GenericTokenParser()创建一个${}标识符解析

    请添加图片描述

    继续下一步调用GenGenericTokenParser.parse对我们的SQL语句进行校验

    ${}分析

    请添加图片描述

    parse()中可以看到如果在我们sql语句中发现了${那么继续执行,如果没有就直接返回,而在继续执行的最后调用了builder.append(this.handler.handleToken(expression.toString())),在handler.handleToken中将isDynamic更改为了true
    请添加图片描述

    请添加图片描述

    isDynamic为true,会实例化一个DynamicSqlSource对象,返回sqlSource
    请添加图片描述

    #{}分析

    从上面关于${}的分析可以知道,如果我们的sql语句构造为#{},那么将在XMLScriptBuilder.parseScriptNode方法中使用RawSqlSource来构造sqlSource

    在过程中同样经过GenGenericTokenParser.parse对我们的SQL语句进行校验,在其中将#{}替换成了?
    请添加图片描述

    请添加图片描述

    like

    在mybatis中错误的like使用语句为select * from user where name like "%${id}%"

    通过构造id = 1%" or 1=1 # 使最后在数据库执行select * from user where name like "%1%" or 1=1 # %"

    请添加图片描述

    正确的构造方法应该为SELECT * FROM user where name like concat('%',#{name}, '%')

    order by

    至于oeder by的话,和JDBC中的分析相似,如果order by后面跟的变量的话,应该进行校验和过滤

    参考

    https://www.liaoxuefeng.com/wiki/1252599548343744/1321748435828770
    https://juejin.cn/post/6844903490058190862
    https://xz.aliyun.com/t/10593
    https://xz.aliyun.com/t/10686

  • 相关阅读:
    [附源码]Python计算机毕业设计Django通用病例管理系统
    0动态规划中等 LeetCode467. 环绕字符串中唯一的子字符串
    Rust 流程控制和循环控制
    m无线通信的调制解调过程的matlab仿真,包括ASK,FSK,PSK
    springcloud二手交易平台系统源码
    ros之乌龟做圆周运动and订阅乌龟的位姿信息
    「贪心笔记」通过最少操作次数使得数组的和相等
    云原生之容器化:Dockerfile详解
    2:set和map解决力扣题
    《六月集训》(第二十六天)——并查集
  • 原文地址:https://blog.csdn.net/Destiny_one/article/details/126156823