目录
本节讲述JavaWeb代码审计中存在SQL注入漏洞的情况。
基于spring boot,mysql,两种连接方式:jdbc和mybatis。
代码比较多,适合自己创建项目,跟着走一遍,如果只想了解原理,可以忽略代码,只看大概即可。比如Controller类或者sql语句的编写。
SQL注入漏洞是对数据库进行的一种攻击方式。 其主要形成方式是在数据交互中,前端数据通过后台在对数据库进行操作时,由于没有做好安全防护,导致攻击者将恶意代码拼接到请求参数中,被当做SQL语句的一部分进行执行,最终导致数据库被攻 击。 可以说所有可以涉及到数据库增删改查的系统功能点都有可能存在SQL注入漏洞。 虽然现在针对SQL注入的防护层出不穷,但大多情况下由于开发人员的疏忽或特定的使用场景,还是会存在SQL注入漏洞的代码。
一、新建一个spring boot的有sql环境的工程
选择这四个依赖进入
数据库创建好 加点数据,我用的phpstudy,mysql。
项目结构如下
打开 src/main/resources/application.properties 配置文件,将以下数据库连接信息添加至配置中
#访问端口号
server.port=7089
#数据库连接信息
spring.datasource.url=jdbc:mysql://localhost:3306/jdbcdemo?AllowPublicKeyRetrieval=true&useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
二、JDBC的SQL注入
动态拼接
SQL语句动态拼接导致的SQL注入漏洞是先前最为常见的场景。 其主要原因是后端代码将前端获取的参数动态直接拼接到SQL语句中使用 java.sql.Statement 执行 SQL语句从而导致SQL注入漏洞的出现。
在这里关键点有两个:①、动态拼接参数。②、使用 java.sql.Statement 执行SQL语句。
2.1 java.sql.Statement
Statement 对象用于执行一条静态的 SQL 语句并获取它的结果。 createStatement() :创建一个 Statement 对象,之后可使用 executeQuery() 方法执行SQL语句。 executeQuery(String sql) 方法:执行指定的 SQL 语句,返回单个 ResultSet 对象。
2.2 示例代码
在 src\main\java\com\example\demo\jdbcinjection 下新建一个名为 JdbcDynamicController 的 Java Class。
package com.example.sqlinjection.jdbcinjection;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.sql.*;
@RestController
@RequestMapping("/jdbcsql")
public class JdbcDynamicController {
private static String driver = "com.mysql.cj.jdbc.Driver";
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String user;
@Value("${spring.datasource.password}")
private String password;
@RequestMapping("/dynamic")
public String jdbcdynamic(@RequestParam("id") String id) throws ClassNotFoundException, SQLException {
StringBuilder result = new StringBuilder();
Class.forName(driver);
Connection conn = DriverManager.getConnection(url, user, password);
Statement statement = conn.createStatement();
String sql = "select * from user where id = '" + id + "'";
ResultSet rs = statement.executeQuery(sql);
while (rs.next()) {
String rsUsername = rs.getString("username");
String rsPassword = rs.getString("password");
String info = String.format("%s: %s\n", rsUsername, rsPassword);
result.append(info);
}
rs.close();
conn.close();
return result.toString();
}
}
解释一下,StringBuilder是一个类,可以追加字符串,用于字符串拼接。
单引号闭合的sql语句。
使用闭合1' or 1=1%23,直接爆出了所有数据,可以看到是有sql注入的 。
执行sql语句也是没问题的
2.3 预编译防御以及非正确使用
在动态拼接中是使用Statement执行SQL语句。如果使用 PreparedStatement 预编译参数化查询是能够 有效防止SQL注入的。 但如果没有正确的使用 PreparedStatement 预编译还是会存在SQL注入风险的。
PreparedStatement是继承Statement的子接口。 PreparedStatement 会对SQL语句进行预编译,不论输入什么,经过预编译后全都以字符串来执行SQL 语句。 PreparedStatement会先使用 ? 作为占位符将SQL语句进行预编译,确定语句结构,再传入参数进行执 行查询。如下述代码:
String sql = "select * from users where username = ?";
PreparedStatement preparestatement = conn.prepareStatement(sql);
preparestatement.setString(1, username);
但如果没有正确的使用 PreparedStatement 预编译还是会存在SQL注入风险的。 简单来说,可能由于开发人员疏忽或经验不足等原因,虽然使用了预编译 PreparedStatement ,但没 有根据标准流程对参数进行标记,依旧使用了动态拼接SQL语句的方式,进而造成SQL注入漏洞。 在 src\main\java\com\example\demo\jdbcinjection 下新建一个名为 JdbcPrepareStatement 的 Java Class,并键入以下代码。
package com.example.sqlinjection.jdbcinjection;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.sql.*;
@RestController
@RequestMapping("/jdbcsql")
public class JdbcPrepareStatement {
private static String driver = "com.mysql.cj.jdbc.Driver";
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String user;
@Value("${spring.datasource.password}")
private String password;
@RequestMapping("/preSec")
public String jdbcPreSec(@RequestParam("id") String id) throws ClassNotFoundException, SQLException {
StringBuilder result = new StringBuilder();
Class.forName(driver);
Connection conn = DriverManager.getConnection(url, user, password);
String sql = "select * from user where id=?";
PreparedStatement preparedStatement = conn.prepareStatement(sql);
preparedStatement.setString(1,id);
ResultSet rs = preparedStatement.executeQuery();
while (rs.next()) {
String resUsername = rs.getString("username");
String resPassword = rs.getString("password");
String info = String.format("%s: %s\n", resUsername, resPassword);
result.append(info);
}
rs.close();
conn.close();
return result.toString();
}
@RequestMapping("preNot")
public String jdbcPreNot(@RequestParam("id") String id) throws SQLException, ClassNotFoundException {
StringBuilder result = new StringBuilder();
Class.forName(driver);
Connection conn = DriverManager.getConnection(url, user, password);
//还是直接进行了拼接 无效
String sql = "select * from user where id = '" + id + "'";
PreparedStatement preparestatement = conn.prepareStatement(sql);
ResultSet rs = preparestatement.executeQuery();
while (rs.next()) {
String reUsername = rs.getString("username");
String resPassword = rs.getString("password");
String info = String.format("%s: %s\n", reUsername, resPassword);
result.append(info);
}
rs.close();
conn.close();
return result.toString();
}
}
正确的预编译代码效果:
错误的预编译代码效果:
2.4 order by注入
在SQL语句中, order by 语句用于对结果集进行排序。 order by 语句后面需要是字段名或者字段位 置。 在使用 PreparedStatement 预编译时,会将传递任意参数使用单引号包裹进而变为了字符串。 如果使用预编译方式执行 order by 语句,设置的字段名会被人为是字符串,而不再是字段名。 因此,在使用 order by 时,就不能使用 PreparedStatement 预编译了
下新建一个名为 jdbcOrderby 的Java Class, 并键入以下代码
package com.example.sqlinjection.jdbcinjection;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.sql.*;
@RestController
@RequestMapping("/jdbcsql")
public class jdbcOrderby {
private static String driver = "com.mysql.jdbc.Driver";
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String user;
@Value("${spring.datasource.password}")
private String password;
@RequestMapping("/PreOrderby")
public String jdbcOrderby(@RequestParam("id") String id) throws
ClassNotFoundException, SQLException {
StringBuilder result = new StringBuilder();
Class.forName(driver);
Connection conn = DriverManager.getConnection(url, user, password);
String sql = "select * from user" + " order by " + id;
PreparedStatement preparestatement = conn.prepareStatement(sql);
ResultSet rs = preparestatement.executeQuery();
while (rs.next()) {
String reUsername = rs.getString("username");
String resPassword = rs.getString("password");
String info = String.format("%s: %s\n", reUsername, resPassword);
result.append(info);
}
rs.close();
conn.close();
return result.toString();
}
}
利用报错函数,页面并没有返回值,但是在后端可以看到
可以结合if进行盲注
也可以直接尝试用sqlmap跑一下,这里就不再多说了。
三、Mybatis的SQL注入
MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎 所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映 射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
在Mybatis中拼接SQL语句有两种方式:一种是占位符 #{} ,另一种是拼接符 ${} 。
占位符 #{} :对传入的参数进行预编译转义处理。类似JDBC中的 PreparedStatement 。
拼接符 ${} :对传入的参数不做处理,直接拼接,进而会造成SQL注入漏洞。
#{} 可以有效防止SQL注入漏洞。 ${} 则无法防止SQL注入漏洞。 因此在我们对JavaWeb整合Mybatis系统进行代码审计时,应着重审计SQL语句拼接的地方。 除非开发人员的粗心对拼接语句使用了 ${} 方式造成的SQL注入漏洞。
在Mybatis中有几种场景是不能使用预编译方式的,比如: order by 、 in , like 。
3.1 示例代码
代码较多。
在 src.main.java.com.example.demo.mybatisinjection 包下新建一个Java Class,名为 User ,这是一个实体类,和数据表做下映射,键入以下代码
package com.example.sqlinjection.mybatisinjection;
public class User {
private int id;
private String username;
private String password;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User [id=" + id + ", username=" + username + ", password=" +
password + "]";
}
}
在 src.main.java.com.example.demo.mybatisinjection 文件下新建一个名为 UserMapper 的 Java Interface,键入以下代码
package com.example.sqlinjection.mybatisinjection;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@Mapper
public interface UserMapper {
List<User> orderbyInjection(@RequestParam("sort") String sort);
@Select("select * from user where id in (${params})")
List<User> inInjection(@Param("params")String params);
List<User> likeInjection(@Param("username") String username);
//Mybatis查询SQL语句的另一种使用注解方式,这也是存在SQL注入的。
//@Select("select * from users where username = '${username}'")
//List<User> likeInjection(@Param("username") String username);
}
在 src.main.resources 文件下先新建名为 mapper 的文件夹,再新建一个名为 UserMapper.xml 文件,与dao层的 UserMapper 做好映射绑定,键入以下代码
<?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.example.sqlinjection.mybatisinjection.UserMapper">
<resultMap type="com.example.sqlinjection.mybatisinjection.User" id="User">
<id column="id" property="id" javaType="java.lang.Integer" jdbcType="NUMERIC"/>
<id column="username" property="username" javaType="java.lang.String" jdbcType="VARCHAR"/>
<id column="password" property="password" javaType="java.lang.String" jdbcType="VARCHAR"/>
</resultMap>
<select id="orderbyInjection" parameterType="String" resultMap="User">
select * from user order by ${sort} asc
</select>
<select id="likeInjection" parameterType="String" resultMap="User">
select * from user where username like '%${username}%'
</select>
</mapper>
在 src.main.java.com.example.demo.mybatisinjection 文件下新建一个名为 MybatisController 的Java class,键入以下代码
package com.example.sqlinjection.mybatisinjection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/mybatisSql")
public class MybatisController {
@Autowired
private UserMapper userMapper;
//orderby查询 http://localhost:7089/sqli/mybatis/orderby?sort=username
@GetMapping("/orderby")
public List<User> orderbySql(@RequestParam("sort") String sort) {
return userMapper.orderbyInjection(sort);
}
//in查询 http://localhost:7089/sqli/mybatis/in?params=1
@GetMapping("/in")
public List<User> inSql(@RequestParam("params") String params) {
return userMapper.inInjection(params);
}
//Like查询 http://localhost:7089/sqli/mybatis/like?username=admin
@GetMapping("/like")
@ResponseBody
public List<User> likeSql(@RequestParam("username") String username){
return userMapper.likeInjection(username);
}
}
最后,在 src\main\resources\application.properties 配置文件中添加以下代码,最终如下 图所示
#?????
server.port=7089
#???????
spring.datasource.url=jdbc:mysql://localhost:3306/jdbcdemo?AllowPublicKeyRetrieval=true&useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
## Mybatis ??
## mybatis.typeAliasesPackage ???com.example.demo.dao??????????
mybatis.typeAliasesPackage=com.example.sqlinjection.mybatisinjection
## mybatis.mapperLocations ??? classpath ??? mapper ???* ??????? xml???
mybatis.mapperLocations=classpath:mapper/*.xml
3.2 orderby 注入
可以看到有明显的sleep现象,sql语句被执行,可以尝试盲注.
3.3 in注入
IN语句 :常用于where表达式中,其作用是查询某个范围内的数据。
比如: select * from where field in (value1,value2,value3,…); 如上所示,in在查询某个范围数据是会用到多个参数,在Mybtis中如果直接使用占位符 #{} 进行查询会 将这些参数看做一个整体,查询会报错。
因此很多开发人员可能会使用拼接符 ${} 对参数进行查询,从而造成了SQL注入漏洞。
比如: select * from users where id in (${params})
正确的做法是需要使用foreach配合占位符 #{} 实现IN查询。比如:
<!-- where in 查询场景 -->
<select id="select" parameterType="java.util.List" resultMap="BaseResultMap">
SELECT *
FROM user
WHERE name IN
<foreach collection="names" item="name" open="(" close=")" separator=",">
#{name}
</foreach>
</select>
3.4 like注入
使用like语句进行查询时如果使用占位符 #{} 查询时程序会报错(大家可自行调试)。
比如: select * from users where username like '%#{username}%' 因此经验不足的开发人员可能会使用拼接符 ${} 对参数进行查询,从而造成了SQL注入漏洞。 比如: select * from users where username like '%${username}%' 下面代码是正确的做法,可以防止SQL注入漏洞,如下。
SELECT * FROM users WHERE name like CONCAT("%", #{name}, "%")
四、SQL注入漏洞修复
php的sql注入漏洞修复也可以用在这里,经常用到正则匹配过滤危险输入。java的话这里主要是说关于预编译的修复。
通过上面我们其实知道,一般情况,预编译是可以解决所有输入的。因为一般情况,所有输入都会被当成字符串。但是在面对oeder by等查询时,预编译就没用了,原因是:order by后一般是接字段名,而字段名是不能带引号的,比如 order by username;如果带上引号成了order by 'username',那username就是一个字符串不是字段名了,这就产生了语法错误。预编译只有自动加引号的setString()方法,没有不加引号的方法;而另一方面order by后接的字段名不能有引号。
事实上,凡是字符串但又不能加引号的位置都不能参数化;包括sql关键字、库名表名字段名函数名等等。
不能参数化的位置,不管怎么拼接,最终都是和使用“+”号拼接字符串的功效一样:拼成了sql语句但没有防sql注入的效果。但好在的一点是,不管是sql关键字,还是库名表名字段名函数名对于后台开发者来说他的集合都是有限的,更准确点应该说也就那么几个。所以考虑可以使用黑白名单等加以限制。
4.1 白名单
正则过滤就不多说了,可以自己编写过滤特定关键字和函数名。
// 插入数据用户可控时,应使用白名单处理
// example for order by
String orderBy = "{user input}";
String orderByField;
switch (orderBy) {
case "name":
orderByField = "name";break;
case "age":
orderByField = "age"; break;
default:
orderByField = "id";
}
这里对列名进行白名单处理,所有的列名都在上面,但凡输入不是白名单的都会被赋值成id。
4.2 预编译(jdbc)
一般情况就预编译,正常写。特殊场景如下。
// like 模糊查询场景
String sql = "SELECT * FROM users WHERE name like ?";
PreparedStatement pre = conn.prepareStatement(sql);
pre.setString(1, "%"+name+"%");
ResultSet rs = pre.executeQuery();
// where in 查询场景
String sql = "select * from user where id in (";
Integer[] ids = new Integer[]{1,2,3};
StringBuilder placeholderSql = new StringBuilder(sql);
for(int i=0,size=ids.length;i<size;i++) {
placeholderSql.append("?");
if (i != size-1) {
placeholderSql.append(",");
}
}
placeholderSql.append(")");
PreparedStatement pre = conn.prepareStatement(placeholderSql.toString());
for(int i=0,size=ids.length;i<size;i++) {
pre.setInt(i+1, ids[i]);
}
ResultSet rs = pre.executeQuery();
4.3 预编译(mabatis)
<!-- like 查询场景 -->
<select id="select" parameterType="java.lang.String" resultMap="BaseResultMap">
SELECT *
FROM user
WHERE name like CONCAT("%", #{name}, "%")
</select>
<!-- where in 查询场景 -->
<select id="select" parameterType="java.util.List" resultMap="BaseResultMap">
SELECT *
FROM user
WHERE name IN
<foreach collection="names" item="name" open="(" close=")" separator=",">
#{name}
</foreach>
</select>