freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

JAVA代码审计 — SQL注入
2021-12-18 16:09:02
所属地 广东省

前言

很长时间都没有写文章了,原因是因为在之前做漏洞复现的时候发现自己能力的不足,单纯使用他人的paylaod来进行攻击和脚本小子无异,所以才下定决心学习代码审计,学习代码审计是一个长久且枯燥的过程,希望和大家一起共勉。

环境

使用mysql,数据库名为test,含有1表名为users,users内数据如下

1639812628_61bd8e148f3c00185f846.png!small?1639812629609

1639813602_61bd91e22d99eba1a16ab.png!small?1639813603010

JDBC下的SQL注入

在JDBC下有两种方法执行SQL语句,分别是Statement和PrepareStatement,即其中,PrepareStatement为预编译

Statement

SQL语句

SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'

当传入数据为

username = admin
password = admin
SELECT * FROM users WHERE username = 'admin' AND password = 'admin';

即当存在username=admin和password=admin的数据时则返回此用户的数据

万能密码:admin' and 1=1#

1639813695_61bd923f694573027c18c.png!small?1639813696211

最终的sql语句变为了

SELECT * FROM users WHERE username = 'admin' and 1=1#

即返回用户名为admin,同时1=1的所有数据,1=1恒为真,所以始终返回所有数据

如果输入的时:admin' or 1=1#就会返回所有数据,因为admin' or 1=1恒为真

所以JDBC使用Statement是不安全的,需要程序员做好过滤,所以一般使用JDBC的程序员会更喜欢使用PrepareStatement做预编译,预编译不仅提高了程序执行的效率,还提高了安全性

PreParedStatement

与Statement的区别在于PrepareStatement会对SQL语句进行预编译,预编译的好处不仅在于在一定程度上防止了sql注入,还减少了sql语句的编译次数,提高了性能,其原理是先去编译sql语句,无论最后输入为何,预编译的语句只是作为字符串来执行,而SQL注入只对编译过程有破坏作用,执行阶段只是把输入串作为数据处理,不需要再对SQL语句进行解析,因此解决了注入问题

因为SQL语句编译阶段是进行词法分析、语法分析、语义分析等过程的,也就是说编译过程识别了关键字、执行逻辑之类的东西,编译结束了这条SQL语句能干什么就定了。而在编译之后加入注入的部分,就已经没办法改变执行逻辑了,这部分就只能是相当于输入字符串被处理

详情:数据库预编译为何能防止SQL注入? - 知乎 (zhihu.com)

而Statement方法在每次执行时都需要编译,会增大系统开销。理论上PrepareStatement的效率和安全性会比Statement要好,但并不意味着使用PrepareStatement就绝对安全,不会产生SQL注入。

PrepareStatement防御预编译的写法是使用?作为占位符然后将SQL语句进行预编译,由于?作为占位符已经告诉数据库整个SQL语句的结构,即?处传入的是参数,而不会是sql语句,所以即使攻击者传入sql语句也不会被数据库解析

String sql = "SELECT * FROM users WHERE username = ? AND password = ?";

//预编译sql语句
PreparedStatement pstt = connection.prepareStatement(sql);
pstt.setString(1,username);
pstt.setString(2, password);
ResultSet resultSet = pstt.executeQuery();//返回结果集,封装了全部的产部的查询结果

首先先规定好SQL语句的结构,然后在对占位符进行数据的插入,这样就会对sql语句进行防御,攻击者构造的paylaod会被解释成普通的字符串,我们可以通过过输出查看最终会变成什么sql语句

1639813779_61bd92939e0a322f80d30.png!small?1639813780526

可以发现还会对单引号进行转义,一般只能通过宽字节注入,下面将会在代码的层面展示为什么预编译能够防止SQL注入,同时解释为什么会多出一个转义符

不安全的预编译

拼接

总所周知,sql注入之所以能被攻击者利用,主要原因在于攻击者可以构造payload,虽然有的开发人员采用了预编译但是却由于缺乏安全思想或者是偷懒会直接采取拼接的方式构造SQL语句,此时进行预编译则无法阻止SQL注入的产生

代码:

//创建sql语句
String sql = "SELECT * FROM users WHERE username = '" + req.getParameter("username") + "' AND password = '" + req.getParameter("password") + "'";
System.out.println(sql);
//预编译sql语句
PreparedStatement pstt = connection.prepareStatement(sql);
ResultSet resultSet = pstt.executeQuery(sql);//返回结果集,封装了全部的产部的查询结果

