浅析java代码审计中的sql注入漏洞

admin 2022年7月11日21:07:03评论26 views字数 6256阅读20分51秒阅读模式

写在前面

市面上关于java代码审计的课程和教程比较少,所以我们打算开设一个新的专栏,在这个专栏里面免费和大家分享一些从0到1的关于java代码审计相关的知识

随着国内网络空间安全技术的发展,最初的一些只会使用黑客工具的“脚本小子”逐渐被能够进行代码分析、编写脚本的专业安全人员所代替。可以说,一个优秀的安全人员,必须要懂编程。但是懂编程仅仅是第一步,懂编程的人不胜其数,但将编程能力演变成代码审计能力,并且利用代码审计能力去挖掘漏洞的人却少之又少

浅析java代码审计中的sql注入漏洞

1.注入漏洞简介

注入漏洞,是指攻击者可以通过HTTP请求将payload注入某种代码中,导致payload被当作代码执行的漏洞。例如SQL注入漏洞,攻击者将SQL注入payload插入SQL语句中,并且被SQL引擎解析成SQL代码,影响原SQL语句的逻辑,形成注入。同样,文件包含漏洞、命令执行漏洞、代码执行漏洞的原理也类似,也可以看作代码注入漏洞。


2.SQL注入漏洞简介

SQL注入(SQL Injection)是因为程序未能正确对用户的输入进行检查,将用户的输入以拼接的方式带入SQL语句中,导致了SQL注入的产生。黑客通过SQL注入可直接窃取数据库信息,造成信息泄露。


3.JDBC拼接不当造成SQL注入

JDBC有两种方法执行SQL语句,分别为PrepareStatementStatement。两个方法的区别在于PrepareStatement会对SQL语句进行预编译,而Statement方法在每次执行时都需要编译,会增大系统开销。理论上PrepareStatement的效率和安全性会比Statement要好,但并不意味着使用PrepareStatement就绝对安全,不会产生SQL注入。

第一关:使用Statement

小渣是一名菜鸡程序员,这天他收到了一封匿名黑客邮件,黑客声称已经获取了小渣数据库中所有用户的详细信息,并要求小渣对此负责,于是小渣委托同事大牛上机排查,锁定了下面这几行代码:

