从gitee下载源码
根据开发文档,我们首先将项目导入idea开发工具中
之后在Mysql中新建ofcms数据库
在ofcms-admin模块中的src/main/resources/conf/dev/conf/db-config.properties
文件中配置JDBC连接配置
之后将其重命名为db.properties
初始化数据库,有两种方式,一种是手工的,一种是自动的,我这里选择的是手工的方式
将doc/sql
文件夹的sql文件导入到mysql中执行
最开开启Tomcat容器,启动项目
管理后台
默认密码为admin/123456
首先我们可以看看这个CMS用的组件,是不是有一些组件可以直接造成漏洞
我们可以发现有着freemarker
,那么是否有着模板注入的风险呢,答案当然是有的
我们同时可以大概看一下项目
上面提到了使用了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
进行了命令执行
对于这中漏洞的防御,我们可以添加完善的WAF进行拦截
或者是使用TemplateClassResolver
文档:
https://freemarker.apache.org/docs/api/freemarker/core/TemplateClassResolver.html
但是同样的,如果有着一些不当的配置仍然能够Bypass,比如说?api
的开启
这里不存在有文件上传的点,只是在编辑修改模板文件的时候,能够指定文件绝对路径和文件名,且没有过滤文件后缀
我们在上面SSTI分析中,对模板的修改保存流程的跟踪中我们知道调用了TemplateController#save
方法进行保存修改的模板文件内容
在该方法中获取了dirs
参数,将会和前面获取到了的pathFile
进行拼接,之后会获取file_name
参数获取到文件名和文件内容,最后将文件内容写入文件中
值得注意的是,这里并没有存在任何的过滤,不管是路径穿越,又或者是文件名,或者是危险函数,能够进行文件上传
最后也能够在Tomcat容器中发现写入了shell.jsp
同样在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漏洞
当然不止这一处的位置具有XSS,几乎在页面所有可以评论的地方都会形成存储型XSS
可以抓包发现并没有使用CSRF-Token进行防御
我们可以尝试通过CSRF来进行恶意操作
我们可以注意到在后台存在一个代码生成的功能点
能够执行sql语句,或许这里存在sql注入
找到后端代码点
检索路由
我们可以定位到SystemGenerateController
类控制器
关注到create
方法中
通过获取sql
参数值获取了type的sql语句,之后调用Db.update
执行sql语句
又调用了MAIN.update
继续执行sql语句,其中MAIN
是DbPro
类,即调用了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语句的执行,但是根本没有发挥任何的作用