java代码审计sql注入2(jdbc)

https://www.cnblogs.com/nihao-xing/p/23459079

承接上文

本篇文章展示BookStore-jdbc的审计思路。

项目地址:https://github.com/StarryZB/BookStore

第1步:肯定是判断技术框架

BookStore 没有 pom.xml,是 Eclipse 老项目

思考:你一开始不知道这是jdbc,怎么确认这是jdbc

信号①:看依赖(构建文件 / lib 目录)

没有 pom.xml,直接看 WEB-INF/lib/:

复制代码
web/WEB-INF/lib/mysql-connector-java-5.1.40-bin.jar   ← 全项目唯一的数据库 jar

只有 mysql-connector-java(数据库驱动),没有任何 ORM/框架 jar → 说明数据访问是手写的,没走框架。

对照:如果出现 mybatis-*.jar = MyBatis,hibernate-*.jar = JPA/Hibernate,jfinal-*.jar = JFinal。

信号②:看 import(一眼定位 SQL 层来自哪个包)

bash 复制代码
grep -rhE "^import" --include="*.java" . | sort | uniq -c

BookStore 结果里只有:

复制代码
2  import java.sql.*        ← JDK 自带的标准 JDBC 包

没有 org.apache.ibatis.*、没有 javax.persistence.*、没有 com.jfinal.*、没有 org.springframework.jdbc.* → 是原生 JDBC。

import 特征 框架
java.sql.* 原生 JDBC
org.apache.ibatis.* MyBatis
javax.persistence.* / jakarta.persistence.* JPA/Hibernate
com.jfinal.* / io.jboot.* JFinal/JBoot
org.springframework.jdbc.* Spring JdbcTemplate

信号③:看代码特征(铁证)

打开 DBUtil.java,原生 JDBC 有固定的四连:

java 复制代码
Class.forName("com.mysql.jdbc.Driver");              // ① 反射加载驱动
Connection conn = DriverManager.getConnection(url, username, password);  // ② DriverManager 拿连接
Statement stmt = conn.createStatement();             // ③ 创建 Statement
ResultSet rs = stmt.executeQuery(sql);               // ④ 执行 SQL 字符串

只要看到 DriverManager.getConnection + createStatement/executeQuery 这套,就是原生 JDBC,不用再怀疑。


第 2 步:锁定 Sink(数据库执行点)

在 IDEA 里用 全局搜索(Ctrl+Shift+F) 搜 JDBC 的危险 API 关键词:

