致远OA ajaxAction文件上传漏洞代码分析
致远OA系统由北京致远互联软件股份有限公司开发,是一款基于互联网高效协作的协同管理软件,在各企业机构中被广泛使用。
1.漏洞介绍
致远 OA 系统的一些版本存在代码执行漏洞,攻击者在无需登录的情况下可通过向 URL /seeyon/ajax.do地址发送构造好的POST请求包,造成代码执行,可向目标服务器写入任意文件造成getshell。
影响版本:
致远OA V8.0、V8.0SP1
致远OA V7.1、V7.1SP1
2.漏洞演示
发送构造的请求
成功写入shell
3.漏洞原理
3.1代码执行
根据漏洞触发点,找对应的控制器
/ajax.do 对应的class是com.seeyon.ctp.common.service.AjaxControlle
反编译其对应的jar包,在
com.seeyon.ctp.common.service.AjaxController#invokeService
处,对http请求进行处理,
ZipUtil.uncompressRequest
Gzip解码arguments参数为 utf-8编码的string字符串
Object service = getService(Strings.escapeJavascript(serviceName));
获取formulaManager bean,实现formulaManager接口
<bean id="formulaManager" class="com.seeyon.ctp.common.formula.manager.FormulaManagerImpl">
接着调用invokeMethod方法
把string类型的arguments转成json
Method.invoke反射调用formulaManagerlmpl的validate方法
堆栈信息:
invokeMethod:591, AjaxController (com.seeyon.ctp.common.service)
invokeService:359, AjaxController (com.seeyon.ctp.common.service)
ajaxAction:163, AjaxController (com.seeyon.ctp.common.service)
invoke:-1, GeneratedMethodAccessor928 (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeNamedMethod:471, MultiActionController (org.springframework.web.servlet.mvc.multiaction)
handleRequestInternal:408, MultiActionController (org.springframework.web.servlet.mvc.multiaction)
handleRequest:153, AbstractController (org.springframework.web.servlet.mvc)
handle:48, SimpleControllerHandlerAdapter (org.springframework.web.servlet.mvc)
doDispatch:933, DispatcherServlet (org.springframework.web.servlet)
doService:867, DispatcherServlet (org.springframework.web.servlet)
processRequest:951, FrameworkServlet (org.springframework.web.servlet)
doPost:853, FrameworkServlet (org.springframework.web.servlet)
service:660, HttpServlet (javax.servlet.http)
service:827, FrameworkServlet (org.springframework.web.servlet)
service:741, HttpServlet (javax.servlet.http)
internalDoFilter:231, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:52, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:47, GenericFilter (com.seeyon.ctp.common.web)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:58, CharacterEncodingFilter (com.seeyon.ctp.common.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:88, CTPSecurityFilter (com.seeyon.ctp.common.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:38, CTPCsrfGuardFilter (com.seeyon.ctp.common.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:26, CTPSessionRepositoryFilter (org.springframework.session.web.http)
doFilter:80, OncePerRequestFilter (org.springframework.session.web.http)
invokeDelegate:343, DelegatingFilterProxy (org.springframework.web.filter)
doFilter:260, DelegatingFilterProxy (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
invoke:199, StandardWrapperValve (org.apache.catalina.core)
invoke:96, StandardContextValve (org.apache.catalina.core)
invoke:493, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:137, StandardHostValve (org.apache.catalina.core)
invoke:81, ErrorReportValve (org.apache.catalina.valves)
invoke:87, StandardEngineValve (org.apache.catalina.core)
service:343, CoyoteAdapter (org.apache.catalina.connector)
service:798, Http11Processor (org.apache.coyote.http11)
process:66, AbstractProcessorLight (org.apache.coyote)
process:808, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1498, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)
跟进formulaManagerlmpl.class
Validate方法为overloading,当参数为4个的时候,调用的是
FormulaUtil.validate方法
跟进seeyon-ctp-core.jar包中,
可以看到,这里对FormulaType做判断,GroovyFunction和Variable为固定值2和1,FormulaType可控。通过后执行eval写入文件。
跟进com.seeyon.ctp.common.formula.FormulaUtil#eval
其最终调用的是com.seeyon.ctp.common.script.ScriptEvaluator#eval
执行代码,写入文件。
run:8, Script1
eval:321, GroovyScriptEngineImpl (org.codehaus.groovy.jsr223)
eval:72, GroovyCompiledScript (org.codehaus.groovy.jsr223)
eval:92, CompiledScript (javax.script)
eval:63, CompiledScriptRunner (com.seeyon.ctp.common.script)
eval:91, ScriptEvaluator (com.seeyon.ctp.common.script)
eval:536, FormulaUtil (com.seeyon.ctp.common.formula)
validate:417, FormulaUtil (com.seeyon.ctp.common.formula)
validate:481, FormulaManagerImpl (com.seeyon.ctp.common.formula.manager)
invoke:-1, FormulaManagerImpl$$FastClassBySpringCGLIB$$cf3c5088 (com.seeyon.ctp.common.formula.manager)
invoke:204, MethodProxy (org.springframework.cglib.proxy)
invokeJoinpoint:701, CglibAopProxy$CglibMethodInvocation (org.springframework.aop.framework)
proceed:150, ReflectiveMethodInvocation (org.springframework.aop.framework)
proceedWithInvocation:103, CTPTransactionInterceptor$1 (org.springframework.transaction.interceptor)
invokeWithinTransaction:133, CTPTransactionInterceptor (org.springframework.transaction.interceptor)
invoke:101, CTPTransactionInterceptor (org.springframework.transaction.interceptor)
proceed:172, ReflectiveMethodInvocation (org.springframework.aop.framework)
invoke:91, ExposeInvocationInterceptor (org.springframework.aop.interceptor)
proceed:172, ReflectiveMethodInvocation (org.springframework.aop.framework)
intercept:633, CglibAopProxy$DynamicAdvisedInterceptor (org.springframework.aop.framework)
validate:-1, FormulaManagerImpl$$EnhancerBySpringCGLIB$$fecdef2d (com.seeyon.ctp.common.formula.manager)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeMethod:591, AjaxController (com.seeyon.ctp.common.service)
invokeService:359, AjaxController (com.seeyon.ctp.common.service)
ajaxAction:163, AjaxController (com.seeyon.ctp.common.service)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeNamedMethod:471, MultiActionController (org.springframework.web.servlet.mvc.multiaction)
handleRequestInternal:408, MultiActionController (org.springframework.web.servlet.mvc.multiaction)
handleRequest:153, AbstractController (org.springframework.web.servlet.mvc)
handle:48, SimpleControllerHandlerAdapter (org.springframework.web.servlet.mvc)
doDispatch:933, DispatcherServlet (org.springframework.web.servlet)
doService:867, DispatcherServlet (org.springframework.web.servlet)
processRequest:951, FrameworkServlet (org.springframework.web.servlet)
doPost:853, FrameworkServlet (org.springframework.web.servlet)
service:660, HttpServlet (javax.servlet.http)
service:827, FrameworkServlet (org.springframework.web.servlet)
service:741, HttpServlet (javax.servlet.http)
internalDoFilter:231, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:52, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:47, GenericFilter (com.seeyon.ctp.common.web)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:58, CharacterEncodingFilter (com.seeyon.ctp.common.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:88, CTPSecurityFilter (com.seeyon.ctp.common.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:38, CTPCsrfGuardFilter (com.seeyon.ctp.common.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:26, CTPSessionRepositoryFilter (org.springframework.session.web.http)
doFilter:80, OncePerRequestFilter (org.springframework.session.web.http)
invokeDelegate:343, DelegatingFilterProxy (org.springframework.web.filter)
doFilter:260, DelegatingFilterProxy (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
invoke:199, StandardWrapperValve (org.apache.catalina.core)
invoke:96, StandardContextValve (org.apache.catalina.core)
invoke:493, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:137, StandardHostValve (org.apache.catalina.core)
invoke:81, ErrorReportValve (org.apache.catalina.valves)
invoke:87, StandardEngineValve (org.apache.catalina.core)
service:343, CoyoteAdapter (org.apache.catalina.connector)
service:798, Http11Processor (org.apache.coyote.http11)
process:66, AbstractProcessorLight (org.apache.coyote)
process:808, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1498, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)
3.2未授权
在tomcat中,在接收到请求时会对客户端提交的参数、URL、Header和Body数据进行解析,并生成Request对象,在Servlet处理URL请求的路径时,HTTPServletRequest有如下几个常用的函数:
request.getRequestURL():返回全路径;
request.getRequestURI():返回除去Host(域名或IP)部分的路径;
request.getContextPath():返回工程名部分,如果工程映射为/,则返回为空;
request.getServletPath():返回除去Host和工程名部分的路径;
request.getPathInfo():仅返回传递到Servlet的路径,如果没有传递额外的路径信息,则此返回Null;
回到seeyon代码,查看其web.xml中配置的filter,可以发现,所以的.do都会经过dispatcherServlet,CSRFGuard,SecurityFilter,encodingFilter,从命名来看,猜测主要的权限判断在SecurityFilter。
跟进SecurityFilter
com.seeyon.ctp.common.web.filter.CTPSecurityFilter#doFilter
根据filter链,如果access=true,则可以filterChain.doFilter下一个filter,
只要accept = authenticator.authenticate(req, resp);为真,就能满足条件。
跟进authenticate方法,可以看到,方法内会检查当前用户,是否在线和用户权限等情况,当用户为空的时候,会检查当前访问地址,是否需要权限认证
if (user == null) {
AppContext.removeThreadContext("SESSION_CONTEXT_USERINFO_KEY");
isAnnotationNeedlessLogin = this.isNeedlessCheckLogin(context);
if (!isAnnotationNeedlessLogin) {
LoginTokenUtil.checkLoginToken(request);
}
} else {
isGuest = user.isGuest();
OnlineUser onlineUser = !isGuest ? OnlineRecorder.getOnlineUser(user) : null;
isOnlineMember = !isGuest && onlineUser != null && onlineUser.getSessionIds().contains(user.getSessionId());
if (!isOnlineMember) {
isAnnotationNeedlessLogin = this.isNeedlessCheckLogin(context);
}
AppContext.putThreadContext("SESSION_CONTEXT_USERINFO_KEY", user);
}
在代码中可以看到,isNeedlessCheckLogin是做权限判断,继续跟进。
private boolean isNeedlessCheckLogin(CTPRequestContext context) throws BusinessException {
HttpServletRequest request = context.getRequest();
String accessUrl = request.getRequestURI();
String method = this.getRealMethodName(context);
if (context.isAjax()) {
accessUrl = context.getParameter("managerName");
}
Map<String, Set<String>> needlessUrlMap = this.checkLoginAnnotationAware.getNeedlessUrlMap();
Set<String> keys = needlessUrlMap.keySet();
boolean needlessUrl = false;
Iterator var8 = keys.iterator();
while(var8.hasNext()) {
String key = (String)var8.next();
if (accessUrl.indexOf(key) != -1) {
Set<String> methods = (Set)needlessUrlMap.get(key);
needlessUrl = methods.contains("*") || methods.contains(method);
if (needlessUrl) {
break;
}
}
}
return needlessUrl;
}
该方法通过getRequestURI去获取用户请求地址,再和白名单中不需要认证的路由地址做判断,如果accessUrl的值中包含白名单中的值,则放行。
可以看到未对uri做处理,至此,造成未授权,结合前面的代码执行漏洞,造成直接写入shell。