对Java CMS的审计的一次尝试
2022-10-14 21:6:3 Author: xz.aliyun.com(查看原文) 阅读量:11 收藏

环境搭建

从gitee下载源码

传送门

  1. 根据开发文档,我们首先将项目导入idea开发工具中

  2. 之后在Mysql中新建ofcms数据库

  3. 在ofcms-admin模块中的src/main/resources/conf/dev/conf/db-config.properties文件中配置JDBC连接配置

之后将其重命名为db.properties

  1. 初始化数据库,有两种方式,一种是手工的,一种是自动的,我这里选择的是手工的方式
    doc/sql文件夹的sql文件导入到mysql中执行

  2. 最开开启Tomcat容器,启动项目

管理后台

默认密码为admin/123456

分析

首先我们可以看看这个CMS用的组件,是不是有一些组件可以直接造成漏洞

我们可以发现有着freemarker,那么是否有着模板注入的风险呢,答案当然是有的

我们同时可以大概看一下项目

SSTI

上面提到了使用了freemarker组件,我们可以后台中找到一处可以修改模板的位置

这里就存在一个模板注入的漏洞

对于freemarker的攻击方式

freemarker可利用的点在于模版语法本身,直接渲染用户输入payload会被转码而失效,所以一般的利用场景为上传或者修改模版文件,正好这里是一处可以加载模板的功能点

我们首先在这个位置编辑修改了模板文件之后,在调用这个文件的时候将会对模板进行渲染,后端调用了com.ofsoft.cms.admin.controller.cms.TemplateController#save方法来获取模板,打下断点分析一下

简单看一下这之前的调用栈

save:108, TemplateController (com.ofsoft.cms.admin.controller.cms)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invoke:73, Invocation (com.jfinal.aop)
intercept:41, IocInterceptor (com.ofsoft.cms.core.spring)
invoke:67, Invocation (com.jfinal.aop)
intercept:21, ApiInterceptor (com.jfinal.weixin.sdk.jfinal)
invoke:67, Invocation (com.jfinal.aop)
intercept:44, SessionInViewInterceptor (com.jfinal.ext.interceptor)
invoke:67, Invocation (com.jfinal.aop)
intercept:60, ShiroInterceptor (com.ofsoft.cms.core.plugin.shiro)
invoke:67, Invocation (com.jfinal.aop)
handle:82, ActionHandler (com.jfinal.core)
handle:14, WebSocketHandler (com.ofsoft.cms.core.handler)
handle:75, DruidStatViewHandler (com.jfinal.plugin.druid)
handle:48, ActionHandler (com.ofsoft.cms.core.handler)
doFilter:73, JFinalFilter (com.jfinal.core)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:61, ProxiedFilterChain (org.apache.shiro.web.servlet)
executeChain:108, AdviceFilter (org.apache.shiro.web.servlet)
doFilterInternal:137, AdviceFilter (org.apache.shiro.web.servlet)
doFilter:125, OncePerRequestFilter (org.apache.shiro.web.servlet)
doFilter:66, ProxiedFilterChain (org.apache.shiro.web.servlet)
executeChain:449, AbstractShiroFilter (org.apache.shiro.web.servlet)
call:365, AbstractShiroFilter$1 (org.apache.shiro.web.servlet)
doCall:90, SubjectCallable (org.apache.shiro.subject.support)
call:83, SubjectCallable (org.apache.shiro.subject.support)
execute:383, DelegatingSubject (org.apache.shiro.subject.support)
doFilterInternal:362, AbstractShiroFilter (org.apache.shiro.web.servlet)
doFilter:125, OncePerRequestFilter (org.apache.shiro.web.servlet)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
invoke:196, StandardWrapperValve (org.apache.catalina.core)
invoke:97, StandardContextValve (org.apache.catalina.core)
invoke:542, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:135, StandardHostValve (org.apache.catalina.core)
invoke:81, ErrorReportValve (org.apache.catalina.valves)
invoke:698, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:78, StandardEngineValve (org.apache.catalina.core)
service:364, CoyoteAdapter (org.apache.catalina.connector)
service:624, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:831, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1673, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