String sql = "select * from user where id = " + req.getParameter("id");
out.println(sql);
try {
    Statement st = con.createStatement();
    ResultSet rs = st.executeQuery(sql);
    while (rs.next()) {
        out.println("<br> id: " + rs.getObject("id"));
        out.println("<br> name: " + rs.getObject("name"));
    }
catch (SQLException throwables) {
    throwables.printStackTrace();
}

大牛🐂:该代码使用拼接的方式将用户输入的参数“id”带入SQL语句中,存在明显的sql注入,例如:经过拼接构造后,最终在数据库执行的语句可能为“select * from user where id = 1 or 1=2”,改变了程序想要查询“id=1”的语义

大牛🐂:还有,真正的程序员从来不用select *

小渣:咦?为什么呢?

大牛🐂:自己去悟!

第二关:使用PrepareStatement

程序员小渣知道了Statement方法的危害,于是他决定在之后的Coding中都采用PrepareStatement方法

小渣:熬了一通宵,项目中所有的Statement都改成PrepareStatement啦!😊

String sql = "select * from user where id = " + req.getParameter("id");
out.println(sql);
try {
    PreparedStatement pstt = con.prepareStatement(sql);
    ResultSet rs = pstt.executeQuery();
    while (rs.next()) {
        out.println("<br> id: " + rs.getObject("id"));
        out.println("<br> name: " + rs.getObject("name"));
    }
catch (SQLException throwables) {
    throwables.printStackTrace();
}

大牛🐂:。。。你这个还是采取拼接的方式构造SQL语句,此时进行预编译则无法阻止SQL注入的产生,没什么卵用

小渣:啊。。。那我要怎么写才能保证不出现sql注入呢?😟

大牛🐂:可以考虑这样写,PrepareStatement方法支持使用‘?’对变量位进行占位,在预编译阶段填入相应的值构造出完整的SQL语句,此时可以避免SQL注入的产生

PrintWriter out = resp.getWriter();
String sql = "select * from user where id = ?";
out.println(sql);
try {
    PreparedStatement pstt = con.prepareStatement(sql);
    pstt.setInt(1, Integer.parseInt(req.getParameter("id")));
    ResultSet rs = pstt.executeQuery();
    while (rs.next()){
        out.println("<br> id: "+rs.getObject("id"));
        out.println("<br> name: "+rs.getObject("name"));
    }
catch (SQLException throwables) {
    throwables.printStackTrace();
}

小渣:哎。。又要熬夜了┭┮﹏┭┮


4.框架使用不当造成SQL注入

在实际的代码开发工作中,JDBC方式是将SQL语句写在代码块中,不利于后续维护。如今的Java项目或多或少会使用对JDBC进行更抽象封装的持久化框架,如MyBatis和Hibernate。通常,框架底层已经实现了对SQL注入的防御,但在研发人员未能恰当使用框架的情况下,仍然可能存在SQL注入的风险。

第三关:使用MyBatis框架

小渣最近学了MyBatis框架,于是赶紧跑来和大牛炫耀

小渣:我用了MyBatis,将SQL语句编入配置文件中,避免SQL语句在Java程序中大量出现,方便后续对SQL语句的修改与配置。正确使用MyBatis框架可以有效地阻止SQL注入的产生O(∩_∩)O

大牛🐂:那你是怎么写的呢?(略带嘲讽的语气)

小渣:MyBatis中使用parameterType向SQL语句传参,在SQL引用传参可以使用#{Parameter}${Parameter}两种方式,我个人比较喜欢${Parameter}这种方式,因为它前面有美元🐱‍🐉

<select id="getUsername" resultType="com.z1ng.bean.User">
   select id,name,age from user where name = #{name}
</select>

大牛🐂:你测试过你的代码了吗。。。

小渣:当然,不做单元测试的程序员不是好程序员!当输入的“name”值为“genius”时,成功查询到结果,Debug的回显如下

Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc. onnectionImpl@6d3af739]
==>  Preparing: select id,name,age from user where name = 'genius'
==> Parameters: 
<==    Columns: id, name, age
<==        Row: 1, z1ng, 18
<==      Total: 1
User{id=1, name='genius', age=18}

大牛🐂:你再传一个'aaaa' or 1=1试试

小渣:貌似还是把用户爆出来了。。😳

Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc. onnectionImpl@6d3af739]
==>  Preparing: select id,name,age from user where name = 'aaaa' or 1=1
==> Parameters: 
<==    Columns: id, name, age
<==        Row: 1, genius, 18
<==      Total: 1
User{id=1, name='genius', age=18}

大牛🐂:在底层构造完整SQL语句时,MyBatis的两种传参方式所采取的方式不同。#{Parameter}采用预编译的方式构造SQL,避免了SQL注入的产生。而${Parameter}采用拼接的方式构造SQL,在对用户输入过滤不严格的前提下,此处很可能存在SQL注入

小渣:那我还是乖乖用#吧。。

第四关:Hibernate框架

小渣最近的项目组使用到了Hibernate框架,于是赶紧跑来问问大牛

大牛🐂:Hibernate框架是Java持久化API(JPA)规范的一种实现方式。Hibernate 将Java 类映射到数据库表中,从 Java 数据类型映射到 SQL 数据类型。Hibernate是目前主流的Java数据库持久化框架,采用Hibernate查询语言(HQL)注入

Hibernate是对持久化类的对象进行操作而不是直接对数据库进行操作,因此HQL查询语句由Hibernate引擎进行解析,这意味着产生的错误信息可能来自数据库,也可能来自Hibernate引擎

小渣:懂了,我这就回去写代码(信心满满)

两天后。。。

小渣:哎。写的代码又有sql注入了/(ㄒoㄒ)/~~

factory = new Configuration().configure().buildSessionFactory();
Session session = factory.openSession();
Transaction tx = null;
try{
    tx = session.beginTransaction();
    String parameter = " zaaaa' or '1'='1 ";
    List user = session.createQuery("FROM User where name='"+ parameter+"'",User.class).getResultList();
    for (Iterator iterator =
         user.iterator(); iterator.hasNext();){
        User user1 = (User) iterator.next();
        System.out.println(user1.toString());
    }
    tx.commit();
}catch (HibernateException e) {
    if (tx!=null) tx.rollback();
    e.printStackTrace();
}finally {
    session.close();
}

大牛🐂:别灰心,我来教你几招

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

位置参数:

String parameter = "z1ng";
Query<User> query = session.createQuery("from com.z1ng.bean.Userwhere name = ?1", User.class);
query.setParameter(1, parameter);

命名参数:

Query<User> query = session.createQuery("from com.z1ng.bean.Userwhere name = ?1", User.class);String parameter = "z1ng";
Query<User> query = session.createQuery("from com.z1ng.bean.Userwhere name = :name", User.class);
query.setParameter("name", parameter);

命名参数列表:

List<String> names = Arrays.asList("z1ng""z2ng");
Query<User> query = session.createQuery("from com.z1ng.bean.User where name in (:names)", User.class);
query.setParameter("names", names);

类实例:

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

Hibernate还支持原生的SQL语句执行,与JDBC的SQL注入相同,直接拼接构造SQL语句会导致安全隐患的产生,应采用参数绑定的方式构造SQL语句

拼接构造如下:

Query<User> query = session.createNativeQuery("select * from user where name = '"+parameter+"'");

参数绑定如下:

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

小渣:不愧是大牛👍


结语

在实际开发的过程中,有些地方难免需要使用拼接构造SQL语句,例如SQL语句中order by后面的参数无法使用预编译赋值。此时应严格检验用户输入的参数类型、参数格式等是否符合程序预期要求。

黑夜给了我黑色的眼睛,我却用它寻找漏洞👀



昆仑云安全实验室系列文章仅限技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担!

昆仑云安全实验室拥有对此文章的修改、删除和解释权限,如转载或传播此文章,需保证文章的完整性,未经允许,禁止转载!

原文始发于微信公众号(昆仑云安全):浅析java代码审计中的sql注入漏洞

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年7月11日21:07:03
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   浅析java代码审计中的sql注入漏洞http://cn-sec.com/archives/1171066.html

发表评论

匿名网友 填写信息