网络安全之SQL注入深入分析

0x00 前言

我们知道代码审计Java的SQL注入主要有两点:参数可控和SQL语句可拼接(没有预编译)。并且我们也清楚修复SQL注入的方式就是预编译,但是可能我们并不清晰内部预编译的具体实现。本文主要从代码层面深入分析三种Java不同数据库框架下的SQL注入以及预编译。

0x01 JDBC SQLi

不使用占位符拼接情况分析

Statement statement = connection.createStatement();
String sql = "select * from user where id=" + value;
ResultSet resultSet = statement.executeQuery(sql);

不使用占位符时,输入的内容和sql拼接形成最终的sql语句:

image.png

预编译情况:

String sql = "select * from user where id=?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1,value);
ResultSet resultSet = preparedStatement.executeQuery();

预编译会在传入的字符串前后添加',然后再进行拼接,保证了输入的字符串在SQL语句中是数值而不是关键字。

image.png

最终在执行的时候select * from user where id='2 and 1=2 union select * from user'

image.png

到这里我们肯定会想就算在两边加了',也可以在value中添加'来闭合绕过:

2' and 1=2 union select * from user where '1'='1

然而事实并非那么简单,JDBC在ClientPreparedQueryBindings.setString()中对一些特殊符号包括'做了转义处理,因此预编译可以防止SQL注入:

image.png

【一>所有资源获取<一】
1、200份很多已经买不到的绝版电子书
2、30G安全大厂内部的视频资料
3、100份src文档
4、常见安全面试题
5、ctf大赛经典题目解析
6、全套工具包
7、应急响应笔记
8、网络安全学习路线

0x02 Mybatis SQLi

Mybatis解析执行过程

Mybatis解析执行过程如下图:

image.png

以查询SQL分析,主要步骤如下:

  1. SqlSession创建过程:SqlSessionFactoryBuilder().build(inputStream)创建一个SqlSession,创建的时候会进行配置文件解析生成Configuration属性实例,解析时会将mapper解析成MapperStatement加到Configuration中,MapperStatement是执行SQL的必要准备,SqlSource是MapperStatement的属性,实例化前会先创建动态和非动态SqlSource即DynamicSqlSource和RawSqlSource,DynamicSqlSource对应解析$以及动态标签如foreach,RawSqlSource创建时解析#并将#{}换成占位符?

  2. 执行准备过程:DefaultSqlSession.selectOne()执行sql(如果是从接口getMapper方式执行,首先会从MapperProxy动态代理获取DefaultSqlSession执行方法selectxxx|update|delete|insert),首先从Configuration获取MapperStatement,执行executor.query()。executor执行的第一步会先通过MapperStatement.getBoundSql()获取SQL,此时如果MapperStatement.SqlSource是动态即DynamicSqlSource,会先解析其中的动态标签比如${}会换成具体传入的参数值进行拼接,获取到SQL之后调用executor.doQuery(),如果存在预编译首先会调用JDBC处理预编译的SQL,最终通过PreparedStatementHandler调用JDBC执行SQL;

  3. JDBC执行SQL并返回结果集

如下是mapper的select示例,第一个使用${id},第二个使用#{id},我们具体通过调试来看下#$这两种符号的解析和执行过程中的处理方式。

<select id="getById" resultType="com.r17a.commonvuln.injection.sqli.mybatis.pojo.User">
    SELECT * FROM user where id=${id}
</select>
<select id="getByIdPrepare" resultType="com.r17a.commonvuln.injection.sqli.mybatis.pojo.User">
    SELECT * FROM user where id=#{id}
</select>

解析过程中$#的不同

在解析StatementNode过程中创建SqlSource时,会调用XMLScriptBuilder.parseScriptNode()来生成动态和非动态SqlSource

image.png

深入分析XMLScriptBuilder.parseScriptNode(),先调用XMLScriptBuilder.parseDynamicTags()解析动态tag

image.png

