简介
Hawt Hawtio是一款用于管理Java内容的模块化Web控制台程序。 Hawt Hawtio 2.5.0及之前版本中存在代码问题漏洞。该漏洞源于网络系统或产品的代码开发过程中存在设计或实现不当的问题。
环境搭建
源码包下载地址:
https://oss.sonatype.org/content/repositories/public/io/hawt/hawtio-default/2.5.0/hawtio-default-2.5.0.war
通过tomcat部署:
去tomcat的后台,然后选择WAR file to deply
栏目,点击选择hawtio-default-2.5.0.war
上传,最后deplay部署即可:
![image-20240709175157949 image-20240709175157949](/svg/loading.min.svg)
布置以后,会出现布置好的应用,点击应用进入即可:
![image-20240709175244400 image-20240709175244400](/svg/loading.min.svg)
![image-20240709175254892 image-20240709175254892](/svg/loading.min.svg)
漏洞分析
源码获取方式:
- 可以通过反编译获取本程序的源码,
- 或者通过 github 的 tree 分支来获取源码。
先测试一下漏洞点:
1
|
http://127.0.0.1:8080/hawtio-default-2.5.0/proxy/http://localhost:7777/1.txt
|
![image-20240709175639767 image-20240709175639767](/svg/loading.min.svg)
hawtio-system-2.5.0.jar直接反编译
![image-20240709175850746 image-20240709175850746](/svg/loading.min.svg)
通过漏洞描述可以看出问题出现在proxy
,所以直接搜索ProxyServlet之类的字眼
![image-20240709185519716 image-20240709185519716](/svg/loading.min.svg)
找到相关文件:hawtio-system/src/main/java/io/hawt/web/proxy/ProxyServlet.java
从service
方法开始分析
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
|
protected void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws ServletException, IOException {
ProxyAddress proxyAddress = this.parseProxyAddress(servletRequest);
if (proxyAddress != null && proxyAddress.getFullProxyUrl() != null) {
if (proxyAddress instanceof ProxyDetails) {
ProxyDetails details = (ProxyDetails)proxyAddress;
if (!this.whitelist.isAllowed(details)) {
LOG.debug("Rejecting {}", proxyAddress);
ServletHelpers.doForbidden(servletResponse, ForbiddenReason.HOST_NOT_ALLOWED);
return;
}
}
String method = servletRequest.getMethod();
String proxyRequestUri = proxyAddress.getFullProxyUrl();
URI targetUriObj;
try {
targetUriObj = new URI(proxyRequestUri);
} catch (URISyntaxException var25) {
LOG.error("URL '{}' is not valid: {}", proxyRequestUri, var25.getMessage());
servletResponse.setStatus(404);
return;
}
Object proxyRequest;
if (servletRequest.getHeader("Content-Length") == null && servletRequest.getHeader("Transfer-Encoding") == null) {
proxyRequest = new BasicHttpRequest(method, proxyRequestUri);
} else {
HttpEntityEnclosingRequest eProxyRequest = new BasicHttpEntityEnclosingRequest(method, proxyRequestUri);
eProxyRequest.setEntity(new InputStreamEntity(servletRequest.getInputStream(), (long)servletRequest.getContentLength()));
proxyRequest = eProxyRequest;
}
this.copyRequestHeaders(servletRequest, (HttpRequest)proxyRequest, targetUriObj);
String username = proxyAddress.getUserName();
String password = proxyAddress.getPassword();
if (Strings.isNotBlank(username) && Strings.isNotBlank(password)) {
String encodedCreds = Base64.encodeBase64String((username + ":" + password).getBytes());
((HttpRequest)proxyRequest).setHeader("Authorization", "Basic " + encodedCreds);
}
Header proxyAuthHeader = ((HttpRequest)proxyRequest).getFirstHeader("Authorization");
if (proxyAuthHeader != null) {
String proxyAuth = proxyAuthHeader.getValue();
HttpSession session = servletRequest.getSession();
if (session != null) {
String previousProxyCredentials = (String)session.getAttribute("proxy-credentials");
if (previousProxyCredentials != null && !previousProxyCredentials.equals(proxyAuth)) {
this.cookieStore.clear();
}
session.setAttribute("proxy-credentials", proxyAuth);
}
}
this.setXForwardedForHeader(servletRequest, (HttpRequest)proxyRequest);
CloseableHttpResponse proxyResponse = null;
int statusCode = 0;
try {
if (this.doLog) {
this.log("proxy " + method + " uri: " + servletRequest.getRequestURI() + " -- " + ((HttpRequest)proxyRequest).getRequestLine().getUri());
}
LOG.debug("proxy {} uri: {} -- {}", new Object[]{method, servletRequest.getRequestURI(), ((HttpRequest)proxyRequest).getRequestLine().getUri()});
proxyResponse = this.proxyClient.execute(URIUtils.extractHost(targetUriObj), (HttpRequest)proxyRequest);
statusCode = proxyResponse.getStatusLine().getStatusCode();
if (statusCode != 401 && statusCode != 403) {
if (this.doResponseRedirectOrNotModifiedLogic(servletRequest, servletResponse, proxyResponse, statusCode, targetUriObj)) {
return;
}
} else {
if (this.doLog) {
this.log("Authentication Failed on remote server " + proxyRequestUri);
}
LOG.debug("Authentication Failed on remote server {}", proxyRequestUri);
}
servletResponse.setStatus(statusCode, proxyResponse.getStatusLine().getReasonPhrase());
this.copyResponseHeaders(proxyResponse, servletResponse);
this.copyResponseEntity(proxyResponse, servletResponse);
} catch (Exception var26) {
if (proxyRequest instanceof AbortableHttpRequest) {
AbortableHttpRequest abortableHttpRequest = (AbortableHttpRequest)proxyRequest;
abortableHttpRequest.abort();
}
LOG.debug("Proxy to " + proxyRequestUri + " failed", var26);
if (!(var26 instanceof ConnectException) && !(var26 instanceof UnknownHostException)) {
if (var26 instanceof ServletException) {
servletResponse.sendError(502, var26.getMessage());
} else if (var26 instanceof SecurityException) {
servletResponse.setHeader("WWW-Authenticate", "Basic");
servletResponse.sendError(statusCode, var26.getMessage());
} else {
servletResponse.sendError(500, var26.getMessage());
}
} else {
servletResponse.setStatus(404);
}
} finally {
if (proxyResponse != null) {
EntityUtils.consumeQuietly(proxyResponse.getEntity());
try {
proxyResponse.close();
} catch (IOException var24) {
LOG.error("Error closing proxy client response: {}", var24.getMessage());
}
}
}
} else {
servletResponse.setStatus(404);
}
}
|
通过parseProxyAddress
函数获取 URL 地址,跟进:
![image-20240709190029613 image-20240709190029613](/svg/loading.min.svg)
通过getPathInfo()
方法进行同名方法调用,需要注意的是这里获取了Authorization
字段,只是获取了里面的username和password进行base64解码直接获取,没有进行身份验证
![image-20240709190058946 image-20240709190058946](/svg/loading.min.svg)
在ProxyDetails
方法中获取了/proxy/
路径后内容,并且进行相应的协议、路径、端口解析,需要注意,这里只能用http协议,否则在catch中会将端口80或443
拼接,最终请求错误
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
|
public ProxyDetails(String pathInfo) {
this.scheme = "http";
this.path = "";
this.port = 80;
this.hostAndPort = pathInfo.replace(" ", "%20");
if (this.hostAndPort != null) {
while(this.hostAndPort.startsWith("/")) {
this.hostAndPort = this.hostAndPort.substring(1);
}
if (this.hostAndPort.startsWith("http/")) {
this.scheme = "http";
this.hostAndPort = this.hostAndPort.substring(5);
} else if (this.hostAndPort.startsWith("https/")) {
this.scheme = "https";
this.hostAndPort = this.hostAndPort.substring(6);
}
int idx = this.hostAndPort.indexOf("@");
if (idx > 0) {
this.userName = this.hostAndPort.substring(0, idx);
this.hostAndPort = this.hostAndPort.substring(idx + 1);
idx = this.indexOf(this.userName, ":", "/");
if (idx > 0) {
this.password = this.userName.substring(idx + 1);
this.userName = this.userName.substring(0, idx);
}
}
this.host = this.hostAndPort;
int schemeIdx = this.indexOf(this.hostAndPort, "://");
if (schemeIdx > 0) {
this.scheme = this.hostAndPort.substring(0, schemeIdx);
this.hostAndPort = this.hostAndPort.substring(schemeIdx + 3);
} else {
schemeIdx = this.indexOf(this.hostAndPort, ":/");
if (schemeIdx > 0) {
this.scheme = this.hostAndPort.substring(0, schemeIdx);
this.hostAndPort = this.hostAndPort.substring(schemeIdx + 2);
}
}
idx = this.indexOf(this.hostAndPort, ":", "/");
if (idx > 0) {
this.host = this.hostAndPort.substring(0, idx);
String portText = this.hostAndPort.substring(idx + 1);
idx = portText.indexOf("/");
if (idx >= 0) {
this.path = portText.substring(idx);
portText = portText.substring(0, idx);
}
if (Strings.isNotBlank(portText)) {
try {
this.port = Integer.parseInt(portText);
this.hostAndPort = this.host + ":" + this.port;
} catch (NumberFormatException var6) {
this.port = "http".equals(this.scheme) ? 80 : 443;
this.path = "/" + portText + this.path;
this.hostAndPort = this.host;
}
} else {
this.hostAndPort = this.host;
}
}
this.stringProxyURL = this.scheme + "://" + this.hostAndPort + this.path;
if (LOG.isDebugEnabled()) {
LOG.debug("Proxying to " + this.stringProxyURL + " as user: " + this.userName);
}
}
}
|
返回主代码中,然后判断其是否为空,如果不为空,通过whitelist.isAllowed()
判断该 URL 是否在白名单里,跟进 whitelist
:
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
|
public ProxyWhitelist(String whitelistStr, boolean probeLocal) {
if (Strings.isBlank(whitelistStr)) {
this.whitelist = new CopyOnWriteArraySet();
this.regexWhitelist = Collections.emptyList();
} else {
this.whitelist = new CopyOnWriteArraySet(this.filterRegex(Strings.split(whitelistStr, ",")));
this.regexWhitelist = this.buildRegexWhitelist(Strings.split(whitelistStr, ","));
}
if (probeLocal) {
LOG.info("Probing local addresses ...");
this.initialiseWhitelist();
} else {
LOG.info("Probing local addresses disabled");
this.whitelist.add("localhost");
this.whitelist.add("127.0.0.1");
}
LOG.info("Initial proxy whitelist: {}", this.whitelist);
this.mBeanServer = ManagementFactory.getPlatformMBeanServer();
try {
this.fabricMBean = new ObjectName("io.fabric8:type=Fabric");
} catch (MalformedObjectNameException var4) {
throw new RuntimeException(var4);
}
}
|
判断 URL 是否为 localhost、127.0.0.1或者用户自己更新的白名单列表,如果不是返回 false。
返回到 service()
,向下走:
![image-20240709190751511 image-20240709190751511](/svg/loading.min.svg)
BasicHttpEntityEnclosingRequest()
拥有RequestLine
、HttpEntity
以及Header
,这里用的是 entity,HttpEntity即为消息体,包含了三种类型:数据流方式、自我包含方式以及封装模式(包含上述两种方式),这里就是一个基于HttpEntity的, HttpRequest接口实现,类似于urlConnection
。
所以 service()
的主要作用就是:获取请求,然后HttpService
把HttpClient
传来的请求通过向下转型成BasicHttpEntityEnclosingRequest
,再调用HttpEntity
,最终得到请求流内容。
虽然对传入的 URL 进行了限制,但是没有对端口、协议进行相应的限制,从而导致了 SSRF 漏洞。
修复方案
![image-20240709192618960 image-20240709192618960](/svg/loading.min.svg)
未经验证的用户禁止访问该页面。
参考
https://xz.aliyun.com/t/7186
https://hackerqwq.github.io/2021/11/10/javaweb%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E5%AD%A6%E4%B9%A0-SSRF/#%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-9827
https://github.com/hawtio/hawtio/compare/hawtio-2.5.0...hawtio-2.9.1
https://github.com/hawtio/hawtio/tree/hawtio-2.5.0/