Java代码审计-sql注入,万字长文详解

admin 2024年12月6日15:33:50评论12 views字数 19405阅读64分41秒阅读模式

漏洞简介

SQL注入漏洞是对数据库进行的一种攻击方式。 SQL注入漏洞可以说是在企业运营中会遇到的最具破坏性的漏洞之一,它也是目前被利用得比较多的漏洞。

其主要形成方式是在数据交互中,前端数据通过后台在对数据库进行操作时,由于没有做好安全防护,导致攻击者将恶意代码直接或者间接的拼接到请求参数中,导致攻击者的恶意代码被当做SQL语句的一部分进行执行,最终导致数据库被攻击。

所有可以涉及到数据库增删改查操作的系统功能点都有可能存在SQL注入漏洞。

虽然现在针对SQL注入的防护层出不穷,但大多情况下由于开发人员的疏忽或者系统功能点用于特定的使用场景,导致现在的大部分系统还是会存在SQL注入漏洞。

目前Java里面常见的数据库连接方式有三种,分别是JDBC,Mybatis,和Hibernate。其中见得比较多的是mybatis。目前Mybatis主要与spring框架搭配。

漏洞审计入门

JDBC中的SQL注入

动态拼接导致的SQL注入

在使用JDBC进行sql连接的系统中SQL语句动态拼接导致的SQL注入漏洞是比较常见的场景。 其主要原因是后端代码将前端获取的参数动态直接拼接到SQL语句中执行。 但是在JDBC的注入类型中,有两个比较关键的点,那就是

  • • 系统动态拼接参数

  • • 系统使用 java.sql.Statement 执行SQL语句 当然还有其他的场景,但是哪些场景过于少见。

Java代码审计-sql注入,万字长文详解

执行的流程为:

createStatement( ) :创建一个 Statement 对象

Java代码审计-sql注入,万字长文详解

executeQuery(String sql) 方法:执行指定的 SQL 语句,返回单个 ResultSet 对象

Java代码审计-sql注入,万字长文详解

下面是一个正常的sql查询代码

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

publicclassSelectExample{
publicstaticvoidmain(String[] args){
// JDBC连接参数
Stringurl="jdbc:mysql://localhost:3306/testdatabase";
Stringusername="root";
Stringpassword="root";

try(Connectionconnection=DriverManager.getConnection(url, username, password)){
// 创建Statement对象
Statementstatement= connection.createStatement();

// 执行SQL查询语句
Stringsql="SELECT * FROM employees";
ResultSetresultSet= statement.executeQuery(sql);

// 处理查询结果
while(resultSet.next()){
intid= resultSet.getInt("id");
Stringuser= resultSet.getString("user");
System.out.println("id: "+ id +", user: "+ user);
}

// 关闭ResultSet和Statement
            resultSet.close();
            statement.close();
}catch(SQLException e){
            e.printStackTrace();
}
}
}

在上述代码中,我们使用executeQuery() 方法执行SELECT查询语句,然后使用对象遍历查询结果。最后关闭和对象,释放资源。

看完了正常的代码,接下来看带有漏洞代码

package com.example.demo.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("/sql")
publicclassJdbcIectionController{

privatestaticStringdriver="com.mysql.jdbc.Driver";
@Value("${spring.datasource.url}")
privateString url;
@Value("${spring.datasource.username}")
privateString user;
@Value("${spring.datasource.password}")
privateString password;

@RequestMapping("/JdbcinIection")
publicStringjdbcDynamic(@RequestParam("id")String id)throwsClassNotFoundException,SQLException{

StringBuilderresult=newStringBuilder();
// 注册驱动
Class.forName(driver);
// 获取连接
Connectionconn=DriverManager.getConnection(url, user, password);
Statementstatement= conn.createStatement();
//动态拼接字符串
Stringsql="select * from users where id = '"+ id +"'";
ResultSetrs= statement.executeQuery(sql);
while(rs.next()){
StringUser= rs.getString("username");
StringPass= rs.getString("password");
Stringinfo=String.format("%s: %sn",User,Pass);
            result.append(info);
}
        rs.close();
        conn.close();
return result.toString();
}

}
Java代码审计-sql注入,万字长文详解