这样即使使用了预编译,但是预编译的语句已经是被攻击者构造好的语句,所以无法防御SQL注入

1639813810_61bd92b29509ca9aa3c38.png!small?1639813811404

又或者是前面使用?占位符后,又对语句进行拼接,也会导致SQL注入

想要做到阻止sql注入,首先要做到使用?作为占位符,规定好sql语句的结构,然后在后面不破坏结构

使用in语句

String sql = "delete from users where id in("+delIds+");

此删除语句大多用在复选框内,在in当中使用拼接而不使用占位符做预编译的原因是因为很多时候无法确定deIds里含有多少个对象

输入:1,2

正常只会输出id为1和2的值

如果此时输入:1,2) or 1=1#

就会形成SQL注入,输出苦库里所有的值

1639813845_61bd92d5055ce9d764e95.png!small?1639813845847

正确写法:

还是要用到预编译,所以我们要对传入的对象进行处理,首先确定对象的个数,然后增加同量的占位符?以便预编译

public int gradeDelete(Connection con, String delIds) throws Exception{
    String num = "";
    //将对象分割开来,分割的点以实际而定
    String[] spl = delIds.split(".");

    //根据对象的个数添加同量的占位符?,用来预编译
    for(int i = 0; i< spl.length; i++){
        if(i == 0){
            num += "?";
        } else {
            num += ".?";
        }
    }
    String sql = "delete from users where id in("+num+")";
    prepareStatement pstmt = con.prepareStatement(sql);
    try {
        for(int j = 0; j < spl.length; j++){
            pstmt.setInt(j+1, integer.parseint(spl[j]));
        }
        return pstmt.executeUpdate();
    } catch(Exception e){
        //
    }

    return 0;
}

以bilibili的删除视频为例,当我取消收藏夹复数个视频的收藏时抓到的包为

1639813866_61bd92ea626b354064627.png!small?1639813867143

892223071%3A2%2C542789708%3A2%2C507228005%3A2%2C422244777%3A2%2C549672309%3A2%2C719381183%3A2%2C976919238%3A2%2C722053417%3A2
解码后为
892223071:2,542789708:2,507228005:2,422244777:2,549672309:2,719381183:2,976919238:2,722053417:2

可以发现其以:2,分割,那我们只需在split中填写

String[] spl = delIds.split(":2,");

即可,结果为:

1639813888_61bd93006982e401a8d40.png!small?1639813889370

然后再使用预编译

使用like语句

boolean jud = true;
String sql = "select * from users ";
System.out.println("请输入要查询的内容:");
String con = sc.nextLine();
for (int i = 0; i < con.length(); i++){
    if(!Character.isDigit(con.charAt(i))){
        jud = false;
        break;
    }
}
if(jud){
    sql += "where password like '%" + con + "%'";
}else{
    sql += "where username like '%" + con + "%'";
}

当用户输入的为字符串则查询用户名和密码含有输入内容的用户信息,当用户输入的为纯数字则单查询密码,用拼接地方式会造成SQL注入

正常执行:

1639813905_61bd9311442fbbd30cd2e.png!small?1639813906047

1639813910_61bd9316d22537755983a.png!small?1639813911587

SQL注入

1639813923_61bd9323320c4e67df44f.png!small?1639813924020

正确写法

首先我们要将拼接的地方全部改为?做占位符,但是使用占位符后要使用setString来把传入的参数替换占位符,所以我们要先进行判断,判断需要插入替换多少个占位符

boolean jud = true;
int v = 0;
String sql = "select * from users ";
System.out.println("请输入要查询的内容:");
String con = sc.nextLine();
for (int i = 0; i < con.length(); i++){
    if(!Character.isDigit(con.charAt(i))){
        jud = false;
        break;
    }
}
if(jud){
    sql += "where password like ?";
    v = 1;
}else{
    sql += "where username like ? and password like ?";
    v = 2;
}

//预编译sql语句
PreparedStatement pstt = connection.prepareStatement(sql);
if(v == 1){
    pstt.setString(1, "%"+con+"%");
}else if (v == 2){
    pstt.setString(1, "%"+con+"%");
    pstt.setString(2, "%"+con+"%");
}

1639813946_61bd933a3f9c5a37b124d.png!small?1639813947095

尝试进行SQL注入

1639813957_61bd93453e9d34cbde78a.png!small?1639813958038

