Java代码审计入门-sql注入漏洞剖析

0x00 写在前面

对于审计小白来说,在各种漏洞形式里,更吸引我的还是sql注入和RCE,特别是后者,也往往需要去做白盒审计才能发现漏洞;今天我就先跟着审一个sql吧!

案例是panda师傅的一个审计项目和ofcms v1.1.2源码。

0x01 漏洞原理

SQL注⼊,懂得都懂,通过将 SQL 命令插⼊应⽤程序的sql语句即 http 请求中,并在服务器端被接收后⽤于参与数据库操作,最终达到欺骗服务器执⾏恶意的 SQL 命令的效果,JavaSQL 注⼊和 PHP 中的 SQL 注⼊,其实差别不⼤,理论上来讲,应⽤程序中只要是与数据库存在数据交互,⽆论是增删改查,只要传⼊的数据完全受⽤户控制,且应⽤程序对⽤户传⼊的数据没有进⾏妥当的处理,那么这些地⽅都是可能存在 SQL 注⼊的。

例如在panda师傅的项目案例的UserInfoDaoImpl.java 中,存在如下部分代码:

public UserInfo UserInfoFoundDao(String id){
            Connection conn = null;
            PreparedStatement ps = null;
            ResultSet rs = null;
            UserInfo userinfo = null;
            try{
                Class.forName("com.mysql.cj.jdbc.Driver");
                conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/sec_sql","root","admin888");
                String sql = "select * from userinfo where id = " + id;
                ps = conn.prepareStatement(sql);
                //传值
                //ps.setInt(1,id);
                rs = ps.executeQuery();
                while(rs.next()){
                    userinfo = new UserInfo();
                    userinfo.setId(rs.getString("id"));
                    userinfo.setName(rs.getString("name"));
                    userinfo.setAge(rs.getInt("age"));
                    userinfo.setContent(rs.getString("content"));
                    userinfo.setAddress(rs.getString("address"));
                }
            } catch (ClassNotFoundException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } 
                .......
        }

一眼丁真,这种传统的jdbc代码,在sql语句中,存在着拼接的String类型变量id,没有做任何的过滤以及预编译处理;也就可以直接随便注了。

而对于没有用任何中间件的原生的java代码来说,使用自带的预编译就可以杜绝绝大多数的sql注入,如下:

String sql = "select * from userinfo where id = ?";
ps = conn.prepareStatement(sql); 
ps.setInt(1,id);

预编译在此处确实解决了sql注入问题;

不过如下图当使用order by关键字时,order by后面的语句,是不能进行预编译的,只能通过拼接来进行操作了,所以就得手动过滤。

String sql = "select * from userinfo where id = ?"+"order by ''" + age + "' asc'" ;
ps = conn.prepareStatement(sql);
ps.setInt(1,id);

这些都只是基于原生的servlet来解析sql注入,在实际生产环境中,中间件框架的使用占了绝大部分,但实际上原理还是类似的,只是表面形式不同,如Mybatis中的likeinorder by语句、Hibernate中的createQuery()函数等,如果使用不当,仍然可能造成sql注入。

0x02 案例刨析-(CVE-2019-9615)

环境tomcat8+mysql8+java8+maven3.6

ofcms是国内一小型团队开发的一款基于java研发的内容管理系统。

gitee上下载v1.1.2版本的ofcms后导入IDEA,运行项目。项目部署这里就只简略截几个图了:

首先导入项目并配置好tomcat后,把pom.xml里的mysql版本改成本机对应的,再刷新加载maven进行下载jar包。

image-20220805013208791

ofcms-admin/src/main/resources/dev/conf/文件夹的db.properties下更改自己的mysql账号密码;

image-20220805010924490

然后在数据库里创建一个ofcms,将sql文件导入即可。

image-20220805015632869

image-20220805015747725

启动项目

image-20220805020156474

image-20220805020456038

图片有点bug,不过起来了。

image-20220805020608085

进入后台:admin/123456

根据漏洞描述,漏洞点为ofcms-admin/src/main/java/com/ofsoft/cms/admin/controller/system/SystemGenerateController.javacreate函数

public void create() {
        try {
            String sql = getPara("sql");
            Db.update(sql);
            rendSuccessJson();
        } catch (Exception e) {
            e.printStackTrace();
            rendFailedJson(ErrorCode.get("9999"), e.getMessage());
        }
    }

image-20220805021748074

函数一进来,通过getPara函数获取到sql参数的值(getPara函数是Controller类里的函数,根据上图一眼丁真,就是调用的ServletRequest.getParameter()sql参数也没做其他过滤。

然后紧接着就是update(),一层一层往上找找看,最后定位到了DbPro.classint update(Config config, Connection conn, String sql, Object... paras)

image-20220806205636598

先是获取数据库连接,然后创建一个PreparedStatement对象,来预编译sql,我们正常使用PreparedStatement,应该如下,会用上占位符去

PreparedStatement pst = con.prepareStatement("update users set name = ? where id = ?");
pst.setString(1, "ikun"); 
pst.setInt(2, 38);

作者在此处虽然创建了PreparedStatement对象来进行预编译,但在SystemGenerateController.create()里并没有用上占位符,这就等于没有对传进来的sql语句进行预编译处理,白用了,这也就是漏洞的根本原因,不复杂。

根据代码里的提示,找到了后台对应的功能点:系统设置->代码生成->增加

image-20220805030523750

image-20220805034227619

正常语句执行,可以看到控制台显示完整的执行的update

image-20220805034344835

则可以直接进行报错注入了。

update of_cms_bbs set title = updatexml(0,concat(0x7e,(user())),0) where bbs_id = 4

image-20220805034614148

主要来讲,一般审计项目可以先用工具来扫,扫完看重要函数在哪,然后一个个慢慢去跟,对于这里的ofcms就是找到代码生成功能点的update函数没有去用到占位符,也没有去做其他过滤,从而导致直接执行恶意的函数。

0x03 总结

本⽂简单讨论了Java 中的 SQL 注⼊漏洞,包括其原理、简单的 java代码示例以及 CVE 实例,全篇下来也没啥重要姿势;希望对像我样初⼊java 代码审计的朋友有所帮助。