浏览器访问http://localhost:8888/sql/JdbcinIection?id=1

Java代码审计-sql注入,万字长文详解

上面代码中真正造成漏洞的代码是37、38行

Java代码审计-sql注入,万字长文详解

用户传入的参数被直接拼接到sql查询语句中,导致了这段代码sql注入漏洞的出现,添加单引号

Java代码审计-sql注入,万字长文详解

使用' and '1'='1进行验证

Java代码审计-sql注入,万字长文详解

使用sqlmap进行注入

Java代码审计-sql注入,万字长文详解

错误的预编译导致的sql注入

为什么先说Statement,因为PreparedStatement虽然可以注入,但是能注入的场景太少了,不像Statement那样具有普遍性。因为如果使用了PreparedStatement,哪么这条语句就会预编译参数化然后再去执行查询操作,这样是能够有效防止SQL注入的。但如果没有正确的使用 PreparedStatement 预编译还是会存在SQL注入风险的。==原理:PreparedStatement不会对拼接的字符串进行预处理。==

我们先来研究正确的预编译是什么样

package com.example.demo.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("/sql")
publicclassJdbcPrepareStatement{
privatestaticStringdriver="com.mysql.jdbc.Driver";
@Value("${spring.datasource.url}")
privateString url;
@Value("${spring.datasource.username}")
privateString user;
@Value("${spring.datasource.password}")
privateString password;

@RequestMapping("/sec")
publicStringjdbcsec(@RequestParam("username")String username)throwsSQLException,ClassNotFoundException{

StringBuilderresult=newStringBuilder();
Class.forName(driver);
Connectionconn=DriverManager.getConnection(url, user, password);
// 安全代码
Stringsql="select * from users where id = ?";
PreparedStatementpreparestatement= conn.prepareStatement(sql);
        preparestatement.setString(1, username);
ResultSetrs= preparestatement.executeQuery();

while(rs.next()){
StringresUsername= rs.getString("username");
StringresPassword= rs.getString("password");
Stringinfo=String.format("%s: %sn", resUsername, resPassword);
            result.append(info);
}
System.out.println("编译前:"+sql);
System.out.println("编译后:"+ preparestatement);
        rs.close();
        conn.close();
return result.toString();
}

}
Java代码审计-sql注入,万字长文详解

直接访问?id=1

Java代码审计-sql注入,万字长文详解

此时终端显示如下

Java代码审计-sql注入,万字长文详解

我们可以看到系统识别到然后执行的sql语句是

select * from users whereid='1'

下面是我们攻击者预想中的sql语句

select * from users whereid=1

那这和我们预想中的sql语句有什么区别?区别是系统自动给我们传入的参数加了引号,准确的来说是把我们传入的参数值作为sql变量的字段属性了,而不是像最开始的sql注入那样直接将我们传入的参数作为sql变量进行执行。比如我们打单引号测试

Java代码审计-sql注入,万字长文详解

可以看到这单引号没有像上面那样影响到系统,使得系统报错,下面是系统执行sql时的sql语句

Java代码审计-sql注入,万字长文详解

我们再执行一个sql进行测试

Java代码审计-sql注入,万字长文详解
Java代码审计-sql注入,万字长文详解

现在就很明显了,预编译将我们恶意语句限制住了,gg。但是也会出现一些问题,简单来说,可能由于开发人员疏忽或经验不足等原因,虽然使用了预编译 PreparedStatement ,但没有根据标准流程对参数进行标记,依旧使用了动态拼接SQL语句的方式,进而造成SQL注入漏洞。例子:

package com.example.demo.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("/sql")
publicclassJdbcPrepareStatement{
privatestaticStringdriver="com.mysql.jdbc.Driver";
@Value("${spring.datasource.url}")
privateString url;
@Value("${spring.datasource.username}")
privateString user;
@Value("${spring.datasource.password}")
privateString password;

@RequestMapping("/preparestaementsqli")
publicStringjdbcPrepare(@RequestParam("id")String id)throwsClassNotFoundException,SQLException{
StringBuilderresult=newStringBuilder();
Class.forName(driver);
Connectionconn=DriverManager.getConnection(url, user, password);
//没有正确使用预编译方式,SQL语句还是进行了动态拼接
Stringsql="select * from users where id = '"+ id +"'";
PreparedStatementpreparestatement= conn.prepareStatement(sql);
ResultSetrs= preparestatement.executeQuery();
while(rs.next()){
StringreUsername= rs.getString("username");
StringresPassword= rs.getString("password");
Stringinfo=String.format("%s: %sn", reUsername, resPassword);
            result.append(info);
}
System.out.println("编译前:"+sql);
System.out.println("编译后:"+ preparestatement);
        rs.close();
        conn.close();
return result.toString();
}
}
Java代码审计-sql注入,万字长文详解

