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

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

写在前面

之前四月就关注到了,可是后面不知道什么原因某步下了公众号,今天又被再次提起,当时分析了一半也就是权限相关的调用,现在补上另一半

正文

鉴权相关配置简析

既然和权限绕过相关那么第一步我们必然要去先看看相关配置,在web.xml配置文件当中,可以看到相关的如下配置

这里我们只要关注两点,第一servelet需要以/carsrs开头,第二配置文件在/com/virtual/plat/config/beans-*.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<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对应的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LoginParameter {
private boolean a;

public LoginParameter() {
}

public boolean isIgnorePw() {
return this.a;
}

public void setIgnorePw(boolean var1) {
this.a = var1;
}
}

在不能运行的情况下,我们只能尝试去搜索看看,通过许少写的jar analyzer很快便定位到了其调用位置,从以下函数逻辑来看,显然函数逻辑只是和密码有效期相关

image-20240513194157134

因此,我们只剩下this.a(var6, var7, var5, var4)可以关注

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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()逻辑可以看出,默认情况下是没有认证中心的,也就是本地认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static boolean isAuthorCenter() {
return gInstance == null ? false : gInstance.useAuthorCenter();
}

public boolean useAuthorCenter() {
return "authorCenter".equals(this.authorizeType);
}


@Service
public 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,因此密码是否正确都不会影响程序的执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
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认证的认证过程,不难得出利用的流程

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

文件上传

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

image-20240513200944485

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}
}

因此完整的利用也就分析出了,由于没有环境,以上分析仅作参考