致远OA ajaxAction文件上传漏洞代码分析

致远OA系统由北京致远互联软件股份有限公司开发,是一款基于互联网高效协作的协同管理软件,在各企业机构中被广泛使用。

1.漏洞介绍

致远 OA 系统的一些版本存在代码执行漏洞,攻击者在无需登录的情况下可通过向 URL /seeyon/ajax.do地址发送构造好的POST请求包,造成代码执行,可向目标服务器写入任意文件造成getshell。

影响版本:

致远OA V8.0、V8.0SP1

致远OA V7.1、V7.1SP1

2.漏洞演示

发送构造的请求

image-20210105160520952

成功写入shell

image-20210105160530893

3.漏洞原理

3.1代码执行

根据漏洞触发点,找对应的控制器

image-20210105162233922

/ajax.do 对应的class是com.seeyon.ctp.common.service.AjaxControlle

反编译其对应的jar包,在

com.seeyon.ctp.common.service.AjaxController#invokeService

处,对http请求进行处理, image018

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方法

image-20210302103755916

堆栈信息:

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方法 image020

跟进seeyon-ctp-core.jar包中, image-20210302104042636

可以看到,这里对FormulaType做判断,GroovyFunction和Variable为固定值2和1,FormulaType可控。通过后执行eval写入文件。

跟进com.seeyon.ctp.common.formula.FormulaUtil#eval

其最终调用的是com.seeyon.ctp.common.script.ScriptEvaluator#eval

执行代码,写入文件。

image-20210302104347998

image-20210302104426990

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; image-20210105163521820

回到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);为真,就能满足条件。 image-20210302113330063

跟进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的值中包含白名单中的值,则放行。

image-20210302104913707

image-20210302104841043

可以看到未对uri做处理,至此,造成未授权,结合前面的代码执行漏洞,造成直接写入shell。