项目代码:https://github.com/JPressProjects/jpress?tab=readme-ov-file
参考链接:https://drun1baby.top/2022/11/06/jpress-V4-2-%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1
JPress 是一个基于Java开发的类似WordPress的CMS系统,并在基础上增加了电商功能。它采用多模块架构,支持插件化扩展,具有完整的用户管理、内容管理、电商管理等功能。
如果看这个项目的pom.xml,没有filter,主要是有很多个module
模版渲染引起的RCE
在steeing目录下有多个模版渲染漏洞,比如
1 | /admin/article/setting |
分析
在”文章”->”设置”中“评论邮件通知管理员”的地方,看到可以写一个模版,官方给出的例子有 #(comment.id),所以可以推测可能存在模版渲染的问题

找到和文章评论相关的类,io.jpress.module.article.controller.front.ArticleController找到postComment()方法,下断点
先在设置开启“评论邮件通知管理员”功能,然后填写模版内容,在文章下面留下评论

开始跟进

下面是一些简单的判断,重点来到268行的,跟进到notify()方法,看里面是怎么通知的
1 | ArticleNotifyKit.notify(article, comment, conmmentUser); |
分为email和sms两种方法

看到email里面,最后会走到doSendEmail()方法,重点在第90和91行的renderToString()方法,分别对模版标题和内容做渲染

跟到后面有一个exec()方法,一直跟,会有一个地方去遍历处理内容

注意:因为前面在标题模版我填入了#set(str=comment.content),是set类型,走到这里才会变成statArray[0]才是set类型,下一步的才能进到com.jfinal.template.stat.ast中Set里面的exec()方法
走到assignVariable()里面,要控制Object rightValue = right.eval(scope);里面要right的属性是method,这里用的内容是#(x.setAutoTypeSupport(true))

最后走到 com.jfinal.template.expr.ast.Method 这个类的 eval() 方法下,里面有构造反射的条件

调用栈
1 | eval:83, Method (com.jfinal.template.expr.ast) |
Velocity作为模版引擎是可以用来写脚本的,可以运行一些简单的输出、赋值等操作,在 Velocity 中
"#"用来标识 Velocity 的脚本语句,包括#set、#if 、#else、#end、#foreach、#end、#iinclude、#parse、#macro等;
所以在这里可以写到代码,就可以构造POC执行任意命令
1 | #set(x=net.sf.ehcache.util.ClassLoaderUtil::createNewInstance("javax.script.ScriptEngineManager")) |
按理说这时候去前台评论任意内容,都会触发模版渲染,从而执行恶意代码,但是这里遇到了报错,

在第三行存在不合法符号,说是单引号,所以要换一种打法
漏洞利用
写入模版,不带有单引号,结合fastjson机制
1 | #set(str=comment.content) |
启用了 fastjson 的 AutoType 支持,手动解析 JSON 字符串并实例化指定类,通过 ScriptEngineManager 加载脚本引擎,获取到前台comment.content的内容,然后经过处理,最后可以执行
然后我们在前台评论
1 | javax.script.ScriptEngineManager|{"@type":"javax.script.ScriptEngineManager"}|js|java.lang.Runtime.getRuntime().exec("open -a calculator") |
把代码传过去处理,然后就能成功弹出计算机
修复说是用的是编码转义,重新跑最新版本不知道为什么没跑起来,就没有复现了
小结
我比较多思考的是漏洞点的发现,到最后找到Method里面的exec有反射,如果一套代码给到我,我应该从哪里下手找到这个调用链,或者说在看到#comment.id的时候想到模版注入,但是也要花时间找到后面的链条,找到有一个刚好可以反射执行代码的地方,
前台任意文件上传
位置确认
存在漏洞的路由/ucenter/avatar,这里找到对应上传函数的思路是先找到avatar.html,看到

上传处理逻辑是/commons/attachment/upload,直接在idea全局搜@RequestMapping("/commons/attachment")

最后确定是在io/jpress/web/commons/controller/AttachmentController.java里面的upload()方法
打个断点前台尝试上传,可以确认位置没找错

分析
漏洞产生的原因是web应用允许上传一个file[]数组,而不是单一文件,从而利用这个特性构造多个文件同时上传绕过
upload()方法里面两句
1 | UploadFile uploadFile = getFile(); |
1 | public UploadFile getFile() { |
这里先测试同时上传两个文件,看是什么情况

在第一关getFile()方法就已经获取上传到文件

执行完getFile()会看到文件已经被上传到attachment目录,这里是暂存的逻辑

查了一下原理
getFile() 本身不会“手写文件”,而是触发了 JFinal 的 multipart 解析器去处理请求体;解析器在解析时就把上传的文件流直接落地到磁盘临时/上传目录,然后返回一个已经指向磁盘文件的 UploadFile。
过到后面拿出file[]数组第0个元素,也就是第一个文件出来进行后续的判断,后面的文件不管,所以有得绕过空间

通过了一系列判断,到后面moveFile()才会把第一个文件移动到要保存的地方,但是之前上传多的文件还保留着暂存区,也是在服务器本地了
修复
看到最新版本的代码

在getFile()里面加上了getFirstFileOnly()限制,如果只有一个文件,返回即可,如果有多个文件,拿出第一个,其他的都直接delete
安装插件出命令执行漏洞
影响接口 admin/addon/install
分析
这里允许我们上传jar包,我尝试按上面的方法找到主要作用函数
就io/jpress/web/admin/_AddonController.java的doUploadAndInstall()方法在处理上传逻辑

先直接下断点,上传写好的恶意jar包,jpress项目代码有提供写好的插件代码,我直接在里面改写了一段,然后再打包出来

到后台上传调试,看具体逻辑,会有一些判断和加载

重点来到118行,有一个install()方法,跟进

有一个readAddonInfo()方法加载jar文件,跟进

后面有一个classLoader.load(),先获得jar文件详情,然后后面加载进去
在load方法里面,按类别加载各个部分

其中就有一个把我们的controller加载进去

接下来处理请求都会利用到controller,从而实现攻击
测试,成功执行命令,这里插件没有写太好,没有回显,但是效果也是有了

这里看官方说没有修复,因为比较苛刻,需要管理员后台才有插件安装权限,也能理解,不过以后实际遇到这个框架就可以试试这个漏洞利用了,万一呢。
小结
过了这几个比较感兴趣的漏洞,体会就是基础很重要,我还是在思考如果给我一套完整的源码,我要怎么下手去审计,期待我在后面的学习中找到答案