浅析H3C-CAS虚拟化管理系统权限绕过致文件上传漏洞
写在前面
之前四月就关注到了,可是后面不知道什么原因某步下了公众号,今天又被再次提起,当时分析了一半也就是权限相关的调用,现在补上另一半
正文
鉴权相关配置简析
既然和权限绕过相关那么第一步我们必然要去先看看相关配置,在web.xml
配置文件当中,可以看到相关的如下配置
这里我们只要关注两点,第一servelet需要以/carsrs
开头,第二配置文件在/com/virtual/plat/config/beans-*.xml
下
12345678910111213141516171819202122232425 |
<context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath*:/com/virtual/plat/config/beans-*.xml </param-value></context-param>xxxxxx省略xxxxxx<servlet-mapping> <servlet-name>Jersey Spring Web Application</servlet-name> <url-pattern>/casrs/*</url-pattern></servlet-mapping><servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:/com/virtual/plat/config/dispatcher-servlet.xml</param-value> </init-param> <init-param> <param-name>dispatchOptionsRequest</param-name> <param-value>true</param-value> </init-param> <load-on-startup>1</load-on-startup> <async-supported>true</async-supported></servlet> |
关联对应配置,这里由于路由前缀固定,想尝试通过静态文件去绕过鉴权限制的老思路可以先暂时放弃,在这里可以重点关注鉴权对应处理的digestFilter
对应的类
1234567891011121314151617181920 |
xxxxxx省略xxxxxx(列举部分)<http pattern="/html/help/**" security="none"/><http pattern="/js/lib/jquery-1.9.1.min.js" security="none"/><http pattern="/warnManage/add" security="none"/>xxxxxx省略xxxxxx<http pattern="/casrs/**" entry-point-ref="digestEntryPoint"> <intercept-url pattern="/**" access="hasRole('ROLE_RSCLIENT')" requires-channel="any"/> <custom-filter ref="digestFilter" position="BASIC_AUTH_FILTER"/> <csrf disabled="true"/></http><!-- rest接口使用 --><beans:bean id="digestFilter" class="com.virtual.plat.server.rs.ext.event.PasswordProtectDigestAuthenticationFilter"> <beans:property name="userDetailsService" ref="casUserDetailsService" /> <beans:property name="authenticationEntryPoint" ref="digestEntryPoint" /> <beans:property name="userCache" ref="casAuthUserCache" /></beans:bean> |
“阉割”的鉴权路由
接下来我们来我们就具体看看com.virtual.plat.server.rs.ext.event.PasswordProtectDigestAuthenticationFilter
做了什么处理
从代码中不难看出,如果Path为/vm/backUpFromCasserver
,那么变量var4
则会被设置为true
123456789101112131415161718192021222324 |
public void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException { GenericHttpRequest var6 = new GenericHttpRequest((HttpServletRequest)var1); if (this.a(var6, var2)) { boolean var4 = false; String var5 = ((HttpServletRequest)var6).getPathInfo(); if ("/vm/backUpFromCasserver".equals(var5)) { var4 = true; } super.doFilter(var6, var2, var3, var4); if (SecurityContextHolder.getContext().getAuthentication() == null) { HttpServletRequest var7; String var8; if ((var8 = (var7 = (HttpServletRequest)var6).getHeader("Authorization")) != null && var8.startsWith("Digest ")) { this.a(var6); } return; } this.b(var6); }} |
继续跟进super.doFilter
的调用,其父类的调用为com.virtual.plat.server.rs.ext.event.DigestAuthenticationFilterExt#doFilter
在这里,我们重点关注var4
这个参数的传递过程,它出现在两个部分:
this.a(var6, var7, var5, var4))
(var8 = new LoginParameter()).setIgnorePw(var4);
123456789101112131415161718 |
public void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3, boolean var4) throws IOException, ServletException { HttpServletRequest var6 = (HttpServletRequest)var1; HttpServletResponse var7 = (HttpServletResponse)var2; String var5; if ((var5 = var6.getHeader("Authorization")) == null || !var5.startsWith("Digest ") || this.a(var6, var7, var5, var4)) { if (var5 == null || !var5.startsWith("LDAP ") || this.b(var6, var7, var5)) { LoginParameter var8; if ((var8 = LocalParameter.get()) == null) { (var8 = new LoginParameter()).setIgnorePw(var4); LocalParameter.put(var8); } else { var8.setIgnorePw(var4); } var3.doFilter(var6, var7); } }} |
由于后者名字没有混淆更直观,因此我们选择优先查看其如何被调用,从英文名来看,似乎字面意思是设置了忽略密码的属性
由于我只有代码没有环境想在环境中动态调试验证明显不太可能,换个方向思考,有设置必然有获取
从类LoginParameter
的方法当中我们不难看出在获取并判断时使用了方法isIgnorePw
1234567891011121314 |
public class LoginParameter { private boolean a; public LoginParameter() { } public boolean isIgnorePw() { return this.a; } public void setIgnorePw(boolean var1) { this.a = var1; }} |
在不能运行的情况下,我们只能尝试去搜索看看,通过许少写的jar analyzer
很快便定位到了其调用位置,从以下函数逻辑来看,显然函数逻辑只是和密码有效期相关
因此,我们只剩下this.a(var6, var7, var5, var4)
可以关注
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556 |
private boolean a(HttpServletRequest var1, HttpServletResponse var2, String var3, boolean var4) throws IOException, ServletException { if (AuthorCenterService.isAuthorCenter()) { Map var15; if ((var15 = a(a(var3.substring(7), ','), "=", "\"")) == null) { a.error("handleAuthAC Error: headerMap is null."); return false; } else { String var5 = (String)var15.get("username"); String var6 = (String)var15.get("realm"); String var7 = (String)var15.get("nonce"); String var8 = (String)var15.get("uri"); String var9 = (String)var15.get("response"); String var10 = (String)var15.get("qop"); String var11 = (String)var15.get("nc"); String var16 = (String)var15.get("cnonce"); DigestInfo var12; (var12 = new DigestInfo()).setEntryPoint(this.getAuthenticationEntryPoint()); var12.setUsername(var5); var12.setRealm(var6); var12.setNonce(var7); var12.setUri(var8); var12.setResponseDigest(var9); var12.setQop(var10); var12.setNc(var11); var12.setCnonce(var16); var12.setRequestMethod(var1.getMethod()); var16 = AuthorCenterService.getInstance().digestAuth(var12); if (var16 != null) { this.a((HttpServletRequest)var1, (HttpServletResponse)var2, (AuthenticationException)(new BadCredentialsException(var16))); return false; } else { if (a.isDebugEnabled()) { a.debug("Authentication success for user: '" + var5 + "' with response: '" + var9 + "'"); } UserDetails var13 = this.f.loadUserByUsername(var5); UsernamePasswordAuthenticationToken var14; if (this.h) { var14 = new UsernamePasswordAuthenticationToken(var13, var13.getPassword(), var13.getAuthorities()); } else { var14 = new UsernamePasswordAuthenticationToken(var13, var13.getPassword()); } var14.setDetails(this.c.buildDetails(var1)); SecurityContextHolder.getContext().setAuthentication(var14); if (var1.getSession() != null) { var1.getSession().setAttribute("loginName", var5); } return true; } } } else { return this.b(var1, var2, var3, var4); }} |
由于没有具体代码,从AuthorCenterService.isAuthorCenter()
逻辑可以看出,默认情况下是没有认证中心的,也就是本地认证
123456789101112131415 |
public static boolean isAuthorCenter() { return gInstance == null ? false : gInstance.useAuthorCenter();}public boolean useAuthorCenter() { return "authorCenter".equals(this.authorizeType);}public class AuthorCenterService { private static Log log = LogFactory.getLog(AuthorCenterService.class); private OperatorMgr operatorMgr = null; String authorizeType = "local"; |
因此自然而然函数的调用流向了com.virtual.plat.server.rs.ext.event.DigestAuthenticationFilterExt#b(HttpServletRequest, HttpServletResponse, java.lang.String, boolean)
,在这个认证中我们主要看if (!var14.equals(var10) && !var4) {
,它的作用就是比对response
摘要信息是否一致,而由于var4
为true
,因此密码是否正确都不会影响程序的执行
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121 |
private boolean b(HttpServletRequest var1, HttpServletResponse var2, String var3, boolean var4) throws IOException, ServletException {Map var5;String var6 = (String)(var5 = a(a(var3 = var3.substring(7), ','), "=", "\"")).get("username");String var7 = (String)var5.get("realm");String var8 = (String)var5.get("nonce");String var9 = (String)var5.get("uri");String var10 = (String)var5.get("response");String var11 = (String)var5.get("qop");String var12 = (String)var5.get("nc");String var25 = (String)var5.get("cnonce");if (var6 != null && var7 != null && var8 != null && var9 != null && var2 != null) { if (!"auth".equals(var11) || var12 != null && var25 != null) { if (!var7.equals(this.getAuthenticationEntryPoint().getRealmName())) { this.a((HttpServletRequest)var1, (HttpServletResponse)var2, (AuthenticationException)(new BadCredentialsException(this.messages.getMessage("DigestAuthenticationFilter.incorrectRealm", new Object[]{var7, this.getAuthenticationEntryPoint().getRealmName()}, "Response realm name '{0}' does not match system realm name of '{1}'")))); return false; } else if (!Base64.isBase64(var8.getBytes())) { this.a((HttpServletRequest)var1, (HttpServletResponse)var2, (AuthenticationException)(new BadCredentialsException(this.messages.getMessage("DigestAuthenticationFilter.nonceEncoding", new Object[]{var8}, "Nonce is not encoded in Base64; received nonce {0}")))); return false; } else { String[] var13; if ((var13 = StringUtils.delimitedListToStringArray(var3 = new String(Base64.decode(var8.getBytes())), ":")).length != 2) { this.a((HttpServletRequest)var1, (HttpServletResponse)var2, (AuthenticationException)(new BadCredentialsException(this.messages.getMessage("DigestAuthenticationFilter.nonceNotTwoTokens", new Object[]{var3}, "Nonce should have yielded two tokens but was {0}")))); return false; } else { long var18; try { var18 = new Long(var13[0]); } catch (NumberFormatException var22) { this.a((HttpServletRequest)var1, (HttpServletResponse)var2, (AuthenticationException)(new BadCredentialsException(this.messages.getMessage("DigestAuthenticationFilter.nonceNotNumeric", new Object[]{var3}, "Nonce token should have yielded a numeric first token, but was {0}")))); return false; } if (!a(var18 + ":" + this.getAuthenticationEntryPoint().getKey()).equals(var13[1])) { this.a((HttpServletRequest)var1, (HttpServletResponse)var2, (AuthenticationException)(new BadCredentialsException(this.messages.getMessage("DigestAuthenticationFilter.nonceCompromised", new Object[]{var3}, "Nonce token compromised {0}")))); return false; } else { boolean var24 = false; UserDetails var26; if ((var26 = this.e.getUserFromCache(var6)) == null) { var24 = true; try { var26 = this.f.loadUserByUsername(var6); } catch (UsernameNotFoundException var21) { this.a((HttpServletRequest)var1, (HttpServletResponse)var2, (AuthenticationException)(new BadCredentialsException(this.messages.getMessage("DigestAuthenticationFilter.usernameNotFound", new Object[]{var6}, "Username {0} not found")))); return false; } if (var26 == null) { throw new AuthenticationServiceException("AuthenticationDao returned null, which is an interface contract violation"); } this.e.putUserInCache(var26); } String var14; if (!(var14 = a(this.g, var6, var7, var26.getPassword(), var1.getMethod(), var9, var11, var8, var12, var25)).equals(var10) && !var24 && !var4) { if (a.isDebugEnabled()) { a.debug("Digest comparison failure; trying to refresh user from DAO in case password had changed"); } try { var26 = this.f.loadUserByUsername(var6); } catch (UsernameNotFoundException var20) { this.a((HttpServletRequest)var1, (HttpServletResponse)var2, (AuthenticationException)(new BadCredentialsException(this.messages.getMessage("DigestAuthenticationFilter.usernameNotFound", new Object[]{var6}, "Username {0} not found")))); } this.e.putUserInCache(var26); var14 = a(this.g, var6, var7, var26.getPassword(), var1.getMethod(), var9, var11, var8, var12, var25); } if (!var14.equals(var10) && !var4) { if (a.isDebugEnabled()) { a.debug("Expected response: '" + var14 + "' but received: '" + var10 + "'; is AuthenticationDao returning clear text passwords?"); } this.a((HttpServletRequest)var1, (HttpServletResponse)var2, (AuthenticationException)(new BadCredentialsException(this.messages.getMessage("DigestAuthenticationFilter.incorrectResponse", "Incorrect response")))); return false; } else if (var18 < System.currentTimeMillis()) { this.a((HttpServletRequest)var1, (HttpServletResponse)var2, (AuthenticationException)(new NonceExpiredException(this.messages.getMessage("DigestAuthenticationFilter.nonceExpired", "Nonce has expired/timed out")))); return false; } else { if (a.isDebugEnabled()) { a.debug("Authentication success for user: '" + var6 + "' with response: '" + var10 + "'"); } UsernamePasswordAuthenticationToken var23; if (this.h) { var23 = new UsernamePasswordAuthenticationToken(var26, var26.getPassword(), var26.getAuthorities()); } else { var23 = new UsernamePasswordAuthenticationToken(var26, var26.getPassword()); } var23.setDetails(this.c.buildDetails(var1)); SecurityContextHolder.getContext().setAuthentication(var23); if (var1.getSession() != null) { var1.getSession().setAttribute("loginName", var6); } return true; } } } } } else { if (a.isDebugEnabled()) { a.debug("extracted nc: '" + var12 + "'; cnonce: '" + var25 + "'"); } this.a((HttpServletRequest)var1, (HttpServletResponse)var2, (AuthenticationException)(new BadCredentialsException(this.messages.getMessage("DigestAuthenticationFilter.missingAuth", new Object[]{var3}, "Missing mandatory digest value; received header {0}")))); return false; }} else { if (a.isDebugEnabled()) { a.debug("extracted username: '" + var6 + "'; realm: '" + var6 + "'; nonce: '" + var6 + "'; uri: '" + var6 + "'; response: '" + var6 + "'"); } this.a((HttpServletRequest)var1, (HttpServletResponse)var2, (AuthenticationException)(new BadCredentialsException(this.messages.getMessage("DigestAuthenticationFilter.missingMandatory", new Object[]{var3}, "Missing mandatory digest value; received header {0}")))); return false;}} |
根据digest认证的认证过程,不难得出利用的流程
12 |
1. 访问backUpFromCasserver端点,服务器发送临时的质询码2. 根据质询码计算出响应码并发送给服务端校验 |
而根据代码即可得出Payload的构造
1
|
Authorization: Digest username="admin", realm="VMC RESTful Web Services", nonce="xxxxx", uri="/cas/xxxxx", response="xxxxxx", qop=auth, nc=xxxx, cnonce="xxxxx", algorithm=xxxx
|
最终通过backUpFromCasserver
端点即可获取Cookie身份信息
文件上传
不全给出所有细节了(看文章总需要多自己思考),上传的路由可以自己去找找,给个提示
而这个函数在返回路径时直接做了路径的拼接
12345678910111213141516 |
public static File getTokenedFile(String var0) throws IOException { if (var0 != null && !var0.isEmpty()) { File var1; if (!(var1 = new File("/vms/tmptemplet/" + File.separator + var0)).getParentFile().exists()) { var1.getParentFile().mkdirs(); } if (!var1.exists()) { var1.createNewFile(); } return var1; } else { return null; }} |
因此完整的利用也就分析出了,由于没有环境,以上分析仅作参考
- source:y4tacker
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论