发现被转义了

使用order by语句

通过上面对使用in关键字和like关键字发现,只需要对要传参的位置使用占位符进行预编译时似乎就可以完全防止SQL注入,然而事实并非如此,当使用order by语句时是无法使用预编译的,原因是order by子句后面需要加字段名或者字段位置,而字段名是不能带引号的,否则就会被认为是一个字符串而不是字段名,然而使用PreapareStatement将会强制给参数加上',我在下面会在代码层面分析为什么会这样处理参数

所以,在使用order by语句时就必须得使用拼接的Statement,所以就会造成SQL注入,所以还要在过滤上做好防御的准备

调试分析PrepareStatement防止SQL注入的原理

进入调试,深度查看PrepareStatement预编译是怎么防止sql注入的

用户名输入admin,密码输入admin',目的是查看预编译如何对一个合理的字符串以及一个不合理的字符串进行处理

1639813970_61bd93523a6ff74a1a0ae.png!small?1639813970983

由于我们输入的username和password分别是admin和admin',而其中admin'属于非法值,所以我们只在

pstt.setString(2, password);

打上断点,然后步入setString方法

1639813988_61bd9364ba8451b20e5e2.png!small?1639813989674

步过到2275行,这里有一个名为needsQuoted的布尔变量,默认为true

1639813982_61bd935ee489029bb52ca.png!small?1639813983708

然后进入if判断,其中有一个方法为isEscapeNeededForString

1639813996_61bd936c2761f55c548fa.png!small?1639813997019

步入后发现有一个布尔的needsHexEscape,默认为false,然后将字符串,也就是传入的参数admin'进行逐字解析,判断是否有非法字符,如果有则置needsHexEscape为true且break循环,然后返回needsHexEscape

由于我们传入的是admin',带有'单引号,所以在switch的过程中会捕捉到,置needsHexEscape = true后直接break掉循环,然后直接返回needsHexEscape

1639814008_61bd9378df04c2a4b1f65.png!small?1639814009644

向上返回到了setString方法,经过if判断后运行if体里面的代码,首先创建了一个StringBuilder,长度为传入参数即admin+2,然后分别在参数的开头和结尾添加\'

1639814013_61bd937de87976a09a1af.png!small?1639814014758

简单来说,此switch体的作用就是对正常的字符不做处理,直接向StringBuilder添加同样的字符,如果非法字符,则添加转移后的非法字符,由于不是直接的替换,而是以添加的方式,简单来说就是完全没有使用到用户传入的的参数,自然就做到了防护

1639814020_61bd9384838bd5eb9ab44.png!small?1639814021383

我们传入的为admin’,被switch捕捉到后'后会在StringBuilder添加\\和\',最终我们的admin'会变为\'admin\\\',也就是'admin',同样防止了SQL注入最重要的一环--闭合语句

然后根据要插入占位符的位置进行插入

1639814026_61bd938a4bfebf3dc4c25.png!small?1639814027172

Mybatis下的SQL注入

Mybatis的两种传参方式

首先要了解在Mybatis下有两种传参方式,分别是${}以及#{},其区别是,使用${}的方式传参,mybatis是将传入的参数直接拼接到SQL语句上,二使用#{}传参则是和JDBC一样转换为占位符来进行预编译

Mybatis中#{}和${}传参的区别及#和$的区别小结_java_脚本之家 (jb51.net)

在#{}下运行的结果:

select * from users where username = #{username} and password = #{password}

1639814047_61bd939fb83e4de3dad56.png!small?1639814048571

在${}下运行的结果:

select * from users where username = "${username}" and password = "${password}"

1639814062_61bd93ae82b0af7f8ff91.png!small?1639814063348

SQL注入

${}

PeopleMapper设置

<select id="getPeopleList1" resultType="com.g1ts.People">
    select * from users where username = #{username} and password = #{password}
</select>

正常运行:

username:admin
password:admin

1639814143_61bd93ff230bcb9c07ca2.png!small?1639814144425

sql注入:

username:admin" and 1=1#
password:sef

1639814157_61bd940d4e6366f6dc586.png!small?1639814158307

成功sql注入

#{}

Mapper设置

<select id="getPeopleList2" resultType="People">
    select * from users where username = #{username} and password = #{password}
</select>

正常运行

username:admin
password:admin

1639814184_61bd9428922ccceee58c9.png!small?1639814185523

尝试SQL注入

username:admin" and 1=1#
password:sef

