JAVA代码审计之SQL注入,基于Spring boot和jdbc以及mybatis

目录

一、新建一个spring boot的有sql环境的工程

二、JDBC的SQL注入

2.1 java.sql.Statement

2.2 示例代码

 2.3  预编译防御以及非正确使用

2.4 order by注入

三、Mybatis的SQL注入

3.1 示例代码

3.2 orderby 注入

 3.3 in注入

 3.4 like注入

 四、SQL注入漏洞修复

4.1 白名单

4.2 预编译(jdbc)

4.3 预编译(mabatis)

本节讲述JavaWeb代码审计中存在SQL注入漏洞的情况。

基于spring boot,mysql,两种连接方式:jdbc和mybatis。

代码比较多,适合自己创建项目,跟着走一遍,如果只想了解原理,可以忽略代码,只看大概即可。比如Controller类或者sql语句的编写。

SQL注入漏洞是对数据库进行的一种攻击方式。 其主要形成方式是在数据交互中,前端数据通过后台在对数据库进行操作时,由于没有做好安全防护,导致攻击者将恶意代码拼接到请求参数中,被当做SQL语句的一部分进行执行,最终导致数据库被攻 击。 可以说所有可以涉及到数据库增删改查的系统功能点都有可能存在SQL注入漏洞。 虽然现在针对SQL注入的防护层出不穷,但大多情况下由于开发人员的疏忽或特定的使用场景,还是会存在SQL注入漏洞的代码。

一、新建一个spring boot的有sql环境的工程

选择这四个依赖进入

 数据库创建好  加点数据,我用的phpstudy,mysql。

 项目结构如下

 打开 src/main/resources/application.properties 配置文件,将以下数据库连接信息添加至配置中


#访问端口号
server.port=7089
#数据库连接信息
spring.datasource.url=jdbc:mysql://localhost:3306/jdbcdemo?AllowPublicKeyRetrieval=true&useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

二、JDBC的SQL注入

动态拼接

SQL语句动态拼接导致的SQL注入漏洞是先前最为常见的场景。 其主要原因是后端代码将前端获取的参数动态直接拼接到SQL语句中使用 java.sql.Statement 执行 SQL语句从而导致SQL注入漏洞的出现。 

在这里关键点有两个:①、动态拼接参数。②、使用 java.sql.Statement 执行SQL语句。

2.1 java.sql.Statement

Statement 对象用于执行一条静态的 SQL 语句并获取它的结果。 createStatement() :创建一个 Statement 对象,之后可使用 executeQuery() 方法执行SQL语句。 executeQuery(String sql) 方法:执行指定的 SQL 语句,返回单个 ResultSet 对象。

2.2 示例代码

在 src\main\java\com\example\demo\jdbcinjection 下新建一个名为 JdbcDynamicController 的 Java Class。

package com.example.sqlinjection.jdbcinjection;


import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.sql.*;

@RestController
@RequestMapping("/jdbcsql")
public class JdbcDynamicController {
    private static String driver = "com.mysql.cj.jdbc.Driver";
    @Value("${spring.datasource.url}")
    private String url;
    @Value("${spring.datasource.username}")
    private String user;
    @Value("${spring.datasource.password}")
    private String password;

    @RequestMapping("/dynamic")
    public String jdbcdynamic(@RequestParam("id") String id) throws ClassNotFoundException, SQLException {
        StringBuilder result = new StringBuilder();
        Class.forName(driver);
        Connection conn = DriverManager.getConnection(url, user, password);
        Statement statement = conn.createStatement();
        String sql = "select * from user where id = '" + id + "'";
        ResultSet rs = statement.executeQuery(sql);
        while (rs.next()) {
            String rsUsername = rs.getString("username");
            String rsPassword = rs.getString("password");
            String info = String.format("%s: %s\n", rsUsername, rsPassword);
            result.append(info);
        }
        rs.close();
        conn.close();
        return result.toString();



    }
}

解释一下,StringBuilder是一个类,可以追加字符串,用于字符串拼接。

单引号闭合的sql语句。