正常的请求如下

Java代码审计-sql注入,万字长文详解
Java代码审计-sql注入,万字长文详解

进行sql注入测试

Java代码审计-sql注入,万字长文详解

可以看到我们加单引号就报错了,用真语句进行测试

Java代码审计-sql注入,万字长文详解
Java代码审计-sql注入,万字长文详解

可以看到现在能识别我们恶意拼接的SQL了,我们先看一下刚执行的sql语句在系统中是什么样的

Java代码审计-sql注入,万字长文详解

可以看到系统使用的是PreparedStatement来执行SQL语句,但是还是产生了SQL注入。其根本原因就是PreparedStatement不会对系统动态拼接的字符串进行预处理,可以说如果使用了PreparedStatement还是出现了sql注入,哪么基本上都是开发人员没有根据标准流程对参数进行标记导致的。

order by注入

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

ORDER BY 语句的基本作用

ORDER BY 是 SQL 查询中的一个关键字,用于对查询结果按照指定的列(字段)进行排序。排序可以是升序(ASC,默认)或者降序(DESC

示例SQL语句

SELECT * FROM users ORDER BY id DESC;

上面的 SQL 会查询 users 表,并按 id 字段降序排序。

Java代码审计-sql注入,万字长文详解

在 SQL 查询中,ORDER BY 后面通常需要指定一个字段名或者字段的位置(例如 ORDER BY 1),这部分输入通常是由程序提供的。如果使用 PreparedStatement 来处理 ORDER BY 的字段名,存在一些潜在问题:PreparedStatement 主要是处理查询参数(数据),它会将输入的参数当作数据处理。因此,输入的参数会被转化为字符串(即使传入的是一个数字或其他数据类型)。这对于字段名的处理来说是个问题,因为字段名是 SQL 查询的一部分,不能当作普通数据来处理。

如果我们使用 PreparedStatement 来动态地构造 ORDER BY 语句,并试图通过占位符来传递字段名(如 ORDER BY ?),**PreparedStatement** 会将字段名当作字符串来处理,而不是 SQL 查询的一部分。这意味着,字段名不会被识别为实际的数据库字段,SQL 查询可能就无法正常工作。那我们为什么不能用 PreparedStatement 来处理 ORDER BY 字段名 ? SQL 的预编译语句(即 PreparedStatement)主要是为了防止 SQL 注入攻击而设计的,它会将用户输入的任何数据(如字段值、条件等)当作数据处理,不会将其作为 SQL 语句的结构部分。而 ORDER BY 语句中的字段名是 SQL 查询的一部分,应该直接拼接到 SQL 中。将字段名作为参数传递给 PreparedStatement,就失去了 SQL 结构的预期,因此不能通过 PreparedStatement 处理,如果使用PreparedStatement 来处理可能会产生无法预期的错误。

package com.example.demo.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("/sqli")
publicclassJdbcOrderBy{
privatestaticStringdriver="com.mysql.jdbc.Driver";
@Value("${spring.datasource.url}")
privateString url;
@Value("${spring.datasource.username}")
privateString user;
@Value("${spring.datasource.password}")
privateString password;

@RequestMapping("/orderby1")
publicStringjdbcOrderby1(@RequestParam("username")String username)throwsClassNotFoundException,SQLException{
StringBuilderresult=newStringBuilder();
Class.forName(driver);
Connectionconn=DriverManager.getConnection(url, user, password);
Stringsql="SELECT * FROM users ORDER BY ?";
PreparedStatementpreparestatement= conn.prepareStatement(sql);
        preparestatement.setString(1, username);// 动态传入排序字段名
ResultSetrs= preparestatement.executeQuery();
while(rs.next()){
StringreUsername= rs.getString("username");
StringresPassword= rs.getString("password");
Stringinfo=String.format("%s: %sn", reUsername, resPassword);
            result.append(info);
}
System.out.println("编译前的SQL语句"+ sql);
System.out.println("编译后的SQL语句"+ preparestatement);
        rs.close();
        conn.close();
return result.toString();
}
}
Java代码审计-sql注入,万字长文详解

在这个例子中,? 被替换为 "username"PreparedStatement 会将其当作字符串处理,而不是作为 SQL 查询中的字段名。这会导致查询的语法不正确,因为 SQL 查询会变成 ORDER BY 'username',而不是 ORDER BY username,这在实际的生产环境中就可能导致查询出错或者表现出意外的行为。

Java代码审计-sql注入,万字长文详解

上面说的差不多了,现在我们来看一下在实际生产中order by语句是怎么出现漏洞的

package com.example.demo.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("/sqli")
publicclassJdbcOrderBy{
privatestaticStringdriver="com.mysql.jdbc.Driver";
@Value("${spring.datasource.url}")
privateString url;
@Value("${spring.datasource.username}")
privateString user;
@Value("${spring.datasource.password}")
privateString password;

@RequestMapping("/orderby")
publicStringjdbcOrderby(@RequestParam("id")String id)throwsClassNotFoundException,SQLException{
StringBuilderresult=newStringBuilder();
Class.forName(driver);
Connectionconn=DriverManager.getConnection(url, user, password);

Stringsql="select * from users"+" order by "+ id;
PreparedStatementpreparestatement= conn.prepareStatement(sql);
ResultSetrs= preparestatement.executeQuery();
while(rs.next()){
StringreUsername= rs.getString("username");
StringresPassword= rs.getString("password");
Stringinfo=String.format("%s: %sn", reUsername, resPassword);
            result.append(info);
}
System.out.println("编译前的SQL语句"+ sql);
System.out.println("编译后的SQL语句"+ preparestatement);
        rs.close();
        conn.close();
return result.toString();
}

}
Java代码审计-sql注入,万字长文详解

可以看到,上面的代码使用了预编译,但是由于存在order by,所以使用了拼接的方式并且没有对拼接的内容做校验,导致了上面的代码存在SQL注入漏洞

Java代码审计-sql注入,万字长文详解
Java代码审计-sql注入,万字长文详解
Java代码审计-sql注入,万字长文详解
Java代码审计-sql注入,万字长文详解

使用延时语句进行测试

id=1 AND (SELECT 2708 FROM (SELECT(SLEEP(5)))KZsa)
id=1%20AND%20(SELECT%202708%20FROM%20(SELECT(SLEEP(5)))KZsa)
Java代码审计-sql注入,万字长文详解

Mybatis中的SQL注入

我们先来认识下Mybatis到底是什么。 MyBatis 是一个流行的 Java 持久层框架,用于简化与数据库的交互。它通过提供一种对象关系映射(ORM)的方式,使得 Java 程序员可以更方便地执行数据库操作(如查询、插入、更新、删除)。与传统的 JDBC 相比,MyBatis 提供了更高层次的抽象,并且比 JPA(Hibernate 等)更灵活、易于配置。

MyBatis 主要特点

  1. 1. SQL 映射:MyBatis 允许开发者使用原生的 SQL 语句来操作数据库。它将 SQL 查询与 Java 方法进行映射,使得开发者可以完全控制 SQL 执行过程,同时享受框架带来的简化和支持。

  2. 2. 动态 SQL:MyBatis 支持动态生成 SQL 查询,提供了 <if><choose><foreach> 等标签,可以根据不同的条件动态生成 SQL,避免了冗长且复杂的条件判断逻辑。

  3. 3. 简洁的 XML 配置:MyBatis 使用 XML 文件来配置数据库映射,开发者通过定义 SQL 语句和映射规则,控制 Java 对象与数据库表之间的映射关系。

  4. 4. 接口和注解支持:MyBatis 提供了基于接口的 API,开发者可以在 Java 接口中定义数据库操作方法,并通过注解或者 XML 配置映射 SQL 语句。

  5. 5. 强大的缓存机制:MyBatis 提供了一级缓存和二级缓存。一级缓存是基于 SqlSession 级别的缓存,二级缓存是跨 SqlSession 共享的缓存,默认情况下启用。

  6. 6. 支持事务管理:MyBatis 支持与 Java 的事务管理整合,确保数据库操作的原子性和一致性。

MyBatis 的工作原理

  1. 1. SqlSessionFactory:这是 MyBatis 的核心工厂类,负责创建 SqlSession 对象。SqlSession 是 MyBatis 中用于执行 SQL 语句的主要接口,它通过配置文件读取数据库连接信息,创建并维护数据库连接。

  2. 2. Mapper XML 文件:在 MyBatis 中,SQL 语句通常在 XML 配置文件中定义,开发者需要为每个数据库操作编写对应的 Mapper 文件。这个文件会包含 SQL 语句及其映射规则。

  3. 3. Mapper 接口:开发者通常会创建接口,定义一些方法,这些方法通过 XML 或注解来与具体的 SQL 语句进行绑定。

  4. 4. 映射过程:当应用程序调用 SqlSession 的方法时,MyBatis 会根据 Mapper 接口或注解提供的 SQL 语句信息执行相应的数据库操作,并将结果自动映射为 Java 对象,返回给开发者。

MyBatis 的使用步骤

  1. 1. 配置 MyBatis:通过 mybatis-config.xml 文件进行配置,配置数据库连接、缓存、日志等。

  2. 2. 定义 Mapper:创建 Mapper 接口或类,定义 SQL 操作的方法。

  3. 3. 编写 Mapper XML:为每个 SQL 操作编写对应的 XML 文件,定义 SQL 语句与 Mapper 方法的映射关系。

  4. 4. 创建 SqlSessionFactory:通过配置文件创建 SqlSessionFactory,获取 SqlSession

  5. 5. 执行 SQL 操作:通过 SqlSession 执行 SQL 语句,获取查询结果或执行更新操作。

MyBatis 的代码示例

以下是一个简单的 MyBatis 使用示例:

MyBatis 配置文件 mybatis-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<mappers>
<mapper resource="com/example/mapper/UserMapper.xml"/>
</mappers>
</configuration>

Mapper 接口 UserMapper.java

publicinterfaceUserMapper{
UsergetUserById(int id);
List<User>getAllUsers();
}

Mapper XML 文件 UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<mapper namespace="com.example.mapper.UserMapper">
<select id="getUserById" resultType="com.example.model.User">
        SELECT id, name, email FROM users WHEREid=#{id}
</select>

<select id="getAllUsers" resultType="com.example.model.User">
        SELECT id, name, email FROM users
</select>
</mapper>

Java 主程序 Main.java

import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

publicclassMain{
publicstaticvoidmain(String[] args){
SqlSessionFactorysqlSessionFactory=newSqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
try(SqlSessionsession= sqlSessionFactory.openSession()){
UserMapperuserMapper= session.getMapper(UserMapper.class);
Useruser= userMapper.getUserById(1);
System.out.println(user);
}
}
}

Mybatis中#{}和${}区别

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

#{} 占位符用于预编译参数绑定,MyBatis 会把 #{} 中的值作为参数传递给数据库查询或更新语句,在执行 SQL 时由数据库驱动进行处理,这是一种安全的方式,因为 MyBatis 会自动处理 SQL 注入问题。

  • • 工作原理:当使用 #{} 时,MyBatis 会将参数值传入 SQL 语句中,并在执行时由数据库来处理这些参数。这样做的好处是,参数的值被视为数据,不会直接拼接到 SQL 语句中,防止了 SQL 注入攻击。

  • • 使用场景#{} 适用于所有情况,尤其是对于动态条件、查询参数等。比如查询条件、更新值等。

示例:

查询的xml

<select id="getUserById" resultType="com.example.User">
    SELECT * FROM users WHEREid=#{id}
</select>

对应的Java代码

Useruser= userMapper.getUserById(1);

哪么最后在执行时,MyBatis 会将 id 的值 1 绑定到 SQL 查询中,生成的 SQL 语句会是:

SELECT * FROM users WHEREid=?

比如:SELECT * FROM users WHERE id = #{id} ,如果传入数值为1,哪么最终会被解析成SELECT * FROM users WHERE id ="1" 

${} 用于直接拼接字符串。MyBatis 在解析 SQL 时,${} 不会对传入的参数不做处理,而是直接拼接,MyBatis会把 ${} 中的内容直接替换为对应的参数值,而不进行任何转义或预处理,进而会造成SQL注入漏洞。

  • • 工作原理:当使用 ${} 时,MyBatis 会把传入的参数值直接拼接到 SQL 语句中。由于没有做任何转义处理,这可能导致 SQL 注入等安全问题,尤其是在使用用户输入的参数时。

  • • 使用场景${} 一般用于动态表名、列名、排序字段等场景,通常情况下不应将用户输入的参数直接传入 ${} 中,以防止 SQL 注入。

下面是两个关于${}的例子:

不存在sql漏洞的${}写法

Mapper XML文件

<select id="findByColumn" resultType="com.example.User">
    SELECT * FROM users WHERE ${column}=#{value}
</select>

对应的Java代码

Stringcolumn="age";// 或者是动态获取的字段名
intvalue=25;
List<User> users = userMapper.findByColumn(column, value);

最后在执行时,MyBatis 会将 ${column} 替换成 age,而 #{value} 仍然会用 25 作为参数绑定。最终生成的 SQL 语句为:

SELECT * FROM users WHEREage=?

这里的 age 是动态替换的列名,而 ? 仍然是参数占位符,用于防止 SQL 注入

存在sql漏洞的${}写法

假设我们允许用户通过输入表名查询数据,并且直接用 ${} 来拼接 SQL:

Mapper XML文件

<select id="findByTable" resultType="com.example.User">
    SELECT * FROM ${tableName}WHEREid=#{id}
</select>

如果用户输入 "users; DROP TABLE users;",最终生成的 SQL 会是:

SELECT * FROM users; DROP TABLE users;WHEREid=?

这就是典型的 SQL 注入攻击,攻击者可以执行恶意操作(如删除表) 。再说一个比较简单的例子:

select * from user whereid= ${num}

如果传入数值为1,最终会被解析成

select * from user whereid=1

所以我们可以做一个总结:#{} 可以有效防止SQL注入漏洞。 ${} 则无法直接有效的防止SQL注入漏洞。

因此在我们对JavaWeb系统进行代码审计的时候,如果发现系统中有Mybatis,哪么我们应着重审计SQL语句拼接的地方。 看看是否存在开发人员粗心对拼接语句使用了 ${} 方式造成的SQL注入漏洞。 同时在Mybatis中有几种场景是不能使用预编译方式的,比如: order by 、 in , like 。

ORDER BY 子句注入

ORDER BY语句 :用于对查询结果的排序,asc为升序,desc为降序。默认为升序。

比如:

select * from users order by username asc;

与JDBC预编译中order by注入一样,在 order by 语句后面需要是字段名或者字段位置。因此也不能使

用Mybatis中预编译的方式。

在我们的示例代码工程中UserMapper.xml 的第13行是order by查询,我们可以看到使用了拼接符${},从而造成了SQL注入漏洞,如下图所示:

Java代码审计-sql注入,万字长文详解
Java代码审计-sql注入,万字长文详解
Java代码审计-sql注入,万字长文详解
Java代码审计-sql注入,万字长文详解

最后得到完整的注入payload

/sqli/mybatis/orderby?sort=id%20AND%20(SELECT%202708%20FROM%20(SELECT(SLEEP(5)))KZsa)
Java代码审计-sql注入,万字长文详解

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>
<select id="findUsers" resultType="User">
    SELECT * FROM users WHERE id IN 
<foreach item="id" collection="ids" open="(" separator="," close=")">
#{id}
</foreach>
</select>

错误的代码如下

<select id="findUsers" resultType="User">
    SELECT * FROM users WHERE id IN(${ids})
</select>

像上面这种就是存在sql注入漏洞的代码。

例子:

UserMapper.java

package com.example.demo.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
publicinterfaceUserMapper{

List<User>INuser(@Param("id")String id);

}
Java代码审计-sql注入,万字长文详解

UserMapper.xml

<?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.demo.mybatisinjection.UserMapper">

<resultMap type="com.example.demo.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="INuser" parameterType="String" resultMap="User">
        SELECT * FROM users WHERE id IN(${id})
</select>
</mapper>
Java代码审计-sql注入,万字长文详解

MybitsController.java

package com.example.demo.mybatisinjection;

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


@RestController
@RequestMapping("/sqli")
publicclassMybitsController{

@Autowired
privateUserMapper userMapper;


@GetMapping("/mybatis/injection")
publicList<User>INuser(@RequestParam("id")String id){
return userMapper.INuser(id);
}
}
Java代码审计-sql注入,万字长文详解

进行SQL注入测试

/sqli/mybatis/injection?id=1%20AND%20(SELECT%202708%20FROM%20(SELECT(SLEEP(5)))KZsa)
Java代码审计-sql注入,万字长文详解

like注入

LIKE 子句用于在 SQL 查询中进行模糊匹配。当使用用户输入的内容作为 LIKE 条件时,应该非常小心,因为恶意输入可能导致 SQL 注入。例如:

LIKE语句 :在一个字符型字段列中检索包含对应子串的。

比如:

select * from users where username like 'admin';
Java代码审计-sql注入,万字长文详解

使用like语句进行查询时如果使用占位符 #{} 查询时程序会报错。

比如:

<select id="liked" parameterType="String" resultMap="User">
        select * from users where username like '%#{username}%'
</select>
Java代码审计-sql注入,万字长文详解
Java代码审计-sql注入,万字长文详解
Java代码审计-sql注入,万字长文详解

此时进行模糊匹配会报错

Java代码审计-sql注入,万字长文详解
Java代码审计-sql注入,万字长文详解

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

比如:

<select id="like2" parameterType="String" resultMap="User">
        select * from users where username like '%${username}%'
</select>

下面代码是正确的做法,可以防止SQL注入漏洞

<select id="getUserListLikeConcat" resultType="org.example.User">
    SELECT * FROM user WHERE name LIKE concat('%',#{name},'%')
</select>

或者

<select id="getUserListLike" resultType="org.example.User">
<bind name="pattern" value="'%' + name + '%'"/>
    SELECT * FROM user 
    WHERE name LIKE #{pattern}
</select>

我们可以对代码进行测试

Java代码审计-sql注入,万字长文详解
Java代码审计-sql注入,万字长文详解
Java代码审计-sql注入,万字长文详解

进行sql查询测试

Java代码审计-sql注入,万字长文详解
/sqli/mybatis/like?username=admin%27%20UNION%20ALL%20SELECT%20NULL,NULL,CONCAT(0x717a717671,0x467946675741795a445644424550596c454a484a6d58634c664d524e47654d774177756b734d6776,0x7178707a71)--%20-
Java代码审计-sql注入,万字长文详解
/sqli/mybatis/like?username=admin%27%20AND%20(SELECT%207172%20FROM%20(SELECT(SLEEP(5)))wUIo)%20AND%20%27HUpo%27=%27HUpo
Java代码审计-sql注入,万字长文详解

如何在项目中快速查找sql注入

拿到一个项目后,我们应该先知道它是什么框架,这样才能快速的定位路由,只要找到合适的路由我们才能去谈漏洞,不然项目里面很多基础工具类看着有漏洞,实际上你根本利用不了。比如我们拿到应该springboot+mybatis的项目,哪么这个时候我们如果想快速查找sql注入的话就直接看Mapper的XML文件,然后在里面搜索${ } ,当然还有其他手法,但是这种方式是最快的。下面是可以查找的关键字或者函数

Statement
createStatement
PrepareStatement
like '%${
in(${
in (${
select
update
insert
delete
${
setObject(
setInt(
setString(
setSQLXML(
createQuery(
createSQLQuery(
createNativeQuery(

比如下面的项目

Java代码审计-sql注入,万字长文详解

后面就是找对应的调用了

原文始发于微信公众号(实战安全研究):Java代码审计-sql注入,万字长文详解

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年12月6日15:33:50
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Java代码审计-sql注入,万字长文详解http://cn-sec.com/archives/3474786.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息