首先是tomcat容器进行处理,来到了doFilter中进行过滤器的调用之后因为是访问的/admin路径下的资源,使用了shiro组件进行鉴权操作,在web.xml中也有所体现

在shiro鉴权完成之后将会将其还给tomcat进行下一个Filter链的调用,最后来到了save方法的调用

在这里首先会取出Request请求中的res_path参数值,并和res进行比较,如果成就就会获取resource下的模板文件目录,如果不成功就会获取webapp下的模板文件目录

之后会获取到模板文件的相对路径dirs,在前端UI中就是下图位置

之后将其和模板文件目录进行了拼接,紧接着获取了保存修改的模板文件名,有紧接着获取了需要保存的html文件的内容

之后将获取的时候进行的编码给复原为<>等符号,然后在获取了目标文件之后,调用FileUtils#writeString方法进行文件的写入

但是在这里我有点不太明白,他明明指定了对应html文件的输出流,为什么后端的html代码没有改变,希望有明白的大师傅留个言呗!

好了,现在没有过滤的写入了恶意代码在里面

最后调用了rendSuccessJson返回了成功的提醒

,之后就是解析写入的恶意模板

我们可以直接访问404.html文件

首先看一下调用栈

exec:347, Runtime (java.lang)
exec:80, Execute (freemarker.template.utility)
_eval:62, MethodCall (freemarker.core)
eval:76, Expression (freemarker.core)
evalAndCoerceToString:80, Expression (freemarker.core)
accept:40, DollarVariable (freemarker.core)
visit:257, Environment (freemarker.core)
accept:57, MixedContent (freemarker.core)
visit:257, Environment (freemarker.core)
process:235, Environment (freemarker.core)
process:262, Template (freemarker.template)
render:158, FreeMarkerRender (com.jfinal.render)
handle:99, ActionHandler (com.jfinal.core)
handle:14, WebSocketHandler (com.ofsoft.cms.core.handler)
handle:75, DruidStatViewHandler (com.jfinal.plugin.druid)
handle:48, ActionHandler (com.ofsoft.cms.core.handler)
doFilter:73, JFinalFilter (com.jfinal.core)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
invoke:196, StandardWrapperValve (org.apache.catalina.core)
invoke:97, StandardContextValve (org.apache.catalina.core)
invoke:542, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:135, StandardHostValve (org.apache.catalina.core)
invoke:81, ErrorReportValve (org.apache.catalina.valves)
invoke:698, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:78, StandardEngineValve (org.apache.catalina.core)
service:364, CoyoteAdapter (org.apache.catalina.connector)
service:624, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:831, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1673, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

这里因为直接访问的是/虚拟目录下的资源,所以不会被shiro进行鉴权

首先会触发对应的controller的调用,即IndexController#index方法

也不难理解,就是判断是否是首页,如果是将会渲染index.html模板,当然,如果不是会继续往下走,如果请求的是具体的内容,将会渲染article.html但是我们这里啥也不是,将会渲染FrontConst.pageError即是404.html

接着来到了FreeMarkerRender#render方法进行渲染

前面都是一些内容的处理,来看try语句,在得到404.html的模板之后创建了一PrintWriter对象,通过调用process进行处理

创建了一个Environment对象,并调用了process方法

在获取了Template对象之后得到了他的html的具体内容,通过调用Environment#visit进行访问模板

调用MixedContent#accept方法接收模板内容

在这个方法中将会遍历nestedElements中的元素,一个一个进行处理

那么我们看看这里面有什么?

很明显,Freemarker将模板中分成了好几块,一块一块的进行了处理,我们可以知道在第3次处理中创建了一个命令执行的类,在第4次处理的时候就会执行我们的恶意代码calc

处理模板语句${ex("calc")}

来到了DollarVariable#accept方法

