java之sql注入代码审计
前言
其实找到漏洞原因很简单,主要想学习一下JDBCsql的过程
JDBC
简单介绍
Java通过java.sql.DriverManager来管理所有数据库的驱动注册,所以如果想要建立数据库连接需要先在java.sql.DriverManager中注册对应的驱动类,然后调用getConnection方法才能连接上数据库。
JDBC定义了一个叫java.sql.Driver的接口类负责实现对数据库的连接,所有的数据库驱动包都必须实现这个接口才能够完成数据库的连接操作。java.sql.DriverManager.getConnection(xx)其实就是间接的调用了java.sql.Driver类的connect方法实现数据库连接的。数据库连接成功后会返回一个叫做java.sql.Connection的数据库连接对象,一切对数据库的查询操作都将依赖于这个Connection对象。
后面主要涉及的3个对象
-
connection
connection对象代表数据库
可以设置数据库自动提交。事务提交(connection.commit()),事务回滚(connection.rollback())。
-
statement
调用connnection.createStatement()方法会返回一个statement对象。
是具体执行sql语句
-
PreparedStatement
与statement对象的区别是,不直接放入sql语句,先用?作为占位符进行预编译,等预编译完成后,对?进行赋值,之后调用execute等方法不需要添加参数即可完成执行SQL语句。
-
ResultSet,结果集或一张虚拟表
那么在使用JDBC的时候在使用statement直接拼接SQL语句而不是PreparedStatement预编译方式执行SQL语句的话可能就会造成SQL注入。
环境搭建
我使用的是Httpservert
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.*;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/query")
public class QueryServlet extends HttpServlet {
private static final String JDBC_URL = "jdbc:mysql://localhost:3306/security";
private static final String JDBC_USER = "root";
private static final String JDBC_PASS = "123456";
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String id = request.getParameter("id");
response.setContentType("text/html");
PrintWriter out = response.getWriter();
if (id == null || id.isEmpty()) {
out.println("ID parameter is missing");
return;
}
Connection connection = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
connection = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASS);
String sql = "select * from users where id =?";
//String sql = "select * from users where id = '"+id+"'";
stmt = connection.prepareStatement(sql);
stmt.setString(1, id);
rs=stmt.executeQuery();
// Statement statement = connection.createStatement();
// rs = statement.executeQuery(sql);
if (rs.next()) {
int userId = rs.getInt("id");
String username = rs.getString("username");
out.println("<h1>User Details</h1>");
out.println("<p>ID: " + userId + "</p>");
out.println("<p>Username: " + username + "</p>");
} else {
out.println("No user found with ID: " + id);
}
} catch (ClassNotFoundException e) {
out.println("MySQL JDBC Driver not found");
e.printStackTrace(out);
} catch (SQLException e) {
out.println("Connection to MySQL database failed");
e.printStackTrace(out);
} finally {
try {
if (rs != null) rs.close();
if (stmt != null) stmt.close();
if (connection != null) connection.close();
} catch (SQLException e) {
e.printStackTrace(out);
}
}
}
}
然后还需要在WEB—INF目录下的web.xml下写入
<servlet>
<servlet-name>QueryServlet</servlet-name>
<servlet-class>QueryServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>QueryServlet</servlet-name>
<url-pattern>/query</url-pattern>
</servlet-mapping>
Statement分析
测试
如果我们使用这个来执行我们的sql注入,那么久会有漏洞的存在,我们看看这个过程
因为它是直接把我们的语句拼接后去处理执行
我们试一试
可以发现成功爆出了我们的数据库
分析
我们来分析一下
在这下断点
Statement statement = connection.createStatement();
就是简单的创建了一个StatementImpl对象,然后设置一些东西,里面有我们的传递给构造函数的参数包括连接代理和数据库信息
public java.sql.Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
StatementImpl stmt = new StatementImpl(getMultiHostSafeProxy(), this.database);
stmt.setResultSetType(resultSetType);
stmt.setResultSetConcurrency(resultSetConcurrency);
return stmt;
}
重点还是在
执行sql语句的地方
rs = statement.executeQuery(sql);
进入
先是初始化一些,我们不关心,只看我们sql跑到哪里去了
if (this.doEscapeProcessing) {
Object escapedSqlResult = EscapeProcessor.escapeSQL(sql, this.session.getServerSession().getSessionTimeZone(),
this.session.getServerSession().getCapabilities().serverSupportsFracSecs(), this.session.getServerSession().isServerTruncatesFracSecs(),
getExceptionInterceptor());
sql = escapedSqlResult instanceof String ? (String) escapedSqlResult : ((EscapeProcessorResult) escapedSqlResult).escapedSql;
}
来到这里,对我们的sql语句进行处理
核心逻辑在escapeSQL
以确保 SQL 语句能够被数据库正确解析和执行。这个方法通常用于将 JDBC 标准转义语法转换为特定数据库的本地 SQL 语法。
没有特殊的,然后返回结果,和我们原来的一模一样
然后检查我们的结果集,是否为空,为空抛出异常
if (!isResultSetProducingQuery(sql)) {
throw SQLError.createSQLException(Messages.getString("Statement.57"), MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
}
设置超时任务
CancelQueryTask timeoutTask = null;
String oldDb = null;
try {
timeoutTask = startQueryTimer(this, getTimeoutInMillis());
if (!locallyScopedConn.getDatabase().equals(getCurrentDatabase())) {
oldDb = locallyScopedConn.getDatabase();
locallyScopedConn.setDatabase(getCurrentDatabase());
}
到重要的地方
调用execSQL执行sql语句
this.results = ((NativeSession) locallyScopedConn.getSession()).execSQL(this, sql, this.maxRows, null, createStreamingResultSet(),
getResultSetFactory(), cachedMetaData, false);
我们跟进,重点查看我们sql语句的部分,但是变成了query
首先各个参数的意思
-
query
:要执行的 SQL 查询字符串。 -
packet
:包含查询数据的NativePacketPayload
对象,可能用于发送二进制数据包。
重点这两个参数
然后到
try {
return packet == null
? ((NativeProtocol) this.protocol).sendQueryString(callingQuery, query, this.characterEncoding.getValue(), maxRows, streamResults,
cachedMetadata, resultSetFactory)
: ((NativeProtocol) this.protocol).sendQueryPacket(callingQuery, packet, maxRows, streamResults, cachedMetadata, resultSetFactory);
这里进行的查询
-
发送查询
:
- 如果
packet
为null
,调用sendQueryString
方法发送查询字符串。 - 如果
packet
不为null
,调用sendQueryPacket
方法发送查询数据包。
- 如果
跟进我们的packet也就是数据,为null,进行的是sendQueryString查询
进入sendQueryString方法
重点来到
if (!this.session.getServerSession().getCharsetSettings().doesPlatformDbCharsetMatches() && StringUtils.startsWithIgnoreCaseAndWs(query, "LOAD DATA")) {
sendPacket.writeBytes(StringLengthDataType.STRING_FIXED, StringUtils.getBytes(query));
} else {
sendPacket.writeBytes(StringLengthDataType.STRING_FIXED, StringUtils.getBytes(query, characterEncoding));
}
目的是将 SQL 查询字符串写入到 sendPacket
对象中 ,因为我们刚刚的数据包是空的,这里久把我们的字符串转成这种形式写进去
调用 sendPacket.writeBytes
方法,将转换后的字节数组以 STRING_FIXED
类型写入数据包。
看到我们的数据包
最后
return sendQueryPacket(callingQuery, sendPacket, maxRows, streamResults, cachedMetadata, resultSetFactory);
调用sendQueryPacket发送,然后返回结果
总结
创建 Statement 对象--执行 SQL 查询--execSQL--发送查询字符串--处理查询字符串的字符集和写入数据包--发送数据包并返回结果
PreparedStatement分析
复现
可以看到我们的输入被当做字符串处理了,根本没有起到闭合作用,因为已经编译过sql语句,后面只会当作字符串处理
分析
与Statement的区别在于PrepareStatement会对SQL语句进行预编译,预编译的好处不仅在于在一定程度上防止了sql注入,还减少了sql语句的编译次数,提高了性能,其原理是先去编译sql语句,无论最后输入为何,预编译的语句只是作为字符串来执行,而SQL注入只对编译过程有破坏作用,执行阶段只是把输入串作为数据处理,不需要再对SQL语句进行解析,因此解决了注入问题。这样说你可能不理解,但是你看代码部分就清楚了
我们看下区别
String sql = "select * from users where id =?";
String sql = "select * from users where id = '"+id+"'";
首先是sql语句,我们前面是用?去代替我们的拼接
然后是对sql处理的部分
stmt = connection.prepareStatement(sql);
stmt.setString(1, id);
rs=stmt.executeQuery();
Statement statement = connection.createStatement();
rs = statement.executeQuery(sql);
我们的sql语句是在编译之后才去拼接的,而我们的前面是已经拼接后去编译,这里我们的sql语句是已经编译了,之后再拼接的只能当作字符串去处理
分析过程
prepareStatement方法下断点
重点关注我们的sql传入的地方
这个地方会nativeSQL(sql)处理我们跟进看看
String nativeSql = this.processEscapeCodesForPrepStmts.getValue() ? nativeSQL(sql) : sql;
可以看到是和我们上面那个方法一样的,去预编译我们的sql语句,核心在 EscapeProcessor.escapeSQL方法
然后处理后返回给nativesql赋值
在prepareStatement方法最后把我们的nativesql传给了
pStmt = (ClientPreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);
}
我们看到clientPrepareStatement方法,它和我们Statement.createStatement();方法几乎是一样的,只不过创建的是一个PrepareStatement对象
<init>:206, StatementImpl (com.mysql.cj.jdbc)
<init>:172, ClientPreparedStatement (com.mysql.cj.jdbc)
<init>:211, ClientPreparedStatement (com.mysql.cj.jdbc)
<init>:192, ClientPreparedStatement (com.mysql.cj.jdbc)
getInstance:133, ClientPreparedStatement (com.mysql.cj.jdbc)
clientPrepareStatement:670,
可以看到最后还是调用父类StatementImpl 的构造方法
-
- DBC 驱动创建一个
PreparedStatement
对象,并将其与预编译的 SQL 语句相关联。 - 该对象允许设置参数值,并在执行 SQL 语句时替换这些参数。
- DBC 驱动创建一个
-
绑定参数:
PreparedStatement
对象提供了一系列set
方法,用于将参数值绑定到 SQL 语句中的占位符(?
)。- 这些方法包括
setString
、setInt
、setDate
等。
来到stmt.setString(1, id);方法,setString
方法是用于在 PreparedStatement
对象中设置 SQL 查询参数的位置。具体来说,它将指定的字符串值绑定到预编译 SQL 语句中的占位符(?
)位置。该方法的主要作用是处理参数值的绑定
可以看到是先调用getQueryBindings方法获取bindings获取参数绑定信息,然后
((PreparedQuery) this.query).getQueryBindings().setString(getCoreParameterIndex(parameterIndex), x);
然后跟进setString
获取绑定对象并设置值,调用 getBinding
方法,获取指定参数位置的绑定对象。setBinding
方法,将参数值 x
绑定到指定的位置,并指定参数的类型为 MysqlType.VARCHAR
。
public void setString(int parameterIndex, String x) {
if (x == null) {
setNull(parameterIndex);
return;
}
getBinding(parameterIndex, false).setBinding(x, MysqlType.VARCHAR, this.numberOfExecutions, this.sendTypesToServer);
}
最后来到stmt.executeQuery();执行我们的sql语句
来到
Message sendPacket = ((PreparedQuery) this.query).fillSendPacket(((PreparedQuery) this.query).getQueryBindings());
发生数据是在这里的,就是先获取我们替换参数的位置然后替换发送数据
最后的数据是在executeInternal处理的
this.results = executeInternal(this.maxRows, sendPacket, createStreamingResultSet(), true, cachedMetadata, false);
内部数据传到了execSQL方法
和我们上面一样的
rs = ((NativeSession) locallyScopedConnection.getSession()).execSQL(this, null, maxRowsToRetrieve, (NativePacketPayload) sendPacket,
createStreamingResultSet, getResultSetFactory(), metadata, isBatch);
后面一模一样,不分析了
总结
其实和上面的不同就是绑定参数setString
方法的替换
和寻找占位符的流程,在执行sql语句时进行一个替换,放在我们的packet数据包中
JDBC漏洞点
使用in语句
正常使用是这样的
String delIds = "1, 2, 3"; // 用户输入,可能包含恶意代码
String sql = "DELETE FROM users WHERE id IN (" + delIds + ");"; // 存在 SQL 注入风险
如果我们修改我们的输入内容
如 1; DROP TABLE users; --
,则拼接后的 SQL 语句会变为
DELETE FROM users WHERE id IN (1; DROP TABLE users; --);
为了修复的话我们的方法还是使用展位符,预处理,但是因为可以输入多个的情况,我们还需要循环处理
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;
public class UserDeletion {
public static void deleteUserByIds(Connection connection, List<Integer> delIds) throws SQLException {
if (delIds == null || delIds.isEmpty()) {
throw new IllegalArgumentException("delIds cannot be null or empty");
}
// 构建占位符
StringBuilder placeholders = new StringBuilder();
for (int i = 0; i < delIds.size(); i++) {
if (i > 0) {
placeholders.append(",");
}
placeholders.append("?");
}
// 构建 SQL 语句
String sql = "DELETE FROM users WHERE id IN (" + placeholders.toString() + ")";
try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
// 绑定参数
for (int i = 0; i < delIds.size(); i++) {
pstmt.setInt(i + 1, delIds.get(i));
}
// 执行删除操作
int affectedRows = pstmt.executeUpdate();
System.out.println("Deleted " + affectedRows + " rows.");
}
}
}
就是根据我们传入的个数生成相应的占位符,然后再为占位符绑定参数
最后执行删除操作
LIKE语句
SELECT * FROM Websites
WHERE name LIKE 'G%';
执行输出结果:
“%” 符号用于在模式的前后定义通配符(默认字母)
漏洞例子
String con = "admin%' or 1=1#";
String sql = "SELECT * FROM users WHERE password LIKE '%" + con + "%'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql);
在这个例子中,用户输入 con
被直接拼接到 SQL 查询字符串中,SQL 查询变为:
SELECT * FROM users WHERE password LIKE '%admin%' or 1=1#%'
这样攻击者可以利用输入 con
使得 WHERE
条件总是为真,从而绕过验证,导致 SQL 注入。
正常情况下得到的数据只有admin
拼接之后得到了全部的数据
解决方法还是使用我们的占位符
String con = "admin%' OR '1'='1";
String sql = "SELECT * FROM users WHERE password LIKE ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, "%" + con + "%");
ResultSet rs = pstmt.executeQuery();
%和_
没有手动过滤%。预编译是不能处理这个符号的, 所以需要手动过滤,否则会造成慢查询,造成 dos。
在 SQL 查询中,%
和 _
是通配符,用于 LIKE
操作符的模式匹配:
%
匹配零个或多个字符。_
匹配单个字符。
当你使用 LIKE
语句进行查询时,用户输入的这些通配符可能会导致意外的查询行为。特别是,如果用户输入的字符串包含大量的 %
或 _
,可能会导致非常宽泛的匹配,从而导致慢查询,甚至可能被恶意利用来进行拒绝服务(DoS)攻击。
如果用户输入的字符串包含 %
或 _
,这些通配符可能会导致查询匹配更多的数据。举个例子:
java
Copy
String userInput = "%";
String sql = "SELECT * FROM users WHERE username LIKE ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, "%" + userInput + "%");
ResultSet rs = pstmt.executeQuery();
在这个例子中,实际执行的查询是
SELECT * FROM users WHERE username LIKE '%%%'
这个查询将匹配所有的用户名,而不是用户预期的精确匹配。
原因二:性能问题
当查询字符串包含大量的 %
或 _
时,数据库需要进行更多的模式匹配操作,这可能导致查询性能的显著下降。如果恶意用户故意输入大量的 %
,可能会让数据库执行非常耗时的查询,导致数据库服务器资源耗尽,从而形成拒绝服务(DoS)攻击。
解决方法:手动过滤 %
和 _
在将用户输入绑定到 LIKE
查询前,应该手动转义或过滤 %
和 _
字符,确保它们不会被误解释为通配符。
示例代码
以下是一个处理用户输入,安全地进行 LIKE
查询的示例:
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class UserQuery {
public static ResultSet getUsersByUsernamePattern(Connection connection, String pattern) throws SQLException {
// 手动过滤 % 和 _ 字符
pattern = pattern.replace("%", "\\%").replace("_", "\\_");
String sql = "SELECT * FROM users WHERE username LIKE ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
// 绑定参数,使用 "%" 包裹用户输入的模式
pstmt.setString(1, "%" + pattern + "%");
// 执行查询
return pstmt.executeQuery();
}
}
Order by、from等无法预编译
通过上面对使用in关键字和like关键字发现,只需要对要传参的位置使用占位符进行预编译时似乎就可以完全防止SQL注入,然而事实并非如此,当使用order by语句时是无法使用预编译的,原因是order by子句后面需要加字段名或者字段位置,而字段名是不能带引号的,否则就会被认为是一个字符串而不是字段名,然而使用PreapareStatement将会强制给参数加上’,所以,在使用order by语句时就必须得使用拼接的Statement,所以就会造成SQL注入,需要进行手动过滤,否则存在sql注入。
String sortOrder = "username";
String sql = "SELECT * FROM users ORDER BY ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, sortOrder); // 试图将列名绑定到占位符
ResultSet rs = pstmt.executeQuery();
在这个示例中,SQL 语句会被解析为:
SELECT * FROM users ORDER BY 'username'
这里 'username'
被视为字符串,而不是列名,导致 SQL 语法错误,因为 ORDER BY
子句后面期望的是列名或位置,而不是字符串。
正确处理方式
由于 ORDER BY
子句中的参数不能使用占位符进行预编译,我们需要手动验证和拼接列名。这需要严格的输入验证,确保用户输入的列名是合法的列名。
安全的示例代码
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Arrays;
import java.util.List;
public class UserQueryWithOrderBy {
// 允许排序的合法列名列表
private static final List<String> ALLOWED_SORT_COLUMNS = Arrays.asList("id", "username", "password");
public static ResultSet getUsersWithOrderBy(Connection connection, String sortOrder) throws SQLException {
if (!ALLOWED_SORT_COLUMNS.contains(sortOrder)) {
throw new IllegalArgumentException("Invalid sort column");
}
String sql = "SELECT * FROM users ORDER BY " + sortOrder;
Statement stmt = connection.createStatement();
return stmt.executeQuery(sql);
}
}
Mybatis
什么是mybatis
MyBatis 是一款优秀的持久层框架,可以理解为 MyBatis 就是对 JDBC 访问数据库的过程进行了封装,简化了 JDBC 代码,解决 JDBC 将结果集封装为 Java 对象的麻烦,使开发者只需要关注 SQL 本身,而不需要花费精力去处理例如注册驱动、创建 connection、创建 statement、手动设置参数、结果集检索等 JDBC 繁杂的过程代码。
具体使用时,MyBatis 通过 xml 或注解的方式将要执行的各种 statement(statement、preparedStatemnt)配置起来,并通过 Java 对象和 statement 中的 SQL 进行映射生成最终执行的 SQL 语句,最后由 MyBatis 框架执行 SQL 并将结果映射成 Java 对象并返回。
架构图
- mybatis-config.xml 是Mybatis的核心配置文件,通过其中的配置可以生成SqlSessionFactory,也就是SqlSession工厂;
- SqlSessionFactory 可以生成 SqlSession 对象
- SqlSession 是一个既可以发送 SQL 去执行,并返回结果,类似于 JDBC 中的 Connection 对象,也是 MyBatis 中至关重要的一个对象;
- Executor 是 SqlSession 底层的对象,用于执行 SQL 语句;
- MapperStatement 对象也是 SqlSession 底层的对象,用于接收输入映射(SQL 语句中的参数),以及做输出映射(即将 SQL 查询的结果映射成相应的结果)。
每一个Mapper都是为了一个具体的业务
环境搭建
我们来搭建
- 创建sql数据表然后插入数据
- **配置MyBatis:**确保你的MyBatis配置文件(通常是
mybatis-config.xml
)已经正确配置了数据源、事务管理器和其他相关设置。 - **编写Mapper接口和XML:**定义你的Mapper接口和对应的Mapper XML文件,或者使用注解来直接在接口方法上写SQL语句。
- 编写实体类配置log4j
- **编写测试用例:**使用JUnit或其他测试框架来编写你的测试用例
配置sql数据和实体类
我们的实体类就要根据这个来写
package com.dianchou.bean;
/**
* @author lawrence
* @create 2020-07-10 19:32
*/
public class Employee {
private Integer id;
private String lastName; //注意:与数据表字段不一样,可以使用别名
private String email;
private String gender;
public Employee() {
}
public Employee(Integer id, String lastName, String email, String gender) {
this.id = id;
this.lastName = lastName;
this.email = email;
this.gender = gender;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
@Override
public String toString() {
return "Employee{" +
"id=" + id +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +
", gender='" + gender + '\'' +
'}';
}
}
maven依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>mybatis</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
配置mybatis-config.xml
这个配置文件是用来连接我们数据库的和处理我们的mapper映射,每次创建一个mapper都需要添加进去
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 配置 mybatis 的环境 -->
<environments default="development">
<!-- 配置 mysql 的环境 -->
<environment id="development">
<!-- 配置事务的类型 -->
<transactionManager type="JDBC"/>
<!-- 配置连接数据库的信息:用的是数据源(连接池) -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/security"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<!--sql映射文件一定要注册到全局配置文件中-->
<mappers>
<mapper resource="com.dianchou.dao/EmployMappper.xml"></mapper>
</mappers>
</configuration>
编写Mapper接口和XML
Mapper接口
package com.dianchou.dao;
import com.dianchou.bean.Employee;
import java.util.List;
public interface EmployeeMapper {
List<Employee> getEmps();
}
XML配置文件
里面是我们具体的sql语句
<?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.dianchou.dao.EmployeeMapper">
<select id="getEmps" resultType="com.dianchou.bean.Employee">
select * from employee
</select>
</mapper>
配置log4j
# Set root category priority to INFO and its only appender to CONSOLE.
#log4j.rootCategory=INFO, CONSOLE debug info warn error fatal
log4j.rootCategory=debug, CONSOLE, LOGFILE
# Set the enterprise logger category to FATAL and its only appender to CONSOLE.
log4j.logger.org.apache.axis.enterprise=FATAL, CONSOLE
# CONSOLE is set to be a ConsoleAppender using a PatternLayout.
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30c %x - %m\n
# LOGFILE is set to be a File appender using a PatternLayout.
log4j.appender.LOGFILE=org.apache.log4j.FileAppender
log4j.appender.LOGFILE.File=d:/axis.log
log4j.appender.LOGFILE.Append=true
log4j.appender.LOGFILE.layout=org.apache.log4j.PatternLayout
log4j.appender.LOGFILE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30c %x - %m\n
编写测试用例
import com.dianchou.bean.Employee;
import com.dianchou.dao.EmployeeMapper;
import org.apache.ibatis.session.SqlSession;
import org.junit.jupiter.api.Test;
import java.util.List;
public class UserDaoTest {
@Test
public void test(){
//第一步,获取SqlSession对象
SqlSession sqlSession = MybatisUntils.getSqlSession();
// 执行sql
//方式一:getMapper
EmployeeMapper userDao = sqlSession.getMapper(EmployeeMapper.class);
List<Employee> userList = userDao.getEmps();
for (Employee user : userList) {
System.out.println(user);
}
//关闭sqlSession
sqlSession.close();
}
}
运行成功得到我们的数据
Mybatis下的增删查改
select
根据参数查询
构建一个接口
package com.dianchou.dao;
import com.dianchou.bean.Employee;
public interface byid {
Employee getUserById(int id);
}
编写mapper文件中的sql语句
<?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.dianchou.dao.byid">
<select id="getUserById" resultType="com.dianchou.bean.Employee">
select * from employee where id = #{id}
</select>
</mapper>
当然还有一种写法使用注解
public interface CategoryMapper {
@Select("select * from category_ where name= '${name}' ")
public CategoryM getByName(@Param("name") String name);
}
编写测试类
import com.dianchou.bean.Employee;
import com.dianchou.dao.byid;
import org.apache.ibatis.session.SqlSession;
import org.junit.jupiter.api.Test;
public class Userid {
@Test
public void getUserById() {
SqlSession sqlSession = MybatisUntils.getSqlSession();
byid userid = sqlSession.getMapper(byid.class);
Employee user = userid.getUserById(1);
System.out.println(user);
sqlSession.close();
}
}
运行
inster
<insert id="addUser" parameterType="com.hwt.pojo.User">
insert into mybatis.user (id,name,pwd) values (#{id},#{name},#{pwd});
</insert>
update
xml复制代码<update id="updateUser" parameterType="com.hwt.pojo.User">
update mybatis.user set name=#{name} ,pwd=#{pwd} where id=#{id};
</update>
delete
xml复制代码<delete id="deleterUser" parameterType="int">
delete from mybatis.user where id=#{id};
</delete>
#{}分析
环境搭建
实现类
import com.dianchou.bean.Employee;
import com.dianchou.dao.Username;
import org.apache.ibatis.session.SqlSession;
import org.junit.jupiter.api.Test;
import java.util.List;
public class username {
@Test
public void findname() {
SqlSession sqlSession = MybatisUntils.getSqlSession();
Username username = sqlSession.getMapper(Username.class);
Employee employees = username.getUserByname("OYWL");
System.out.println(employees);
sqlSession.close();
}
}
接口
package com.dianchou.dao;
import com.dianchou.bean.Employee;
public interface Username {
Employee getUserByname(String params);
}
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.dianchou.dao.Username">
<select id="getUserByname" resultType="com.dianchou.bean.Employee" >
SELECT * FROM employee WHERE lastName = #{lastName}
</select>
</mapper>
调试分析
初始化过程
- Resource获取全局配置文件
- 实例化SqlsessionFactoryBuilder
- 解析配置文件流XMLCondigBuilder
- Configration所有的配置信息
- SqlSessionFactory实例化
首先调用getResourceAsStream方法获取我们的配置文件
InputStream inputStream = Resources.getResourceAsStream(resource);
构造SqlSessionFactoryBuilder对象
new SqlSessionFactoryBuilder()
调用对象的build方法解析配置文件,可以看见是构建了一个XMLConfigBuilder对象,它的主要作用是
- 解析 MyBatis 配置文件:读取并解析
mybatis-config.xml
文件的内容。 - 构建
Configuration
对象:根据解析的配置信息,创建并配置 MyBatis 的Configuration
对象。 - 加载映射器:解析并加载 Mapper 配置文件,以便 MyBatis 能够将 SQL 映射到 Java 方法。
parse方法就是解析我们的配置文件生成Configuration` 对象
我重点关注我们sql语句的地方,跟进parseConfiguration方法,可以看见在处理各种标签,我们重点关注mapperElement方法,因为我们的xml文件就是在这里解析的
然后一路来到,它会找到我们的这个路由,然后调用configurationElement
configurationElement(parser.evalNode("/mapper"));
来到buildStatementFromContext,审计过jdbc,这个Statement对象
- 执行 SQL 语句:可以执行静态 SQL 查询、更新和批量操作。
- 处理结果集:可以处理 SQL 查询返回的结果集(
ResultSet
)
我们mapper配置的有三个,它会一个一个调用来解析,我们这里只分析
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
我们主要分析com.dianchou.dao.Username
SELECT * FROM employee WHERE lastName = #{lastName}
一路来到createSqlSource方法
我们的sqlSource是对象继承了SqlSource
接口,可以方便地从不同的源(如 String、File、InputStream 等)获取 SQL 语句,然后将其传递给数据库访问框架进行执行。和sql语句相关
一路来到parseScriptNode方法,它是判断sql语句是否包含动态内容,动态内容通常包括 <if>
、<choose>
、<where>
等标签或者 ${}
占位符。
我们的是#{}所以是静态的
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
重要的初始化就完成了
获取SqlSession
对象和Mapper
SqlSession sqlSession = MybatisUntils.getSqlSession();
Username username = sqlSession.getMapper(Username.class);//就是我们的接口
SqlSession
是 MyBatis 中用于执行 SQL 操作的关键接口。通过它可以执行查询、插入、更新和删除操作,并且可以管理事务。
Mapper 接口的实现可以获取到的 Username
实例的方法,可以执行相应的 SQL 操作
构建sql语句
Employee employees = username.getUserByname("OYWL");
跟进getUserByname方法
这里我主要关注sql语句的构建和执行
query:81, CachingExecutor (org.apache.ibatis.executor)
selectList:148, DefaultSqlSession (org.apache.ibatis.session.defaults)
selectList:141, DefaultSqlSession (org.apache.ibatis.session.defaults)
selectOne:77, DefaultSqlSession (org.apache.ibatis.session.defaults)
execute:82, MapperMethod (org.apache.ibatis.binding)
invoke:59, MapperProxy (org.apache.ibatis.binding)
getUserByname:-1, $Proxy11 (jdk.proxy2)
findname:21, username
invoke0:-1,
是在getBoundSql构建我们的sql语句的,传入的是我们的参数OYWL
来到我们的
sqlSource.getBoundSql(parameterObject);
调用sqlSource的这个方法,因为它含有我们的关键部分
这个sql是从我们的configuration得到的
最后sql语句返回的结果也是
SELECT * FROM employee WHERE lastName = ?
执行sql查询
看到这个应该很熟悉
StatementHandler
是 MyBatis 框架中的一个接口,它负责处理 SQL 语句的创建和执行,用于将 Mapper 方法调用转换为具体的 JDBC 操作。
- 准备 SQL 语句:创建和准备
Statement
对象(例如Statement
、PreparedStatement
或CallableStatement
),并为其设置必要的参数。 - 执行 SQL 语句:根据不同的操作类型(如查询、更新、删除),执行相应的 SQL 语句。
处理查询结果
因为我们是把结果映射到我们的对象上的,最后会调用对象的setter方法为把得到的值返回
可以看到结果是个Employee对象
Employee employees = username.getUserByname("OYWL");
System.out.println(employees);
总结
可以发现它几乎是没有什么漏洞的,因为它和我们的预编译流程是一样的
${}分析
环境搭建
我们还是使用和刚刚一样的环境,不过改一下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.dianchou.dao.Username">
<select id="getUserByname" resultType="com.dianchou.bean.Employee" >
SELECT * FROM employee WHERE lastName = '${lastName}'
</select>
</mapper>
运行结果
我们运行发现
org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'lastName' in 'class java.lang.String'
### Cause: org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'lastName' in 'class java.lang.String'
所以下面的调试分析主要是围绕这个问题展开
为什么会这样呢?
报错解决
调试分析
我们找到报错的地方
是在Reflector的这个方法
public Invoker getGetInvoker(String propertyName) {
Invoker method = getMethods.get(propertyName);
if (method == null) {
throw new ReflectionException("There is no getter for property named '" + propertyName + "' in '" + type + "'");
}
return method;
}
下个断点看看是怎么回事
我们先跟着走一遍
它的初始化过程几乎和上面是一样的,不过在我们的sqldatasource不一样,因为它是动态的sql语句
在这的时候因为我们的sqlSource不同,所以getBoundSql也不同
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
首先创建了一个DynamicContext获取了上下文信息,封装了我们的configuration, parameterObject然后传入apply方法
然后是解析我们的sql语句
来到parser方法,它是解析我们sql语句的主要逻辑的地方
就是解析包含特定占位符的文本,并将这些占位符替换为对应的值
重点进入它的
builder.append(handler.handleToken(expression.toString()));
可以先获取我们传入参数的值,但是一个object类型的,我传入的是String,然后就是判断我们的是否为简单类型,如果是上下文中绑定一个名为 "value"
的键,其值为该参数对象
然后一路跳转来到了我们报错的地方
其实观察这个过程可以发现是去找getter方法
可以发现这里是在获取我们的getter方法,然后寻找我们传入的lastName的getter方法,但是没有找到
我们的method只有这些方法,但是我们自己定义的实体类确实有这个getter方法啊
我们往前看看这个Method是怎么获取到的
是从reflector对象的getter方法
public Invoker getGetInvoker(String name) {
return reflector.getGetInvoker(name);
}
我们看看这个对象是怎么来的
是根据我们的type来获取的,这样说的话,我们的type应该为自己定义的类
this.reflector = reflectorFactory.findForClass(type);
我们寻找这个type是哪里传入的,在这个构造方法,我们再看看谁调用了它
可以发现是根据我们传入的object决定的
重新搭建环境
我们把接口的参数类型改改
public interface Username {
// Employee getUserByname(String params);
Employee getUserByname(Employee lastName);
}
更改测试代码
因为上面的话是会调用我们的getter方法获取值的,所以我们需要先设置好值
import com.dianchou.bean.Employee;
import com.dianchou.dao.Username;
import org.apache.ibatis.session.SqlSession;
import org.junit.jupiter.api.Test;
public class username {
@Test
public void findname() {
SqlSession sqlSession = MybatisUntils.getSqlSession();
Username username = sqlSession.getMapper(Username.class);
Employee params=new Employee();
params.setLastName("OYWL");
Employee employees = username.getUserByname(params);
System.out.println(employees);
sqlSession.close();
}
}
运行一下,成功
调试分析
我们还是看看刚刚那个过程
可以看到是正常了,通过我们的getter方法获取到了值,然后替换
后面就不分析了
漏洞点
select * from users where username like '${lastName}'
这样我们如果输入万能语句,可以发现查询到了,只是因为我们的结果太多l
使用list
List<Employee> employees = username.getUserByname(params);
for (Employee user : employees) {
System.out.println(user);
}
接口也改一下
List<Employee> getUserByname(Employee lastName);
模糊查询
我自己也测试了,懒得截图了,用这个
<select id="findByUserNameVuln02" parameterType="String" resultMap="User">
select * from users where username like '%${_parameter}%'
</select>
正确写法如下:
<select id="findByUserNamesec" parameterType="String" resultMap="User">
select * from users where username like concat('%',#{_parameter}, '%')
</select>
还有其他的,和JDBC大差不差
参考
https://www.cnblogs.com/CoLo/p/15225346.html#like%E6%B3%A8%E5%85%A5-1
https://tttang.com/archive/1726/
ion();
Username username = sqlSession.getMapper(Username.class);
Employee params=new Employee();
params.setLastName(“OYWL”);
Employee employees = username.getUserByname(params);
System.out.println(employees);
sqlSession.close();
}
}
运行一下,成功
[外链图片转存中...(img-Xk9vqgBB-1718692013011)]
#### 调试分析
我们还是看看刚刚那个过程
可以看到是正常了,通过我们的getter方法获取到了值,然后替换
[外链图片转存中...(img-9Nzy5yD4-1718692013011)]
后面就不分析了
### 漏洞点
```java
select * from users where username like '${lastName}'
这样我们如果输入万能语句,可以发现查询到了,只是因为我们的结果太多l
[外链图片转存中…(img-I7STO58I-1718692013011)]
使用list
List<Employee> employees = username.getUserByname(params);
for (Employee user : employees) {
System.out.println(user);
}
接口也改一下
List<Employee> getUserByname(Employee lastName);
[外链图片转存中…(img-mPbK66Cm-1718692013011)]
模糊查询
我自己也测试了,懒得截图了,用这个
<select id="findByUserNameVuln02" parameterType="String" resultMap="User">
select * from users where username like '%${_parameter}%'
</select>
[外链图片转存中…(img-G19F7tVe-1718692013011)]
正确写法如下:
<select id="findByUserNamesec" parameterType="String" resultMap="User">
select * from users where username like concat('%',#{_parameter}, '%')
</select>
还有其他的,和JDBC大差不差
参考
https://www.cnblogs.com/CoLo/p/15225346.html#like%E6%B3%A8%E5%85%A5-1
https://tttang.com/archive/1726/