1639814202_61bd943aeeed54b9c5d60.png!small?1639814203925

SQL注入失败

使用like语句

正确写法

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语句

正确写法

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}+'%'

使用order by语句

和JDBC同理,使用#{}方式传参会导致order by语句失效,所以使用order by语句的时候还是需要做好过滤

调试分析Mybatis防止SQL注入的原理

本人学艺不精,一直定位定位不到XMLScriptBuilder上,所以只好看一下别人写的mybatis解析过程,通过解析过程来定位方法位置

先说结论,首先Mybatis会先对mapper里面的SQL语句进行判断,判断内容为是以${}传参还是以#{}传参,如果以#{}传参则使用?作为占位符进行预编译,Mybatis只会对SQL语句的占位符做一定的处理,处理传入参数最后的步骤还是调用会JDBC的预编译

完整调用流程:

${}解析执行过程

首先在XMLScriptBuilder中的parseDynamicNode()

1639814240_61bd94601364d25a1c8da.png!small?1639814240896

在这里进行了一次判断,先说结论,这个isDynamic的判断其实就是判断mapper.xml中的sql语句是使用#{}预编译还是使用${}拼接,使用${}则进入DynamicSqlSource,否则进入RawSqlSource

进入parseDynamicTags方法,可以发现有两种情况会使isDynamic为true,而其中isDynamic()就是用来判定的1639814246_61bd9466df60ae0db54e6.png!small?1639814248129

进入isDynamic()

1639814251_61bd946bd3d4325604c08.png!small?1639814252640

可以看到运行了两个方法,分别是DynamicCheckerTokenParser()以及createParser(),主要出在createParser()上,查看方法体

1639814255_61bd946fdf8f0c65fd544.png!small?1639814256672

发现调用了GenericTokenParser方法,传入了openToken、closeToken以及handler三个参数,其中openToken的值为${、closeToken的值为},很明显就是对sql语句进行解析,判断是否为${}方式传参,进入到GenericTokenParser方法

1639814261_61bd947534ad4a99394d3.png!small?1639814263562

然而只是单纯的设置变量的值,一直向上返回到isDynamic(),进入下一条语句,parser.parse(this.text);

在调试使就可清楚看到传入的值了,${}和sql语句同时出现,猜测就是在这里进行了匹配

1639814266_61bd947a02d393aa5aa5d.png!small?1639814266881

进入到parse方法,此方法对sql语句进行解析,当遇到${}的字段则将此位置空(null),从返回的StringBuilder值可以看出

1639814273_61bd94818c3a18aebd39b.png!small?1639814275168

执行完后返回到isDynamic()方法下,在return值递归,其实就是返回isDynamic的值,然后向上返回到一直返回到了parseScriptNode()方法

1639814284_61bd948c6ea51aa024239.png!small?1639814285228

最终结果就会创建一个DynamicSqlSource对象

至此,对SQL语句的解析告一段落,直到运行到peopleMapper.getPeopleList1(people1),步入到invoke方法

1639814290_61bd949259bae8a1e6a31.png!small?1639814291179

前面的方法大致就是获取传入的参数和获取sql语句,步进到execute方法,此方法作用是判断SQL语句的类型

1639814296_61bd94980a5093b16841c.png!small?1639814297113

由于我们的SQL语句使select,所以会落在witch体的select内,步入case select的excuteForMany方法

1639814301_61bd949d0a10e74462bb3.png!small?1639814301958

继续步入selectList方法,后面这里我不知道准确流程是怎么样的,反正经过我一番调试最终到了query方法这里,然后步入getBoundSql方法

1639814306_61bd94a26a53b31769ef8.png!small?1639814307269

步入getBoundSql方法后可以看一下参数,发现sqlSource的类型正是前面设置的DynamicSqlSource

1639814310_61bd94a65f0c081926684.png!small?1639814311294

继续步入getBoundSql方法,然后步进到rootSqlNode.apply方法

1639814315_61bd94abd4890cf3c118c.png!small?1639814316729

这里有有个坑点啊,可能是因为我技术不够,由于这个apply方法有很多个实现,直接步进会跑到MixerSqlNode里面,但我查阅了资料发现实际上是在TextSqlNode里面

步入createParser方法,发现调用了GenericTokenParser,这在上面解析的过程也是一样的

1639814322_61bd94b28d46e4b2b4e6e.png!small?1639814323638