使用闭合1' or 1=1%23,直接爆出了所有数据,可以看到是有sql注入的 。

 执行sql语句也是没问题的

 2.3  预编译防御以及非正确使用

在动态拼接中是使用Statement执行SQL语句。如果使用 PreparedStatement 预编译参数化查询是能够 有效防止SQL注入的。 但如果没有正确的使用 PreparedStatement 预编译还是会存在SQL注入风险的。

PreparedStatement是继承Statement的子接口。 PreparedStatement 会对SQL语句进行预编译,不论输入什么,经过预编译后全都以字符串来执行SQL 语句。 PreparedStatement会先使用 ? 作为占位符将SQL语句进行预编译,确定语句结构,再传入参数进行执 行查询。如下述代码:

String sql = "select * from users where username = ?";
PreparedStatement preparestatement = conn.prepareStatement(sql);
preparestatement.setString(1, username);

但如果没有正确的使用 PreparedStatement 预编译还是会存在SQL注入风险的。 简单来说,可能由于开发人员疏忽或经验不足等原因,虽然使用了预编译 PreparedStatement ,但没 有根据标准流程对参数进行标记,依旧使用了动态拼接SQL语句的方式,进而造成SQL注入漏洞。 在 src\main\java\com\example\demo\jdbcinjection 下新建一个名为 JdbcPrepareStatement 的 Java Class,并键入以下代码。

package com.example.sqlinjection.jdbcinjection;


import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.sql.*;

@RestController
@RequestMapping("/jdbcsql")
public class JdbcPrepareStatement {
    private static String driver = "com.mysql.cj.jdbc.Driver";
    @Value("${spring.datasource.url}")
    private String url;
    @Value("${spring.datasource.username}")
    private String user;
    @Value("${spring.datasource.password}")
    private String password;

    @RequestMapping("/preSec")
    public String jdbcPreSec(@RequestParam("id") String id) throws ClassNotFoundException, SQLException {
        StringBuilder result = new StringBuilder();
        Class.forName(driver);
        Connection conn = DriverManager.getConnection(url, user, password);
        String sql = "select * from user where id=?";
        PreparedStatement preparedStatement = conn.prepareStatement(sql);
        preparedStatement.setString(1,id);

        ResultSet rs = preparedStatement.executeQuery();
        while (rs.next()) {
            String resUsername = rs.getString("username");
            String resPassword = rs.getString("password");
            String info = String.format("%s: %s\n", resUsername, resPassword);
            result.append(info);
        }
        rs.close();
        conn.close();
        return result.toString();
    }

    @RequestMapping("preNot")
    public String jdbcPreNot(@RequestParam("id") String id) throws SQLException, ClassNotFoundException {
        StringBuilder result = new StringBuilder();
        Class.forName(driver);
        Connection conn = DriverManager.getConnection(url, user, password);
        //还是直接进行了拼接 无效
        String sql = "select * from user where id = '" + id + "'";
        PreparedStatement preparestatement = conn.prepareStatement(sql);
        ResultSet rs = preparestatement.executeQuery();
        while (rs.next()) {
            String reUsername = rs.getString("username");
            String resPassword = rs.getString("password");
            String info = String.format("%s: %s\n", reUsername, resPassword);
            result.append(info);
        }
        rs.close();
        conn.close();
        return result.toString();

    }

}

正确的预编译代码效果:

 错误的预编译代码效果:

2.4 order by注入

在SQL语句中, order by 语句用于对结果集进行排序。 order by 语句后面需要是字段名或者字段位 置。 在使用 PreparedStatement 预编译时,会将传递任意参数使用单引号包裹进而变为了字符串。 如果使用预编译方式执行 order by 语句,设置的字段名会被人为是字符串,而不再是字段名。 因此,在使用 order by 时,就不能使用 PreparedStatement 预编译了

下新建一个名为 jdbcOrderby 的Java Class, 并键入以下代码

package com.example.sqlinjection.jdbcinjection;


import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.sql.*;