在解析时会先通过TextSqlNode.isDynamic()判断是否存在动态标志

image.png

TextSqlNode.isDynamic()首先创建一个DynamicCheckerTokenParser用来解析动态标识符,调用createParser创建GenericTokenParser

image.png

createParser会返回一个${}标识符的标识符解析

image.png

$解析过程:

继续下一步调用GenericTokenParser.parse()

image.png

GenericTokenParser.parse中找到了openhandler即${,会调用builder.append(handler.handleToken(expression.toString()))

image.png

handler.handleToken()将isDynamic标志为true

image.png

当isDynamic为true,会实例化一个DynamicSqlSource对象,至此$动态SqlSource创建完成。

image.png

image.png

#解析过程:

当SQL是SELECT * FROM user where id=#{id}的情况下调用isDynamic() ,进一步调用GenericTokenParser.parse()

image.png

GenericTokenParser.parse()中没有找到openhandler即${,就不会进入后面的处理,直接将原来的text进行返回,因此isDynamic还是false

image.png

返回后初始化一个RawSqlSource实例

image.png

在RawSqlSource初始化时会自动进行解析:

image.png

SqlSourceBuilder$ParameterMappingTokenHandler主要解析#{}的情况

image.png

#{id}替换成?进行占位,此时sql变成了SELECT * FROM user where id=?

image.png

image.png

小结:在创建SqlSource时,会根据$及动态标签来创建DynamicSqlSource,DynamicSqlSource不会对${}进行具体的处理,而非动态情况会创建RawSqlSource,在其初始化过程会直接将#{}替换成?占位符。

执行过程中$#的不同:

$在执行过程中的解析:

在调用MappedStatement.getBoundSql()时,由于$对应的是DynamicSqlSource,会调用DynamicSqlSource.getBoundSql()获取sql

image.png

DynamicSqlSource.getBoundSql()会调用rootSqlNode.apply()处理,此时调用的是TextSqlNode.apply()

image.png

TextSqlNode.apply()中会创建一个${}的GenericTokenParser然后进行parse解析和追加

image.png

在解析时,调用handler.handleToken()根据标识符获取参数的内容

image.png

handleToken()中会将参数值1 and 1=2 union select Host,User,1,authentication_string from mysql.user limit 1返回

image.png

拼接 最终获取的sql是SELECT * FROM user where id=1 and 1=2 union select Host,User,1,authentication_string from mysql.user limit 1

image.png

#在执行过程中的解析:

$是在getBoundSql()获取sql过程中就将符号进行了处理,跟$不同的是,#是在执行器的执行过程中(本例是doQuery)进行处理,先通过调用SimpleExecutor.prepareStatement()处理预编译情况后,获取statement,然后调用JDBC执行

image.png

深入prepareStatement(),发现其最终通过动态代理调用ClientPreparedStatement.setString()

image.png

调用JDBCClientPreparedStatement.setString()处理过程跟上述0x01部分的JDBC预编译处理statement一样。

image.png

注入场景:

除了上面的where,likeinorder by查询条件不能直接使用#{}会报错,因此在开发时可能会直接使用${}从而产生SQL注入漏洞:

1、like:

当mapper如下:

<select id="getByLike" resultType="com.r17a.commonvuln.injection.sqli.mybatis.pojo.User">
    SELECT * FROM user where name like '%${name}%'
</select>

调用时传入参数为wang%' and 1=2 union select Host,User,1,authentication_string from mysql.user where User like '%root时,绕过%'从而获取数据:

image.png

针对该场景可考虑以下形式修复:

<select id="getByLikePrepare" resultType="com.r17a.commonvuln.injection.sqli.mybatis.pojo.User">
    SELECT * FROM user where name like concat('%',#{name}, '%')
</select>
<select id="getByLikePrepare" resultType="com.r17a.commonvuln.injection.sqli.mybatis.pojo.User">
    SELECT * FROM user where name like "%"#{name}"%"
</select>

2、in:

当mapper如下:

<select id="getByIn" resultType="com.r17a.commonvuln.injection.sqli.mybatis.pojo.User">
    SELECT * FROM user where id in (${id})
</select>

调用时传入参数为0) and 1=2 union select Host,User,1,authentication_string from mysql.user where (1)=(1时,闭合)从而获取数据:

image.png

针对该场景可考虑以下形式修复,传入数组:

<select id="getByInFix" resultType="com.r17a.commonvuln.injection.sqli.mybatis.pojo.User">
    SELECT * FROM user where id in
    <foreach collection="array" item="id" index="index" open="(" close=")" separator=",">
        #{id}
    </foreach>
</select>

3、order by:

当mapper如下:

<select id="getByOrder" resultType="com.r17a.commonvuln.injection.sqli.mybatis.pojo.User">
    SELECT * FROM user order by ${coln} asc
</select>

调用时传入参数为IF((select user())='root@localhost',id,name)时:

image.png

针对该场景可考虑以下形式修复:

<select id="getByOrderFix" resultType="com.r17a.commonvuln.injection.sqli.mybatis.pojo.User">
    SELECT * FROM user order by
    <choose>
        <when test="coln == 'id' or coln == 'name' or coln == 'password'">
            ${coln}
        </when>
        <otherwise>
            name
        </otherwise>
    </choose>
    asc
</select>

0x03 Hibernate SQLi

Hibernate 属于全自动 ORM 映射工具,使用 Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取。Hibernate查询方式如下代码,其中的表名不能是元数据表,必须是实体类名,并且区分大小写,并且Hibernate不支持union联合查询。因此Hibernate的注入存在一定的局限性,不能像常规SQL注入一样利用。

Query<User> query = session.createQuery("from User where name = '" + name + "'", User.class);

上面代码采用拼接方式,当lisi' and user()='root@localhost,返回数据时证明user()='root@localhost',没有返回数据时证明user不是root,可以导致SQL注入:

image.png

当采用占位符预编译时:

Query<User> query = session.createQuery("from User where name = :name", User.class);
query.setParameter("name",name);
User user = query.getSingleResult();

image.png

Loader.prepareQueryStatement()会调用QueryLoader.bindParameterValues来处理预编译情况

image.png

最终QueryLoader.bindParameterValues同样会调用JDBC的ClientPreparedStatement.setString()完成预编译来防止SQL注入

image.png

CTF(Capture The Flag)是一种信息安全竞赛,通常包含各种类型的信息安全挑战,其中SQL注入SQL Injection)是一种常见的web安全攻击技术。在CTF的web安全挑战中,选手需要利用网站应用程序的漏洞执行SQL注入攻击,目的是从数据库中获取敏感信息。 SQL注入攻击的核心是利用用户输入与数据库交互时的不当处理,注入恶意的SQL语句。通过这种注入,攻击者可以控制数据库的查询过程,执行包括但不限于数据检索、修改、删除甚至数据库命令的执行等操作。 在CTF中,解决SQL注入问题通常包括以下几个步骤: 1. 发现注入点:选手需要通过各种手段(如测试特殊字符、逻辑判断、时间延迟等)来判断用户输入是否被直接插入到SQL查询中。 2. 构造注入payload:一旦找到注入点,选手需要构造特定的SQL语句来利用这个漏洞,这通常包括逻辑判断、联合查询、子查询等技术。 3. 数据提取:通过构造的SQL注入语句,选手可以提取数据库中的敏感信息,如用户数据、密码哈希值等。 4. 提取flag:在CTF竞赛中,通常存在一个包含“flag”的数据项,选手需要将获取的数据与flag格式对比,最终提交正确的flag格式以获得分数。 SQL注入攻击是非法的,在实际操作中,了解和学习SQL注入是为了更好地保护自己的网站免受此类攻击,而在CTF竞赛中,这种攻击则是为了学习和提高网络安全技能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值