从parse方法中返回的StringBuider可以发现,已经成功将参数和SQL语句拼接在一起了

1639814327_61bd94b72d4eba9421a53.png!small?1639814328067

#{}解析执行过程

在前面分析${}的过程中就提到了在XMLScriptBuilder中的parseDynamicNode()方法,目的就是为了判断mapper.xml文件内的SQL语句究竟是用${}方式传参还是使用#{}方式传参,如果是#{}方式则最终会调用RawSqlSource方法

1639814331_61bd94bbbb23708a7b61b.png!small?1639814332587

步入RawSqlSource方法

1639814337_61bd94c108260cf778040.png!small?1639814338964

继续运行,步入到sqlSourceParser.parse方法

1639814342_61bd94c60c846e758e290.png!small?1639814343205

可以发现出现了解析${}时用到的函数

GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);

进入方法体后发现目的是设置openToken和closeToken的值分别为#{和}

1639814351_61bd94cfeb02dde02c6bd.png!small?1639814353446

真正对SQL语句进行了操作的是

String sql = parser.parse(originalSql);

步入parser.parse方法,运行到结尾后查看StringBuilder的值,发现函数把#{}用?替换了

1639814364_61bd94dc2fb3e2da82891.png!small?1639814364951

1639814367_61bd94dfae938b1c2d561.png!small?1639814368490

到此,解析过程结束,一直运行到peopleMapper.getPeopleList1(people1),步入到invoke方法,然后前面的流程大致和${}解析一致,进入mapperMethod.execute方法,然后会判断执行的sql语句类型,然后进入executeForMany方法,一直运行到selectList方法,最后进入query方法

1639814373_61bd94e5ac7567676ac6f.png!small?1639814374576

query方法会调用自身作为返回值

1639814378_61bd94ea3fa0b2eeee6fb.png!small?1639814379953

在此方法的返回值又会调用delegate.query方法,而这个方法就是我执行#{}的方法,进入后一直运行到

else {
    list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

后进入

1639814389_61bd94f5cdfc5c33dc2e8.png!small?1639814390763

进入queryFromDatabase方法后运行到

try {
    list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
}

进入doQuery方法,进入prepareStatement()方法

1639814401_61bd9501889e4a767733a.png!small?1639814402442

1639814406_61bd95065b21b62aa575a.png!small?1639814407350

其中

Connection connection = this.getConnection(statementLog);

是与数据库建立连接的对象

步入parameterize()方法

1639814414_61bd950ef03544c0c55a5.png!small?1639814415791

继续步入,到setParameters方法

1639814419_61bd9513f0f721e6c8930.png!small?1639814420963

setParameters方法的作用,是将SQL语句和传入的参数进行拼接

1639814424_61bd9518b7e95fe0fad3c.png!small?1639814426896

List<ParameterMapping> parameterMappings = this.boundSql.getParameterMappings();

中,获取了boundSql,即获取到了设置的sql语句

1639814437_61bd952571de9d2dc17a6.png!small?1639814438599

ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i);

获取到了SQL语句中所需要的参数,我的SQL语句为select * from users where username = #{username} and password = #{password},所以需要username和password两个参数

1639814454_61bd9536b53ffd61600c8.png!small?1639814455792

运行到

1639814458_61bd953a9b7799ac7710f.png!small?1639814459497

步入setParameter方法

1639814463_61bd953fe81c528064c80.png!small?1639814465492

在图示处打上断点,步入setNonNullParameter方法

1639814468_61bd95447ca2157320d9e.png!small?1639814469396

继续在图示处打上断点,步入setParameter方法

1639814474_61bd954a24f87341b3be4.png!small?1639814475087

继续在图示处打上断点,步入setNonNullParameter方法

1639814478_61bd954e550e2bd851c5b.png!small?1639814479324

虽然方法名是一样的,但是并不是同一个方法,步入setString方法

1639814485_61bd9555c5d6f4fb20d1e.png!small?1639814486689

这里用到了动态代理,最终还是调用回了jdbc的preperStatement,在图示处打上断点并步入

1639814491_61bd955b4177020862a76.png!small?1639814492495

发现这个setString和上文所讲的JDBC的预编译使用一个函数,后面的编译方式与JDBC相同

Hibernate

Hibernate执行语句的两种方法

Hibernate可以使用hql来执行SQL语句,也可以直接执行SQL语句,无论是哪种方式都有可能导致SQL注入

Hibernate下的SQL注入

HQL

hql语句:

String hql = "from People where username = '" + username + "' and password = '" + password + "'";

首先先观察一下正常登录和错误登陆下的的情况

正常登录:

Hibernate: 
    /* 
from
    People 
where
    username = 'admin' 
    and password = 'admin' */ select
        people0_.id as id1_0_,
        people0_.username as username2_0_,
        people0_.password as password3_0_ 
    from
        users people0_ 
    where
        people0_.username='admin' 
        and people0_.password='admin'
admin

错误登陆:

Hibernate: 
    /* 
from
    People 
where
    username = 'admin' 
    and password = 'adadawd' */ select
        people0_.id as id1_0_,
        people0_.username as username2_0_,
        people0_.password as password3_0_ 
    from
        users people0_ 
    where
        people0_.username='admin' 
        and people0_.password='adadawd'

可以发现之间的区别在于成功登录后最后面返回了用户名

尝试进行SQL注入:

输入:

请输入用户名:
admin' or '1'='1
请输入密码
qwer

返回:

Hibernate: 
    /* 
from
    People 
where
    username = 'admin' 
    or '1'='1' 
    and password = 'qwer' */ select
        people0_.id as id1_0_,
        people0_.username as username2_0_,
        people0_.password as password3_0_ 
    from
        users people0_ 
    where
        people0_.username='admin' 
        or '1'='1' 
        and people0_.password='qwer'
admin

可以发现,经过拼接后,SQL语句变为了

from People where username = 'admin'  or '1'='1'  and password = 'qw

说明了使用这种拼接的方式和jdbc以及mybatis是一样会产生sql注入的

正确写法:

正确使用以下几种HQL参数绑定的方式可以有效避免注入的产生

位置参数(Positional parameter)

String parameter = "g1ts";
Query<User> query = session.createQuery("from users name = ?1", User.class);
query.setParameter(1, parameter);

命名参数(named parameter)

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);

命名参数列表(named parameter list)

List<String> names = Arrays.asList("g1ts", "g2ts");
Query<User> query = session.createQuery("from users where name in (:names)", User.class);
query.setParameter("names", names);

类实例(JavaBean)

user1.setName("g1ts");
Query<User> query = session.createQuery("from users where name =:name", User.class);
query.setProperties(user1);

SQL

Hibernate支持使用原生SQL语句执行,所以其风险和JDBC是一致的,直接使用拼接的方法时会导致SQL注入

语句如下:

Query<People> query = session.createNativeQuery("select * from user where username = '" + username + "' and password = '" + password + "'");

正确写法

String parameter = "g1ts";
Query<User> query = session.createNativeQuery("select * from user where name = :name");
query.setParameter("name",parameter);

调试分析Hibernate预防SQL注入原理

Hibernate框架最终还是使用了JDBC中预编译防止SQL注入的方法

完整过程

查看一下hibernate预编译的过程

首先在

List<People> list = query.list();

打下断点,步入

1639814595_61bd95c37759144ee942b.png!small?1639814596588

步入list方法

1639814599_61bd95c7f1b1f61045768.png!small?1639814600808

1639814604_61bd95cccae528094eb2f.png!small?1639814605921

1639814612_61bd95d4d8571148914fd.png!small?1639814614223

1639814617_61bd95d9ede5d9086c9bd.png!small?1639814619462

继续步入list方法

1639814627_61bd95e3e90354a025d34.png!small?1639814628750

1639814632_61bd95e85448d0776eeaf.png!small?1639814633169

步入doList方法

1639814639_61bd95ef5a910c77a68c9.png!small?1639814640201

1639814644_61bd95f410a16cfed4c71.png!small?1639814645387

1639814649_61bd95f9454066bc36d93.png!small?1639814650197

1639814653_61bd95fd5c7fbe22a5994.png!small?1639814654311

1639814657_61bd9601e5f577ab91409.png!small?1639814659879

1639814662_61bd960629682563af64a.png!small?1639814663416

步入bind方法

1639814671_61bd960f1feb81cbcd235.png!small?1639814671981

步入nullSafeSet方法

1639814674_61bd961293e812c268178.png!small?1639814675400

1639814678_61bd9616b0331644ed8fe.png!small?1639814679585

步入getBinder方法

1639814684_61bd961c6e42654eaf338.png!small?1639814685564

最后调用的st.setString就是jdbc的setString方法。

# SQL注入 # web安全 # 漏洞分析 # 代码审计 # Java代码审计
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录