浅析H3C-CAS虚拟化管理系统权限绕过致文件上传漏洞

admin 2024年10月29日00:32:28评论14 views字数 12999阅读43分19秒阅读模式

浅析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这个参数的传递过程,它出现在两个部分:

  1. this.a(var6, var7, var5, var4))
  2. (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很快便定位到了其调用位置,从以下函数逻辑来看,显然函数逻辑只是和密码有效期相关

浅析H3C-CAS虚拟化管理系统权限绕过致文件上传漏洞

因此,我们只剩下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);}@Servicepublic class AuthorCenterService {    private static Log log = LogFactory.getLog(AuthorCenterService.class);    @Resource    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摘要信息是否一致,而由于var4true,因此密码是否正确都不会影响程序的执行

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身份信息

文件上传

不全给出所有细节了(看文章总需要多自己思考),上传的路由可以自己去找找,给个提示

浅析H3C-CAS虚拟化管理系统权限绕过致文件上传漏洞

而这个函数在返回路径时直接做了路径的拼接

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

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年10月29日00:32:28
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   浅析H3C-CAS虚拟化管理系统权限绕过致文件上传漏洞https://cn-sec.com/archives/3314471.html

发表评论

匿名网友 填写信息