@RestController
@RequestMapping("/jdbcsql")
public class jdbcOrderby {
    private static String driver = "com.mysql.jdbc.Driver";
    @Value("${spring.datasource.url}")
    private String url;
    @Value("${spring.datasource.username}")
    private String user;
    @Value("${spring.datasource.password}")
    private String password;
    @RequestMapping("/PreOrderby")
    public String jdbcOrderby(@RequestParam("id") String id) throws
            ClassNotFoundException, SQLException {
        StringBuilder result = new StringBuilder();
        Class.forName(driver);
        Connection conn = DriverManager.getConnection(url, user, password);
        String sql = "select * from user" + " order by " + id;
        PreparedStatement preparestatement = conn.prepareStatement(sql);
        ResultSet rs = preparestatement.executeQuery();
        while (rs.next()) {
            String reUsername = rs.getString("username");
            String resPassword = rs.getString("password");
            String info = String.format("%s: %s\n", reUsername, resPassword);
            result.append(info);
        }
        rs.close();
        conn.close();
        return result.toString();
    }
}

利用报错函数,页面并没有返回值,但是在后端可以看到 

可以结合if进行盲注

 

也可以直接尝试用sqlmap跑一下,这里就不再多说了。

三、Mybatis的SQL注入

MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎 所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映 射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

在Mybatis中拼接SQL语句有两种方式:一种是占位符 #{} ,另一种是拼接符 ${} 。

 占位符 #{} :对传入的参数进行预编译转义处理。类似JDBC中的 PreparedStatement 。 

拼接符 ${} :对传入的参数不做处理,直接拼接,进而会造成SQL注入漏洞。

 #{} 可以有效防止SQL注入漏洞。 ${} 则无法防止SQL注入漏洞。 因此在我们对JavaWeb整合Mybatis系统进行代码审计时,应着重审计SQL语句拼接的地方。 除非开发人员的粗心对拼接语句使用了 ${} 方式造成的SQL注入漏洞。 

在Mybatis中有几种场景是不能使用预编译方式的,比如: order by 、 in , like 。

3.1 示例代码

代码较多。

 在 src.main.java.com.example.demo.mybatisinjection 包下新建一个Java Class,名为 User ,这是一个实体类,和数据表做下映射,键入以下代码

package com.example.sqlinjection.mybatisinjection;

public class User {
    private int id;
    private String username;
    private String password;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
    @Override
    public String toString() {
        return "User [id=" + id + ", username=" + username + ", password=" +
                password + "]";
    }
}

在 src.main.java.com.example.demo.mybatisinjection 文件下新建一个名为 UserMapper 的 Java Interface,键入以下代码

package com.example.sqlinjection.mybatisinjection;


import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

@Mapper
public interface UserMapper {
    List<User> orderbyInjection(@RequestParam("sort") String sort);
    @Select("select * from user where id in (${params})")
    List<User> inInjection(@Param("params")String params);
    List<User> likeInjection(@Param("username") String username);
//Mybatis查询SQL语句的另一种使用注解方式,这也是存在SQL注入的。
//@Select("select * from users where username = '${username}'")
//List<User> likeInjection(@Param("username") String username);

}

在 src.main.resources 文件下先新建名为 mapper 的文件夹,再新建一个名为 UserMapper.xml 文件,与dao层的 UserMapper 做好映射绑定,键入以下代码

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.sqlinjection.mybatisinjection.UserMapper">
<resultMap type="com.example.sqlinjection.mybatisinjection.User" id="User">
<id column="id" property="id" javaType="java.lang.Integer" jdbcType="NUMERIC"/>
<id column="username" property="username" javaType="java.lang.String" jdbcType="VARCHAR"/>
<id column="password" property="password" javaType="java.lang.String" jdbcType="VARCHAR"/>
</resultMap>
<select id="orderbyInjection" parameterType="String" resultMap="User">
select * from user order by ${sort} asc
</select>
<select id="likeInjection" parameterType="String" resultMap="User">
select * from user where username like '%${username}%'
</select>
</mapper>

在 src.main.java.com.example.demo.mybatisinjection 文件下新建一个名为 MybatisController 的Java class,键入以下代码

