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 层来自哪个包)
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 有固定的四连:
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循环一样没啥新奇的地方)。
本篇到此结束。
浙公网安备 33010602011771号