创建了一个输出流将命令执行的结果返回,首先会执行的是Expression#evalAndCoerceToString方法的调用

执行了eval方法,最后调用了Execute#exec进行了命令执行

Defence

对于这中漏洞的防御,我们可以添加完善的WAF进行拦截

或者是使用TemplateClassResolver

文档:

https://freemarker.apache.org/docs/api/freemarker/core/TemplateClassResolver.html

但是同样的,如果有着一些不当的配置仍然能够Bypass,比如说?api的开启

任意文件上传

这里不存在有文件上传的点,只是在编辑修改模板文件的时候,能够指定文件绝对路径和文件名,且没有过滤文件后缀

我们在上面SSTI分析中,对模板的修改保存流程的跟踪中我们知道调用了TemplateController#save方法进行保存修改的模板文件内容

在该方法中获取了dirs参数,将会和前面获取到了的pathFile进行拼接,之后会获取file_name参数获取到文件名和文件内容,最后将文件内容写入文件中

值得注意的是,这里并没有存在任何的过滤,不管是路径穿越,又或者是文件名,或者是危险函数,能够进行文件上传

最后也能够在Tomcat容器中发现写入了shell.jsp

Defence

  1. 禁止目录穿越
  2. 检测文件后缀名
  3. 查杀shell

XSS

同样在UI界面我们可以寻找到一个评论框,尝试XSS,成功执行

我们可以定位后端代码

CommentApi#save方法中打下断点

首先会调用getParamsMap方法获取评论的内容 / id等等内容, 之后会添加评论的ip地址,之后执行Db.update方法

在执行update方法之前会生成一个sql语句,我们跟进一下细节

调用MAIN.getSqlPara方法

之后再SqlKit#getSqlTemplate方法中通过key值获取到了对应的sql模板

回到MAIN.getSqlPara方法中

通过模板加上数据渲染出了sqlPara

之后调用Db.update方法执行这个预编译了的sql语句,因为这里是预编译的方式,所以不能够进行sql注入,但是这里没有进行校验就通过insert语句将payload语句写入了数据库中,导致在每次渲染新闻页面的时候都会执行我们的XSS payload,进而形成了存储型XSS漏洞

Defence

当然不止这一处的位置具有XSS,几乎在页面所有可以评论的地方都会形成存储型XSS

  1. 严格校验写入数据库的内容

CSRF

可以抓包发现并没有使用CSRF-Token进行防御

我们可以尝试通过CSRF来进行恶意操作

Defence

  1. 验证 HTTP Referer 字段
  2. 添加Token进行验证
  3. HTTP中添加自定义属性进行验证

SQL注入

我们可以注意到在后台存在一个代码生成的功能点

能够执行sql语句,或许这里存在sql注入

找到后端代码点

检索路由

我们可以定位到SystemGenerateController类控制器

关注到create方法中

通过获取sql参数值获取了type的sql语句,之后调用Db.update执行sql语句

又调用了MAIN.update继续执行sql语句,其中MAINDbPro类,即调用了DbPro#update方法

紧接着跟进了DbPro#update方法的重载

之后将会调用到DbPro#update方法,返回类型为int

首先会进行预编译,之后通过调用MysqlDialect#fillStatement方法填充数据(当然我这里不需要填充)

最后通过调用executeUpdate执行sql语句,跟进

最后将会调用stmt这个句柄的executeUpdate方法进行执行

我们可以关注一下到底能够执行哪些sql语句

从注释中可以知道可以执行update / insert / delete语句

我们就可以构造恶意的sql语句形成注入

update of_cms_link set link_name=updatexml(1,concat(0x7e,(user())),0) where link_id = 4

同样也可以使用其他的报错语句

虽然这里使用了预编译的方式进行了sql语句的执行,但是根本没有发挥任何的作用

Defence

  1. 对用户的输入进行过滤

文章来源: https://xz.aliyun.com/t/11760
如有侵权请联系:admin#unsafe.sh