package com.example.sqlinjection.mybatisinjection;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/mybatisSql")
public class MybatisController {
    @Autowired
    private UserMapper userMapper;
    //orderby查询 http://localhost:7089/sqli/mybatis/orderby?sort=username
    @GetMapping("/orderby")
    public List<User> orderbySql(@RequestParam("sort") String sort) {

        return userMapper.orderbyInjection(sort);
    }
    //in查询 http://localhost:7089/sqli/mybatis/in?params=1
    @GetMapping("/in")
    public List<User> inSql(@RequestParam("params") String params) {
        return userMapper.inInjection(params);
    }
    //Like查询 http://localhost:7089/sqli/mybatis/like?username=admin
    @GetMapping("/like")
    @ResponseBody
    public List<User> likeSql(@RequestParam("username") String username){
        return userMapper.likeInjection(username);
    }
}

最后,在 src\main\resources\application.properties 配置文件中添加以下代码,最终如下 图所示


#?????
server.port=7089
#???????
spring.datasource.url=jdbc:mysql://localhost:3306/jdbcdemo?AllowPublicKeyRetrieval=true&useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
## Mybatis ??
## mybatis.typeAliasesPackage ???com.example.demo.dao??????????
mybatis.typeAliasesPackage=com.example.sqlinjection.mybatisinjection
## mybatis.mapperLocations ??? classpath ??? mapper ???* ??????? xml???
mybatis.mapperLocations=classpath:mapper/*.xml


3.2 orderby 注入

可以看到有明显的sleep现象,sql语句被执行,可以尝试盲注.

 3.3 in注入

IN语句 :常用于where表达式中,其作用是查询某个范围内的数据。

比如: select * from where field in (value1,value2,value3,…); 如上所示,in在查询某个范围数据是会用到多个参数,在Mybtis中如果直接使用占位符 #{} 进行查询会 将这些参数看做一个整体,查询会报错。

因此很多开发人员可能会使用拼接符 ${} 对参数进行查询,从而造成了SQL注入漏洞。

比如: select * from users where id in (${params})

正确的做法是需要使用foreach配合占位符 #{} 实现IN查询。比如:

<!-- where in 查询场景 -->
<select id="select" parameterType="java.util.List" resultMap="BaseResultMap">
SELECT *
FROM user
WHERE name IN
<foreach collection="names" item="name" open="(" close=")" separator=",">
#{name}
</foreach>
</select>

 3.4 like注入

使用like语句进行查询时如果使用占位符 #{} 查询时程序会报错(大家可自行调试)。

比如: select * from users where username like '%#{username}%' 因此经验不足的开发人员可能会使用拼接符 ${} 对参数进行查询,从而造成了SQL注入漏洞。 比如: select * from users where username like '%${username}%' 下面代码是正确的做法,可以防止SQL注入漏洞,如下。

SELECT * FROM users WHERE name like CONCAT("%", #{name}, "%")

 四、SQL注入漏洞修复

php的sql注入漏洞修复也可以用在这里,经常用到正则匹配过滤危险输入。java的话这里主要是说关于预编译的修复。

通过上面我们其实知道,一般情况,预编译是可以解决所有输入的。因为一般情况,所有输入都会被当成字符串。但是在面对oeder by等查询时,预编译就没用了,原因是:order by后一般是接字段名,而字段名是不能带引号的,比如 order by username;如果带上引号成了order by 'username',那username就是一个字符串不是字段名了,这就产生了语法错误。预编译只有自动加引号的setString()方法,没有不加引号的方法;而另一方面order by后接的字段名不能有引号。

事实上,凡是字符串但又不能加引号的位置都不能参数化;包括sql关键字、库名表名字段名函数名等等。

不能参数化的位置,不管怎么拼接,最终都是和使用“+”号拼接字符串的功效一样:拼成了sql语句但没有防sql注入的效果。但好在的一点是,不管是sql关键字,还是库名表名字段名函数名对于后台开发者来说他的集合都是有限的,更准确点应该说也就那么几个。所以考虑可以使用黑白名单等加以限制。

4.1 白名单

正则过滤就不多说了,可以自己编写过滤特定关键字和函数名。

// 插入数据用户可控时,应使用白名单处理
// example for order by

String orderBy = "{user input}";
String orderByField;
switch (orderBy) {
    case "name":
        orderByField = "name";break;
    case "age":
        orderByField = "age"; break;
    default:
        orderByField = "id";
}

这里对列名进行白名单处理,所有的列名都在上面,但凡输入不是白名单的都会被赋值成id。

4.2 预编译(jdbc)

一般情况就预编译,正常写。特殊场景如下。

// like 模糊查询场景
String sql = "SELECT * FROM users WHERE name like ?";
PreparedStatement pre = conn.prepareStatement(sql);
pre.setString(1, "%"+name+"%");
ResultSet rs = pre.executeQuery();

// where in 查询场景
String sql = "select * from user where id in (";
Integer[] ids = new Integer[]{1,2,3};

StringBuilder placeholderSql = new StringBuilder(sql);
for(int i=0,size=ids.length;i<size;i++) {
    placeholderSql.append("?");
    if (i != size-1) {
        placeholderSql.append(",");
    }
}
placeholderSql.append(")");

PreparedStatement pre = conn.prepareStatement(placeholderSql.toString());
for(int i=0,size=ids.length;i<size;i++) {
    pre.setInt(i+1, ids[i]);
}
ResultSet rs = pre.executeQuery();

4.3 预编译(mabatis)

<!-- like 查询场景 -->
<select id="select" parameterType="java.lang.String" resultMap="BaseResultMap">
    SELECT *
    FROM user
    WHERE name like CONCAT("%", #{name}, "%")
</select>

<!-- where in 查询场景 -->
<select id="select" parameterType="java.util.List" resultMap="BaseResultMap">
    SELECT *
    FROM user
    WHERE name IN
    <foreach collection="names" item="name" open="(" close=")" separator=",">
      #{name}
    </foreach>
</select>
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
要创建一个Spring Boot MyBatisSQL Server代码下载,你可以按照以下步骤进行操作: 1. 首先,确保你已经安装了Java开发工具和Maven构建工具。 2. 创建一个新的Spring Boot项目。你可以通过使用Spring Initializr来快速搭建一个基本的Spring Boot项目结构。在Spring Initializr的网站上选择你需要的项目配置,包括项目的名称、包名、Java版本等。点击生成项目按钮,然后下载生成的zip包。 3. 解压下载的zip包,在你选择的目录下打开命令行或终端窗口。 4. 进入解压后的项目目录,运行以下Maven命令来导入项目依赖: ``` mvn clean install ``` 5. 接下来,你需要在项目的pom.xml文件中添加MyBatisSQL Server的依赖。例如,你可以添加以下依赖: ``` <dependencies> ... <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.0</version> </dependency> <dependency> <groupId>com.microsoft.sqlserver</groupId> <artifactId>mssql-jdbc</artifactId> <version>8.4.1.jre11</version> </dependency> ... </dependencies> ``` 6. 在项目的配置文件(例如application.properties或application.yml)中添加SQL Server的连接信息,包括数据库URL、用户名和密码。例如,你可以添加以下配置: ``` spring.datasource.url=jdbc:sqlserver://localhost:1433;databaseName=mydatabase spring.datasource.username=yourusername spring.datasource.password=yourpassword ``` 7. 创建一个MyBatis的Mapper接口和对应的映射文件,用于定义和执行SQL查询语句。你可以在Mapper接口中声明SQL查询方法,并且在映射文件中编写对应的SQL语句。 8. 在你的应用程序中使用自动注入注解(例如@Autowired)将Mapper接口注入到你的服务类或控制器类中。然后,你就可以在这些类中使用Mapper接口定义的方法来执行SQL查询了。 9. 最后,你可以启动你的Spring Boot应用程序,通过访问定义的接口来执行SQL查询操作。你可以使用Postman等工具来测试接口的响应结果。 希望以上步骤能够帮助你成功创建Spring Boot MyBatisSQL Server代码,并实现相关的功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值