关键词 含义 是否危险
createStatement() 普通语句对象 危险,可被注入
executeQuery(sql) 执行查询 危险,传入拼接 SQL
prepareStatement( 预编译 安全(若用 ? 占位)
executeUpdate / execute 执行更新/任意 同样需要追

createStatement()有且仅有一处在DBUtiljava 35

复制代码
 public static Statement createStmt(Connection conn) {
        Statement stmt = null;
        try {
            stmt = conn.createStatement();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return stmt;
    }
    public static ResultSet executeQuery(Statement stmt, String sql) {
        ResultSet rs = null;
        try {
            rs = stmt.executeQuery(sql);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return rs;
    }

stmt.executeQuery(sql) 一处SQL 执行点,只要找到谁往 executeQuery 传了拼接字符串,就能找到注入点。

第 3 步:在 IDEA 里反向追踪调用者

在这里全局搜索的结果也差不了多少

都集中在ProductDao.java

下面看下ProductDao.java下面的三处executeQuery

第一处

复制代码
public List<Product> getListProduct(int begin, int rows) {
        List<Product> productList = new ArrayList<Product>();
        String sql = "SELECT * FROM product LIMIT " + begin + " , " + rows;
        Connection connection = DBUtil.getConn();
        Statement statement = DBUtil.createStmt(connection);
        ResultSet resultSet = DBUtil.executeQuery(statement,sql);
        try {
            while (resultSet.next()) {
                Product product = new Product();
                product.setBook_id(resultSet.getInt("book_id"));
                product.setBook_name(resultSet.getString("book_name"));
                product.setBook_price(resultSet.getDouble("book_price"));
                productList.add(product);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.closers(resultSet);
            DBUtil.closestmt(statement);
            DBUtil.closeconn(connection);
        }
        return productList;
    }

思考:能不能注入

有的朋友肯定会说啊,这能能能,没有预编译,直接拼接,肯定能。

不能

光标放在getListProduct上

然后Ctrl+Alt+H 看调用层次,一次性就能拿到调用全链

复制代码
-ProductDao.getListProduct(int, int) (com.dao)
    -ProductService.getListProduct(int, int) (com.service)
        -PageServlet.doGet(HttpServletRequest, HttpServletResponse) (com.servlet)
        -LoginServlet.doPost(HttpServletRequest, HttpServletResponse) (com.servlet)

LoginServlet.doPost  ─┐
                      ├─→ ProductService.getListProduct ─→ ProductDao.getListProduct
PageServlet.doGet   ──┘         (转发)                        (真正拼 SQL)

PageServlet.doGet(HttpServletRequest, HttpServletResponse) (com.servlet)

这下面

复制代码
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        int page = Integer.parseInt(req.getParameter("page"));
        int begin = page * 7;
        ProductService productService = new ProductService();
        List<Product> listProduct = productService.getListProduct(begin,7);
        req.setAttribute("begin",begin);
        req.setAttribute("listproduct",listProduct);
        req.getRequestDispatcher("product.jsp").forward(req,resp);
    }
}

GET /page.action?page=N
        │
        ▼
int page = Integer.parseInt(req.getParameter("page"));  // ① 字符串 → int
int begin = page * 7;                                    // ② 计算偏移
productService.getListProduct(begin, 7);                 // ③ 进 DAO 拼 SQL
req.setAttribute("begin", begin);                        // ④ 传给 product.jsp 做翻页
forward("product.jsp");

page是强制的int类型,而begin 是page计算偏移的一个结果,所以begin这个参数肯定是没有注入点的。

思考:rows呢,这个参数有没有?

复制代码
// LoginServlet.java:38
listProduct = productService.getListProduct(0,7);   // rows = 7(字面量)

// PageServlet.java:28
listProduct = productService.getListProduct(begin,7); // rows = 7(字面量)

更没有,全是硬编码的7

看第二处

复制代码
public Product getProductById(String book_id) {
        String sql = "SELECT * FROM product WHERE book_id = " + book_id;
        Connection connection = DBUtil.getConn();
        Statement statement = DBUtil.createStmt(connection);
        ResultSet resultSet = DBUtil.executeQuery(statement,sql);
        Product product = new Product();
        try {
            while (resultSet.next()) {
                product.setBook_name(resultSet.getString("book_name"));
                product.setBook_price(resultSet.getDouble("book_price"));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.closers(resultSet);
            DBUtil.closestmt(statement);
            DBUtil.closeconn(connection);
        }
        return product;
    }

根据之前的,光标放在getProductById,和之前一样Ctrl+Alt+H 看调用层次

复制代码
 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        HttpSession session = req.getSession();
        String book_id = req.getParameter("book_id");
        ProductService productService = new ProductService();
        Product product = productService.getProductById(book_id);//用book_id,通过id取得实体类
        product.setBook_id(Integer.parseInt(book_id));
        if (session.getAttribute("idproductmap") != null) {
            Map<String,Product> idproductmap = (Map<String, Product>) session.getAttribute("idproductmap");
            Map<String,Integer> idnummap = (Map<String, Integer>) session.getAttribute("idnummap");
            if (idnummap.get(book_id) != null) {
                int book_num = idnummap.get(book_id) + 1;
                idproductmap.put(book_id,product);
                idnummap.put(book_id,book_num);
                session.setAttribute("idproductmap",idproductmap);
                session.setAttribute("idnummap",idnummap);
            } else {
                int book_num = 1;//暂时定为1
                idproductmap.put(book_id,product);//键为id,值为实体类
                idnummap.put(book_id,book_num);//键为id,值为数目,可以改添加数量
                session.setAttribute("idproductmap",idproductmap);
                session.setAttribute("idnummap",idnummap);
            }

        } else {
            Map<String,Product> idproductmap = new HashMap<String,Product>();//id-product
            Map<String,Integer> idnummap = new HashMap<String,Integer>();//id-num
            int book_num = 1;//暂时定为1
            idproductmap.put(book_id,product);//键为id,值为实体类
            idnummap.put(book_id,book_num);//键为id,值为数目,可以改添加数量
            session.setAttribute("idproductmap",idproductmap);
            session.setAttribute("idnummap",idnummap);
        }
        req.getRequestDispatcher("cart.jsp").forward(req,resp);

    }

哇,好长一串

复制代码
  String book_id = req.getParameter("book_id");
     ProductService productService = new ProductService();
     Product product = productService.getProductById(book_id);//用book_id,通过id取得实体类
     product.setBook_id(Integer.parseInt(book_id));

book_id 是String类型的

Integer.parseInt(book_id)出现在 SQL 执行之后,所以它挡不住注入。

复制代码
                    POST /buy.action
                       book_id=1 AND SLEEP(3)
                                 │
                                 ▼
                 BuyServlet.doPost(HttpServletRequest, HttpServletResponse)
                 (com.servlet)
                 ──────────────────────────────────────────────
                 String book_id = req.getParameter("book_id");   ← Source(String,无转换)
                 productService.getProductById(book_id);          ← 原样 String 传入
                 product.setBook_id(Integer.parseInt(book_id));   ← 在 SQL 之后才转 int
                                 │
                                 ▼
                 ProductService.getProductById(String book_id)
                 (com.service)
                 透明转发:return productDao.getProductById(book_id);
                                 │
                                 ▼
                 ProductDao.getProductById(String book_id)
                 (com.dao)
                 ──────────────────────────────────────────────
                 String sql = "SELECT * FROM product WHERE book_id = " + book_id;  ← Sink 拼接
                 DBUtil.executeQuery(statement, sql);            ← 执行恶意 SQL
                                 │
                                 ▼
                           MySQL 执行

Integer.parseInt(book_id) 为什么挡不住注入

看 BuyServlet.doPost 这两行的先后顺序:

复制代码
Product product = productService.getProductById(book_id);  // ① 先执行 SQL(含恶意 book_id)
product.setBook_id(Integer.parseInt(book_id));             // ② 后转 int(此时 SQL 早已执行完)
  • 第 ① 行已经把 book_id 拼进 SQL 并在数据库里执行了。SLEEP(3) 在这里真实生效。
  • 第 ② 行的 Integer.parseInt("1 AND SLEEP(3)") 会抛 NumberFormatException,但这只发生在 SQL 执行之后,只能让页面 500,无法阻止已经发生的注入。

过滤/校验必须发生在"进入 sink 之前"才有效;发生在 sink 之后的转换,只是"马后炮"。

看第三处。好像可以不用讲了。思路已经出来了。

我思考一下,我好像已经把思路讲的差不多了。第三处搞完后

然后我们一开始不是只是全局搜索了createStatement和executeQuery,还有prepareStatement和executeUpdate / execute,每一次搜索都是之前一样的for循环执行操作(只的审计的步骤像for循环一样没啥新奇的地方)。

本篇到此结束。