初识java代码审计(sql篇) sql注入 我们要先明确,java是强类型语言,实战我想我们也遇到不少尝试sql注入但是遇到java强制类型要求,参数只能是integer类型的java报错,这种是不存在sql注入的,所以只有变量是String类型的时候,才会存在sql注入。
现在就来看看常规的可以执行sql语句的程序:
JDBC 1、jdbc的java.sql.Statement Statement是Java JDBC下执行SQL语句的一种原生方式,执行语句时需要通过拼接来执行。若拼接的语句没有经过过滤,将出现SQL注入漏洞
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public static void main (String[] args) { String url = "jdbc:mysql://localhost:3306/security" ; String username = "root" ; String password = "root" ; Connection connection = null ; Statement statement = null ; ResultSet resultSet = null ; try { Class.forName("com.mysql.cj.jdbc.Driver" ); connection = DriverManager.getConnection(url, username, password); statement = connection.createStatement(); String sql = "select * from users" ; resultSet = statement.executeQuery(sql); while (resultSet.next()) { int id = resultSet.getInt("id" ); String name = resultSet.getString("username" ); String pass = resultSet.getString("password" ); System.out.println("ID: " + id + ", Name: " + name + ", Password: " +pass); } } catch (Exception e) { e.printStackTrace(); } finally { try { if (resultSet != null ) resultSet.close(); if (statement != null ) statement.close(); if (connection != null ) connection.close(); } catch (Exception e) { e.printStackTrace(); } } }
2、jdbc的PreparedStatement PreparedStatement使用了预编译的方式,是statement的升级版,更加安全、效率也更高。能有效防止sql注入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public static void main (String[] args) { String url = "jdbc:mysql://localhost:3306/security" ; String username = "root" ; String password = "root" ; Connection connection = null ; PreparedStatement preparedStatement = null ; ResultSet resultSet = null ; try { Class.forName("com.mysql.cj.jdbc.Driver" ); connection = DriverManager.getConnection(url, username, password); String sql = "SELECT * FROM users WHERE id = ?" ; preparedStatement = connection.prepareStatement(sql); preparedStatement.setInt(1 , 1 ); resultSet = preparedStatement.executeQuery(); while (resultSet.next()) { int id = resultSet.getInt("id" ); String name = resultSet.getString("username" ); String pass = resultSet.getString("password" ); System.out.println("ID: " + id + ", Name: " + name + ", Password: " +pass); } System.out.println(preparedStatement); } catch (Exception e) { e.printStackTrace(); } finally { try { if (resultSet != null ) resultSet.close(); if (preparedStatement != null ) preparedStatement.close(); if (connection != null ) connection.close(); } catch (Exception e) { e.printStackTrace(); } } }
ORM简介 ORM 是一种编程技术 ,用于在面向对象的编程语言(如 Java、Python)和关系型数据库(如 MySQL、PostgreSQL)之间进行数据映射 。它的主要目的是让开发者使用面向对象的方式操作数据库,而不是直接写 SQL 。也就是说它不是直接与数据库尽心交互,而是通过操作对象间接操作数据库。
Mybatis下的sql注入 MyBatis 是对 JDBC 的进一步封装与优化,它属于 半自动化的 ORM 框架 ,相比 Hibernate 灵活性更强,保留了 SQL 的可控性与性能优势。
工作原理简述:
开发者在 Mapper 文件(XML)中提前定义 SQL 语句 。
在 Java 正文中,只需调用 Mapper 接口方法,框架会自动完成 SQL 执行。
MyBatis 本质上是对 JDBC 的封装与升级版 ,简化了数据库操作流程。
直接步入mybtis的sql查询,根据上面的原理简述,我们的sql语句在Mapper的xml文档中,所以下面的代码也是Mapper文件中的。
安全用法(#{}
): 1 2 3 <select id ="getUserById" resultType ="User" > SELECT * FROM users WHERE id = #{id}</select >
生成的 SQL:
1 SELECT * FROM users WHERE id = ?
参数通过 JDBC 绑定,无法注入。
危险用法(${}
) 1 2 3 <select id ="getUsersBySort" resultType ="User" > SELECT * FROM users ORDER BY ${sortColumn}</select >
如果 sortColumn
是用户输入,传入:
最终拼接为:
1 SELECT * FROM users ORDER BY id desc ; DROP TABLE users
造成严重 SQL 注入漏洞,其利用形同最简单的sql注入
下面我们来看看常见的肯能存在漏洞的几种情况
基础漏洞代码 预编译语法错误导致sql注入,预编译虽然能够防止sql注入,但是如果程序员没有养成良好的习惯,也会存在sql注入的风险
1 2 3 String sql = "SELECT * FROM users WHERE id = ?" ; String username1="null" ; sql = sql + " and username like '%" + username1 + "%'" ;
如果我的username1传参为user%' or '1'='1'#
,此时语句就变成了
SELECT * FROM users WHERE id = 1 and username like '%user%' or '1'='1'#%’
返回结果也会是全部信息
下面就看一看需要用到${}的情况
Order by 注入 还有就是order by注入(order by 字段名)不能使用预编译的写法,因为order by语句后面必须要跟字段名(username、id这种)或者字段列数(1、2这种),字段列数肯定是可以预编译的,因为它是Integer类型,但是如果要采用字段名进行排序,就会出现问题,因为setString(1,username)后会自动给usernname套一个引号,而引号会让数据库认为这是一个字符串,而不是字段名,导致order by语句失效,字段列数就不会导致无法使用预编译,如下就是使用预编译的情况下的order by情况:
可以看到并没有使用username进行排序
我换为username所在的字段列数2试试,
这就成功了。所以要么是字段列数+预编译进行排序,要么就是字段名+不使用预编译的方式,
这里就也看看存在order by注入的漏洞代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 String sort = "username" ; String sql = "select * from users order by " + sort; System.out.println(sql); try (Connection connection = DriverManager.getConnection(url, username, password); Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery(sql)) { while (resultSet.next()) { String name = resultSet.getString("username" ); System.out.println(name); } } catch (SQLException e) { e.printStackTrace(); }
sort就是注入点:sort=”EXTRACTVALUE(1, CONCAT(0x7e, (SELECT DATABASE()), 0x7e))”
简单利用
Like模糊查询 select * from Book where name LIKE '%${id}%'
,语法不会报错,但是会存在sql注入
select * from Book where name LIKE '%#{id}%'
,语法会报错
为什么?
因为#{}相当于jdbc中的预编译,相当于语句为select * from Book where name LIKE '%?%'
%?%
,mysql是无法识别语句的,无法识别语法,所以会报错
模糊查询变式 对于模糊查询不代表就不能使用#{}了,下面就是可以使用几种变式来实现预编译
1、concat函数拼接: SELECT * FROM student WHERE name LIKE CONCAT('%', #{stuName}, '%')
2、mysql语法糖:SELECT * FROM student WHERE name LIKE "%"#{stuName}"%"
这就很好的规避了之前无法预编译的问题
in查询 我们现在先明确in查询有何妙用,它主要实现的功能是把原本只查询的id这个字段替换为了一个数组,来实现多次查询,从而避免查询语句的冗余,达到多次查询的结果。
2. 多值查询in之后的参数
1 Select * from news where id in (#{id})
这样的SQL语句虽然能执行但得不到预期结果,于是研发人员将SQL查询语句修改如下:
1 Select * from news where id in (${id})
修改后如下
所以就无法被预编译保护,存在sql注入
in查询变式 采用list数组传入,而不是通过字符串,这个方式就成功避免了无法预编译的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void main (String[] args) { BookDaoin bookDao = new BookDaoin (); List<Integer> ids = Arrays.asList(1 , 2 ); List<Book> books = bookDao.getBooksByIds(ids); books.forEach(System.out::println); } xml文件 <select id="selectBookByIds" parameterType="list" resultType="com.mybatis.dao.Book" > SELECT * FROM Book WHERE id IN <foreach collection="list" item="id" open="(" separator="," close=")" > #{id} </foreach> </select>
实战正则
实战一下 这是xx医院管理系统,其中用了Spring boot、mybatis框架
我们通过检索mapper文件的${
,来找sql注入,找到如下结果
因为用了Spring boot,我们就顺从其框架逻辑,主要一般会分成三层:Controller、Server、Dao
1 Controller → Service → DAO → Database
Controller
:处理 HTTP 请求(如接收前端参数)。
Service
:处理业务逻辑(如校验参数、事务管理)。
DAO
:直接操作数据库(如执行 SQL)。
所以我们就是使用从下往上去追踪getOption
Dao层
Server层
Controller层
看重要部分
mapper文档中,${}包裹的column是用columnName以路由的形式传入
此处也没什么别的过滤,简单构造一下payload:
1 2 3 http: 最终的sql语句为: SELECT distinct (select database() ) FROM users WHERE (select database() ) IS NOT NULL AND (select database() ) != ''
我们直接去web端是验证一下
JPA & Hibernate下的sql注入 JPA & Hibernate简介 Hibernate 是一个功能强大的 ORM 框架,可以把 Java 对象和数据库表自动进行映射。JPA(Java Persistence API) 是 Java 官方定义的 ORM 标准接口 ,它不是真正的 ORM 实现,而是一组 规范(接口) 。JPA 就像 JDBC 的接口,而 Hibernate 是 JPA 的实现类。
HQL注入 因为其高度封装性,我们看不到语句,更看到参数值。难道他就不存在sql注入了吗,那是不可能的。其实他存在HQL注入,HQL(Hibernate Query Language) 是面向对象的查询语言, 它和 SQL 查询语言有些相似。HQL查询并不直接发送给数据库,而是由hibernate引擎对查询进行解析并解释,然后将其转换为SQL 。因为不是直接与数据库进行交互,所以此处使用单引号是无法判断是否存在sql语句的。那我们如何判断呢?
最基础的漏洞代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public static void main (String[] args) { Configuration configuration = new Configuration (); Configuration configure = configuration.configure("hibernate.cfg.xml" ); SessionFactory sessionFactory = configure.buildSessionFactory(); Session session = sessionFactory.openSession(); try { String userInput = "2 or 1=1" ; String hql = "from Book where id = " + userInput; Query<Book> query = session.createQuery(hql, Book.class); Book book = query.uniqueResult(); if (book != null ) { System.out.println(book.toString()); } else { System.out.println("未找到对应的书籍记录" ); } } catch (Exception e) { e.printStackTrace(); } finally { if (session != null ) { session.close(); } if (sessionFactory != null ) { sessionFactory.close(); } }
这里通过 2 or 1=1
来判断是否存在注入,同时还要确保2是已存在的字段,那我们不知道存在字段呢,那只能使用枚举爆破了。
看起来报错了,但是是存在sql注入的,只是说查询函数返回的数据限制为1。这里返回三条所以报错了。
我们还可以使用更高级的模糊查询 ,String userInput = “0 or name like 'A%'
“,这个name是怎么来的,也是通过枚举、爆破、猜解的。依次遍历字符表 A-Z
, a-z
, 0-9
,可以逐步枚举出字段值。
HQL 限制说明
不支持 UNION SELECT
;
不能像原生 SQL 那样使用注入查看系统表(如 information_schema
);
你只能基于对象模型注入,所以字段名要提前知道;
不支持 -
注释(但某些实现允许 /* comment */
);
防注入写法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 public static void main (String[] args) { Configuration configuration = new Configuration (); Configuration configure = configuration.configure("hibernate.cfg.xml" ); SessionFactory sessionFactory = configure.buildSessionFactory(); Session session = sessionFactory.openSession(); try { String userInput = "1" ; Long bookId = Long.parseLong(userInput); String hql = "from Book where id = :id" ; Query<Book> query = session.createQuery(hql, Book.class); query.setParameter("id" , bookId); Book book1 = query.uniqueResult(); printResult("HQL + 参数绑定" , book1); CriteriaBuilder cb = session.getCriteriaBuilder(); CriteriaQuery<Book> cq = cb.createQuery(Book.class); Root<Book> root = cq.from(Book.class); cq.select(root).where(cb.equal(root.get("id" ), bookId)); Book book2 = session.createQuery(cq).uniqueResult(); printResult("Criteria API" , book2); Book book3 = session.get(Book.class, bookId); printResult("session.get()" , book3); } catch (Exception e) { e.printStackTrace(); } finally { if (session != null ) session.close(); if (sessionFactory != null ) sessionFactory.close(); } }private static void printResult (String method, Book book) { System.out.println("\n[查询方式: " + method + "]" ); if (book != null ) { System.out.println(book.toString()); } else { System.out.println("未找到对应的书籍记录" ); } }
HQL + 参数绑定:
适用于:需要使用 HQL 编写复杂查询语句时 优点:语义清晰、灵活度高、支持复杂筛选和排序 防注入机制:通过 :参数名 + setParameter() 实现安全绑定
Criteria API 查询:
适用于:构建动态、多条件、复杂查询时,完全对象化操作 优点:类型安全、可编程构建、免拼接 防注入机制:值通过 API 构造器设置,无字符串拼接风险
session.get() 主键查询:
适用于:只根据主键查数据(id)时 优点:语法最简单,效率高 防注入机制:get() 方法内部安全绑定主键参数
由于hibernate的sql注入确实及其少见,没有什么比较好的案例,我们了解其原理,以后遇到了翻翻笔记再实战即可
总结 对于java的sql注入,着重体现在框架、模块的注入,我们的代码审计的关注点也要根据对应搭建的框架进行特定化挖掘。