耗时两天半的java之sql注入代码审计

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个对象

  1. connection

    connection对象代表数据库

    可以设置数据库自动提交。事务提交(connection.commit()),事务回滚(connection.rollback())。

  2. statement

    调用connnection.createStatement()方法会返回一个statement对象。

    是具体执行sql语句

  3. PreparedStatement

    与statement对象的区别是,不直接放入sql语句,先用?作为占位符进行预编译,等预编译完成后,对?进行赋值,之后调用execute等方法不需要添加参数即可完成执行SQL语句。

  4. 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注入,那么久会有漏洞的存在,我们看看这个过程

因为它是直接把我们的语句拼接后去处理执行

我们试一试

image-20240616221911917

可以发现成功爆出了我们的数据库

分析

我们来分析一下

在这下断点

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);

这里进行的查询

  • 发送查询

    • 如果 packetnull,调用 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 类型写入数据包。

看到我们的数据包

image-20240616230051496

最后

return sendQueryPacket(callingQuery, sendPacket, maxRows, streamResults, cachedMetadata, resultSetFactory);

调用sendQueryPacket发送,然后返回结果

总结
创建 Statement 对象--执行 SQL 查询--execSQL--发送查询字符串--处理查询字符串的字符集和写入数据包--发送数据包并返回结果

PreparedStatement分析

复现

image-20240617011707109

可以看到我们的输入被当做字符串处理了,根本没有起到闭合作用,因为已经编译过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方法

image-20240617012054275

然后处理后返回给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 的构造方法

image-20240617012723624

    • DBC 驱动创建一个 PreparedStatement 对象,并将其与预编译的 SQL 语句相关联。
    • 该对象允许设置参数值,并在执行 SQL 语句时替换这些参数。
  1. 绑定参数

    • PreparedStatement 对象提供了一系列 set 方法,用于将参数值绑定到 SQL 语句中的占位符(?)。
    • 这些方法包括 setStringsetIntsetDate 等。

来到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);

image-20240617020041475

后面一模一样,不分析了

总结

其实和上面的不同就是绑定参数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%';

执行输出结果:

img

“%” 符号用于在模式的前后定义通配符(默认字母)

漏洞例子

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

image-20240617134439373

拼接之后得到了全部的数据

image-20240617134509311

解决方法还是使用我们的占位符

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架构图

  1. mybatis-config.xml 是Mybatis的核心配置文件,通过其中的配置可以生成SqlSessionFactory,也就是SqlSession工厂;
  2. SqlSessionFactory 可以生成 SqlSession 对象
  3. SqlSession 是一个既可以发送 SQL 去执行,并返回结果,类似于 JDBC 中的 Connection 对象,也是 MyBatis 中至关重要的一个对象;
  4. Executor 是 SqlSession 底层的对象,用于执行 SQL 语句;
  5. MapperStatement 对象也是 SqlSession 底层的对象,用于接收输入映射(SQL 语句中的参数),以及做输出映射(即将 SQL 查询的结果映射成相应的结果)。

每一个Mapper都是为了一个具体的业务

环境搭建

我们来搭建

  1. 创建sql数据表然后插入数据
  2. **配置MyBatis:**确保你的MyBatis配置文件(通常是mybatis-config.xml)已经正确配置了数据源、事务管理器和其他相关设置。
  3. **编写Mapper接口和XML:**定义你的Mapper接口和对应的Mapper XML文件,或者使用注解来直接在接口方法上写SQL语句。
  4. 编写实体类配置log4j
  5. **编写测试用例:**使用JUnit或其他测试框架来编写你的测试用例
配置sql数据和实体类

image-20240617160600949

我们的实体类就要根据这个来写

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();
    }
}

运行成功得到我们的数据

image-20240617160711380

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();
    }
}

运行

image-20240617163104184

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>
调试分析
初始化过程
  1. Resource获取全局配置文件
  2. 实例化SqlsessionFactoryBuilder
  3. 解析配置文件流XMLCondigBuilder
  4. Configration所有的配置信息
  5. SqlSessionFactory实例化

首先调用getResourceAsStream方法获取我们的配置文件

InputStream inputStream = Resources.getResourceAsStream(resource);
            

image-20240618010242974

构造SqlSessionFactoryBuilder对象

new SqlSessionFactoryBuilder()

