jdbc注入
经典拼接
@RequestMapping("/jdbc/vuln")
public String jdbc_sqli_vul(@RequestParam("username") String username) {
StringBuilder result = new StringBuilder();
try {
Class.forName(driver);
Connection con = DriverManager.getConnection(url, user, password);
if (!con.isClosed())
System.out.println("Connect to database successfully.");
// sqli vuln code
Statement statement = con.createStatement();
String sql = "select * from users where username = '" + username + "'";
logger.info(sql);
ResultSet rs = statement.executeQuery(sql);
......
}
代码逻辑非常简单,直接一句:@RequestParam
得到输入
RequestMapping是一个用来处理请求地址映射的注解
先测试了数据库是否正常连接,然后执行SQL语句,属于拼接,注入漏洞显而易见。而说这个的重点不在这里,而是去挖掘此类漏洞的时候。使用CodeQL去查找这种形式的注入,首先要找项目中所有以*Mapping
结尾的注释,然后get其输入,也就是@RequestParam
,通过数据污点追踪到最后执行方法*.executeQuery()
//主要逻辑语句
exists(Call call |
sink.asExpr() = call.getArgument(0) and
call.getCallee().hasQualifiedName("java.sql", "Statement", "executeQuery")
)
预编译
@RequestMapping("/jdbc/sec")
public String jdbc_sqli_sec(@RequestParam("username") String username) {
StringBuilder result = new StringBuilder();
try {
Class.forName(driver);
Connection con = DriverManager.getConnection(url, user, password);
if (!con.isClosed())
System.out.println("Connecting to Database successfully.");
// fix code
String sql = "select * from users where username = ?";
PreparedStatement st = con.prepareStatement(sql);
st.setString(1, username);
logger.info(st.toString()); // sql after prepare statement
ResultSet rs = st.executeQuery();
......
}
这里代码逻辑也很清晰,使用了预编译,先使用占位符然后再赋值
st.setString(1, username);
关于PreparedStatement.setString()
:将指定的参数设置为给定的Java字符串值。驱动程序将其发送到数据库时将其转换为SQL VARCHAR或LONGVARCHAR值(取决于参数的大小相对于驱动程序对VARCHAR值的限制)。也就是说在启用预编译的情况下,在进入数据库执行时,会自动加上引号(即转换字符串)。
最后再去执行
ResultSet rs = st.executeQuery();
下面调试下看看预编译都做了些什么,在executeQuery()
下断点
进入跟进
发现什么有用的信息都没有,继续向下
跟进查看,sql语句(含有占位符的)和用户输入的内容时分成两部分的,
漏洞常见处
- 预编译无法处理
order by
,因为order by
不能参数化。原因是 order by 子句后面需要加字段名或者字段位置,而字段名是不能带引号的,否则就会被认为是一个字符串而不是字段名,简单来说就是会报错。
使用 PreapareStatement
将会强制给参数加上'
,所以,在使用 order by 语句时就必须得使用拼接的 Statement
,所以就会造成 SQL 注入,需要手动过滤
String sql = "Select * from news where title =?" + "order by '" + time + "' asc"
- 使用like语句
String sql = "select * from users where password like '%" + con + "%'"; //存在sql注入
- 没有手动过滤
%
预编译无法处理这个符号,需要加入黑名单,否则会造成慢查询,进而造成DOS
- 以为自己使用了预编译,实则并没有
预编译必须使用?
做占位符
String sql = "select * from users where username = '" + username + "'";
PreparedStatement st = con.prepareStatement(sql);
logger.info(st.toString());
ResultSet rs = st.executeQuery();
- in语句
删除语句中可能会存在此类语句,由于无法确定delIds含有对象个数而直接拼接sql语句
String sql = "delete from users where id in("+delIds+"); //存在sql注入
MyBatis
MyBatis存在注入的主要一点就是${Parameter}
跟进查看,具体为什么MyBatis不在配置文件*.xml
中写着,这属于Spring自身的开发特点,也可以使用Java类作为配置文件,使用注释来开发,以实现这种效果。
@Select("select * from users where username = '${username}'")
List<User> findByUserNameVuln01(@Param("username") String username);
可以看到使用了${}
的就是直接拼接。
在MyBatis中使用预编译则是换了一种方式#{Parameter}
@Select("select * from users where username = #{username}")
User findByUserName(@Param("username") String username);
Mybatis中易发生SQL注入的情况
- like关键字模糊查询
<select id="findByUserNameVuln02" parameterType="String" resultMap="User">
select * from users where username like '%${_parameter}%'
</select>
原因就是在使用like进行模糊查询的时候,使用#{}
会报错,那么这个时候开发会将#{}
改为${}
来解决报错,但是这样就会造成拼接问题。
正确的解决方法应该是
<select id="findByUserNamesec" parameterType="String" resultMap="User">
select * from users where username like concat('%',#{_parameter}, '%')
</select>
不同数据库中
mysql:
select * from users where username like concat('%',#{username},'%')
oracle:
select * from users where username like '%'||#{username}||'%'
sqlserver:
select * from users where username like '%'+#{username}+'%'
- 使用in语句
同样的,使用in语句也会报错,改为${}
则会造成拼接。
<select id="findByUserNameVuln04" parameterType="String" resultMap="User">
select * from users where id in (${id})
</select>
// http://localhost:8080/sqli/mybatis/vuln04?id=1)%20or%201=1%23
@GetMapping("/mybatis/vuln04")
public List<User> mybatisVuln04(@RequestParam("id") String id) {
return userMapper.findByUserNameVuln04(id);
}
正确的改写方法是使用 foreach
<select id="findByIdSec04" parameterType="String" resultMap="User">
SELECT
* from users WHERE id IN <foreach collection="id" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</select>
注意定义接口的时候,类型就要写成List了
@GetMapping("/mybatis/sec04")
public List<User> mybatisSec04(@RequestParam("id") List id){
return userMapper.findByIdSec04(id);
}
在Mapper中写一下
List<User> findByIdSec04(@Param("id") List id);
- 至于order by则需要做好过滤,写个Filter过滤器。
MyBatis-plus框架下的SQL注入
参考:https://xz.aliyun.com/t/11672#toc-8
mark一下~
Hibernate框架下的SQL注入
Hibernate是一个开放源代码的对象关系映射框架,它对JDBC进行了非常轻量级的对象封装,使得Java程序员可以随心所欲的使用对象编程思维来操纵数据库。 HQL语句,也是采用拼接的方式做增删查改等操作。而HQL一般采用参数绑定来有效避免注入问题。- 命名参数
Query<User> query = session.createQuery("from users name = ?1", User.class);
String parameter = "g1ts";
Query<User> query = session.createQuery("from users name = :name", User.class);
query.setParameter("name", parameter);
- 位置参数
String parameter = "g1ts";
Query<User> query = session.createQuery("from users name = ?1", User.class);
query.setParameter(1, parameter);
- 命名参数列表
List<String> names = Arrays.asList("g1ts", "g2ts");
Query<User> query = session.createQuery("from users where name in (:names)", User.class);
query.setParameter("names", names);
- 类实例
user1.setName("g1ts");
Query<User> query = session.createQuery("from users where name =:name", User.class);
query.setProperties(user1);
- HQL拼接
<font style="color:rgb(51, 51, 51);">StringEscapeUtils.escapeSql()</font>
对参数进行过滤:(算是黑名单吧)
import org.apache.commons.lang.StringEscapeUtils;
public static void main(String[] args) {
String str = StringEscapeUtils.escapeSql("'");
System.out.println(str);
}
Hibernate也支持原生SQL语句执行,也会有使用拼接而直接造成SQL注入问题。