调用对象的build方法解析配置文件,可以看见是构建了一个XMLConfigBuilder对象,它的主要作用是

  1. 解析 MyBatis 配置文件:读取并解析 mybatis-config.xml 文件的内容。
  2. 构建 Configuration 对象:根据解析的配置信息,创建并配置 MyBatis 的 Configuration 对象。
  3. 加载映射器:解析并加载 Mapper 配置文件,以便 MyBatis 能够将 SQL 映射到 Java 方法。

image-20240618010915558

parse方法就是解析我们的配置文件生成Configuration` 对象

image-20240618011458994

我重点关注我们sql语句的地方,跟进parseConfiguration方法,可以看见在处理各种标签,我们重点关注mapperElement方法,因为我们的xml文件就是在这里解析的

image-20240618011757077

然后一路来到,它会找到我们的这个路由,然后调用configurationElement

configurationElement(parser.evalNode("/mapper"));

来到buildStatementFromContext,审计过jdbc,这个Statement对象

  1. 执行 SQL 语句:可以执行静态 SQL 查询、更新和批量操作。
  2. 处理结果集:可以处理 SQL 查询返回的结果集(ResultSet

我们mapper配置的有三个,它会一个一个调用来解析,我们这里只分析

buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    

image-20240618012550528

我们主要分析com.dianchou.dao.Username

SELECT * FROM employee WHERE lastName = #{lastName}

一路来到createSqlSource方法

我们的sqlSource是对象继承了SqlSource 接口,可以方便地从不同的源(如 String、File、InputStream 等)获取 SQL 语句,然后将其传递给数据库访问框架进行执行。和sql语句相关

一路来到parseScriptNode方法,它是判断sql语句是否包含动态内容,动态内容通常包括 <if><choose><where> 等标签或者 ${} 占位符。

我们的是#{}所以是静态的

image-20240618013337004

sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);

image-20240618013658902

重要的初始化就完成了

获取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

image-20240618014432012

来到我们的

sqlSource.getBoundSql(parameterObject);
  

调用sqlSource的这个方法,因为它含有我们的关键部分

image-20240618014950135

这个sql是从我们的configuration得到的

image-20240618015551135

最后sql语句返回的结果也是

SELECT * FROM employee WHERE lastName = ?

执行sql查询

看到这个应该很熟悉

image-20240618015814278

StatementHandler 是 MyBatis 框架中的一个接口,它负责处理 SQL 语句的创建和执行,用于将 Mapper 方法调用转换为具体的 JDBC 操作。

  1. 准备 SQL 语句:创建和准备 Statement 对象(例如 StatementPreparedStatementCallableStatement),并为其设置必要的参数。
  2. 执行 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语句

image-20240618114651576

在这的时候因为我们的sqlSource不同,所以getBoundSql也不同

BoundSql boundSql = sqlSource.getBoundSql(parameterObject);

image-20240618122430683

首先创建了一个DynamicContext获取了上下文信息,封装了我们的configuration, parameterObject然后传入apply方法

然后是解析我们的sql语句

image-20240618123100132

来到parser方法,它是解析我们sql语句的主要逻辑的地方

就是解析包含特定占位符的文本,并将这些占位符替换为对应的值

重点进入它的

builder.append(handler.handleToken(expression.toString()));

可以先获取我们传入参数的值,但是一个object类型的,我传入的是String,然后就是判断我们的是否为简单类型,如果是上下文中绑定一个名为 "value" 的键,其值为该参数对象

image-20240618123702791

然后一路跳转来到了我们报错的地方

image-20240618124417549

其实观察这个过程可以发现是去找getter方法

可以发现这里是在获取我们的getter方法,然后寻找我们传入的lastName的getter方法,但是没有找到

image-20240618124544139

我们的method只有这些方法,但是我们自己定义的实体类确实有这个getter方法啊

image-20240618124643392

image-20240618124734246

我们往前看看这个Method是怎么获取到的

是从reflector对象的getter方法

public Invoker getGetInvoker(String name) {
    return reflector.getGetInvoker(name);
  }

我们看看这个对象是怎么来的

是根据我们的type来获取的,这样说的话,我们的type应该为自己定义的类

this.reflector = reflectorFactory.findForClass(type);

我们寻找这个type是哪里传入的,在这个构造方法,我们再看看谁调用了它

image-20240618125429702

可以发现是根据我们传入的object决定的

image-20240618125703912

重新搭建环境

我们把接口的参数类型改改

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();
    }
}

运行一下,成功

image-20240618133027689

调试分析

我们还是看看刚刚那个过程

可以看到是正常了,通过我们的getter方法获取到了值,然后替换

image-20240618133722251

后面就不分析了

漏洞点

 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>

image-20211230172916540.png

正确写法如下:

<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/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/721630.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2Y0A21 GP2Y0A21YK0F 红外测距传感器 arduino使用教程

三根线 正极负极信号线 确认自己的三根线分别代表什么&#xff0c;我们的颜色可能不一样 附一张我买的传感器他们的说明图 正极 接 开发板5V 负极 接开发板GND 信号线 接A0 代码arduino ide上面写 // Infračerven senzor vzdlenosti Sharp 2Y0A21// připojen potře…

新火种AI|Sora发布半年之后,AI视频生成领域风云再起

作者&#xff1a;一号 编辑&#xff1a;美美 AI视频最近有些疯狂&#xff0c;Sora可能要着急了。 自OpenAI的Sora发布以来&#xff0c;AI视频生成技术便成为了科技界的热门话题。尽管Sora以其卓越的性能赢得了广泛关注&#xff0c;但其迟迟未能面向公众开放&#xff0c;让人…

Study--Oracle-03-数据库常规操作

一路走来&#xff0c;所有遇到的人&#xff0c;帮助过我的、伤害过我的都是朋友&#xff0c;没有一个是敌人。 一、oracle 版本及主要功能 二、数据安装完成后常用操作SQL 1、检查数据库监听状态 监听的常用命令 启动&#xff1a;[oracleoracle u01]$ lsnrctl stop 停止&am…

买家用洗地机需要注意什么?全面评测热门洗地机品牌

对于忙碌的打工族来说&#xff0c;“做家务”是一件非常消费时间精力的事情&#xff0c;但它又是生活中的一部分&#xff0c;为了解决这些矛盾点&#xff0c;越来越多的清洁家电涌向市场&#xff0c;像集扫地、吸尘、洗地为一体的洗地机&#xff0c;在推拉之间便把脏污处理干净…

网络编程4----网络原理(面试及期末必备)

1 应用层 应用层是与程序员关系最密切的一层&#xff0c;在应用层这里了&#xff0c;很多时候&#xff0c;都是使用程序员自定义的协议&#xff0c;当然&#xff0c;也有很多现成的协议供我们使用。 “自定义协议”&#xff1a; 自定义一个协议&#xff0c;也就是自己做一个…

Idea连接GitLab的过程以及创建在gitlab中创建用户和群组

上期讲述了如何部署GitLab以及修复bug&#xff0c;这期我们讲述&#xff0c;如何连接idea。 首先安装gitlab插件 下载安装idea上并重启 配置ssh免密登录 使用管理员打开命令行输入&#xff1a;ssh-keygen -t rsa -C xxxaaa.com 到用户目录下.ssh查看id_rsa.pub文件 打开复制…

新兴互联网银行搭档Apache SeaTunnel构建数据流通管道!

当新兴互联网银行乘着数字化改革的风潮搭档数据集成平台Apache SeaTunnel&#xff0c;成千万上亿的数据就有了快速流通的管道。6月26日14:00&#xff0c;Apache SeaTunnel社区将带上企业最佳实践与观众见面&#xff0c;与大家面对面交流最新的企业实践部署经验。锁定SeaTunnel视…

OpenCV中的圆形标靶检测——findCirclesGrid()(二)

本章我们开始讲解基于层次聚类的标靶检测算法。当我们调用如下API,且flags中包含cv::CALIB_CB_CLUSTERING标志位时,将会执行基于层次聚类的斑点检测算法。算法支持对称标靶和非对称标靶两类,相应的需要将下述flags设为包含CALIB_CB_SYMMETRIC_GRID或CALIB_CB_ASYMMETRIC_GRI…

设计模式(五)创建者模式之工厂模式

工厂模式 工厂模式上面类图代码实现Coffee 抽象类AmericanCoffeeLatterCoffeeCoffeeStoreUser 简单工厂模式增加工厂方法更改CoffeeStore 类优缺点扩展静态工厂 工厂方法模式概念结构具体类图代码实现Coffee类AmericanCoffeeLatterCoffee抽象工厂CoffeeFactoryAmericanCoffeeFa…

OrangePi Kunpeng Pro 安装 ROS2 + Gazebo

文章目录 1. 初识1.1 到手开箱1.2 OrangePi Kunpeng Pro1.2 上电 2. 安装Ubuntu2.1 准备工作2.2 安装 3. 安装ROS23.1 设置支持UTF-8的locale编码3.2 添加证书3.3 安装ROS3.4 设置环境变量3.5 小海龟来啦 4. 运行实例4.1 安装Gazebo4.2 安装turtlebot 总结 1. 初识 1.1 到手开…

通勤必备!重回儿时回忆,完全免费!

哈喽&#xff0c;各位小伙伴们好&#xff0c;我是给大家带来各类黑科技与前沿资讯的小武。 今天给大家安利三个网站&#xff0c;可以完全免费在线玩的小霸王游戏机&#xff0c;重回童年游戏时刻&#xff01; 最后一款还可以网页在线和朋友联机CS、吃鸡、暗黑破坏神、拳皇、恐龙…

LVS负载均衡集群企业级应用实战-LVS-DR(四)

目录 LVS-DR 一. 环境准备 二. 对虚拟主机操作 三. 对真实服务器操作 四. 打开网页测试 LVS-DR 一. 环境准备 三台虚拟机&#xff0c;都要在同一网段内&#xff0c;统一关闭防火墙和selinux&#xff0c;时间同步&#xff0c;配置好YUM源。系统用centos和roucky都行。 主…

【ai】如何在ollama中随意使用hugging face上的gguf开源模型

【背景】 ollama的pull命令可以直接pull ollama列表中现有的模型&#xff0c;但是ollama可以直接pull的模型大都是英语偏好&#xff08;llama2有直接可以pull的chinese版本&#xff09;&#xff0c;而hugging face上则有大量多语种训练的模型&#xff0c;如果能直接使用huggin…

隧道代理是什么?怎么运作的?

隧道代理作为网络代理的一种形式&#xff0c;已经在现代互联网世界中扮演着重要的角色。无论是保护隐私、访问受限网站还是实现网络流量的安全传输&#xff0c;隧道代理都发挥着重要作用。在本文中&#xff0c;我们将深入探讨隧道代理的概念、运作方式以及在不同场景中的应用。…

如何秒杀系统架构设计

原文路径:https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e5%a6%82%e4%bd%95%e8%ae%be%e8%ae%a1%e4%b8%80%e4%b8%aa%e7%a7%92%e6%9d%80%e7%b3%bb%e7%bb%9f/00%20%e5%bc%80%e7%af%87%e8%af%8d%20%e7%a7%92%e6%9d%80%e7%b3%bb%e7%bb%9f%e6%9e%b6%e6%9e%84%e8%ae%be%e8%ae%…

国际现货黄金的交易方式:二次入场机会识别

近期受地缘局势以及通胀因素的影响&#xff0c;国际现货黄金投资又重新受到市场的青睐。虽然近期金价出现大跌&#xff0c;但投资者反而认为这是低价买金的好机会。为了方便投资者做出决策&#xff0c;下面我们就介绍一些国际现货黄金的交易方式——二次入场点进场。 在国际现货…

Java中序列化与反序列化(五)——static字段

static字段 1、概述2、静态字段的特性2.1、与类相关联2.2、唯一副本2.3、通过类名访问2.4、生命周期 3、静态字段不被序列化4、一个疑问4.1、作用4.2、使用4.2、为什么serialVersionUID是静态的4.2、为什么serialVersionUID会被“序列化” 5、总结 大家好&#xff0c;我是欧阳方…

经典文献阅读之--OrienterNet(自动驾驶中基于网格的交通场景感知)

dia 0. 简介 使用神经网络来匹配2D公开地图的做法是一个很有趣的方法&#xff0c;人们可以使用简单的2D地图在3D环境中指明自己所处的位置&#xff0c;而大部分视觉定位算法则依赖于昂贵的、难以构建和维护的3D点云地图。为了弥合这一差距《OrienterNet: Visual Localization…

项目计划

1.什么是项目计划&#xff1f; 2.软件项目计划的作用 3.项目计划的内容 4.项目计划的主要内容 5.滚动计划方法 6.WBS方法 7.软件项目的特点 8.制定计划的要点 9.直接成本和间接成本 10.为什么说项目计划不是一个文档&#xff0c;而是一个持续的策划过程&#xff1f; 项目计划不…

MySQL数据库回顾(1)

数据库相关概念 关系型数据库 概念: 建立在关系模型基础上&#xff0c;由多张相互连接的二维表组成的数据库。 特点&#xff1a; 1.使用表存储数据&#xff0c;格式统一&#xff0c;便于维护 2.使用SQL语言操作&#xff0c;标准统一&#xff0c;使用方便 SOL SQL通用语法 …