Cisco BroadWorks CommPilot Application Software Unauthenticated Server-Side Request Forgery (CVE-2022-20951)
Summary
The Cisco BroadWorks CommPilot Application exposes a servlet that allows the application to be used as an HTTP proxy server. The lack of validation of the the target URL and the lack of authentication protection allows an unauthenticated attacker to achieve a full-read SSRF.
Product Description (from vendor)
“Cisco BroadWorks is an enterprise-grade calling and collaboration platform delivering unmatched performance, security and scale.”
For more information visit https://www.cisco.com/c/en/us/products/unified-communications/broadworks/index.html.
CVE(s)
Details
Root Cause Analysis
The application implements the HttpProxyServlet
servlet which is meant to allow a user to specify a URL, which is then fetched by the server.
The servlet accepts a target
parameter which contains an URL with HTTP, HTTPS, or FTP as schema, then performs a request to such URL, and finally returns the output in the response.
As a method, the servlet accepts the following methods and uses the same one to perform its request to the target URL:
- GET (if the
deleteAfterDownload
parameter is appropriately populated then the request is converted to DELETE) - POST
- HEAD
Lines from 144 to 157 verify if the HTTP request is authenticated. Unfortunately for Cisco, the check only verifies that a session exists, instead of checking the role or the authentication state of it. Therefore, it is possible to visit the /Login
endpoint to forge a valid session (i.e. getting a fresh JSESSIONID
) with a webClientSession
attribute (line 152).
This results in an authentication bypass since no further checks are in place.
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
| private void doWork(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException {
try {
String string;
this.logInput(httpServletRequest);
HttpSession httpSession = httpServletRequest.getSession(false);
if (httpSession == null) {
httpServletResponse.sendRedirect("/Login");
return;
}
WebClientSession webClientSession = null;
HttpSession httpSession2 = httpSession;
synchronized (httpSession2) {
webClientSession = (WebClientSession)httpSession.getAttribute("webClientSession");
if (webClientSession == null) {
httpServletResponse.sendRedirect("/Login");
return;
}
}
|
The code gets the parameter target
from the request and stores it in the string
variable.
At line 184, the string
variable is stored in the string2
one to be used as a parameter for a new URI object instance at line 186 and stored in the uRI
variable.
178
179
180
181
182
183
184
185
186
| if ((string = httpServletRequest.getParameter("target")) == null || string.equals("")) {
HttpServletUtility.handleError(httpServletRequest, httpServletResponse, 400, "Bad request. Unable to retrieve target URL parameter.", LOG_NAME, false);
return;
}
String string2 = webClientSession.getProxyUri(string);
if (string2 == null) {
string2 = string;
}
URI uRI = new URI(string2);
|
The URL domain is resolved, and an array of inetAddress
is created.
186
187
188
189
190
191
192
193
| URI uRI = new URI(string2);
InetAddress[] inetAddressArray = null;
try {
inetAddressArray = NameServiceFactory.getInstance().lookupAll(NetworkAddress.toNormalizedString((String)uRI.getHost()));
}
catch (NameServiceException nameServiceException) {
inetAddressArray = new InetAddress[]{};
}
|
At line 205, an URI object is created using the information obtained from the uRI
object previously initiated.
Then, based on the request protocol and method, the request is built.
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
| for (InetAddress inetAddress : inetAddressArray) {
Object object;
try {
Appendable appendable;
Object object2;
uRI = new URI(uRI.getScheme(), uRI.getUserInfo(), inetAddress.getHostAddress(), uRI.getPort(), uRI.getPath(), uRI.getQuery(), uRI.getFragment());
if (bl && ((String)(object2 = uRI.toURL().toString())).length() > ((String)object2).lastIndexOf("/")) {
object2 = ((String)object2).substring(((String)object2).lastIndexOf("/") + 1);
httpServletResponse.setHeader("Content-Disposition", "attachment; filename=\"" + (String)object2 + "\"");
}
object2 = httpServletResponse.getOutputStream();
if (uRI.getScheme().toLowerCase().startsWith("ftp")) {
if (!this.proxyFtp(httpServletRequest, httpServletResponse, uRI)) continue;
break;
}
if ("GET".equalsIgnoreCase(httpServletRequest.getMethod())) {
getMethod = new GetMethod(uRI.toURL().toString());
} else if ("POST".equalsIgnoreCase(httpServletRequest.getMethod())) {
getMethod = new PostMethod(uRI.toURL().toString());
}
getMethod.setFollowRedirects(true);
HttpServletUtility.copyHttpHeadersClientToRequest(httpServletRequest, (HttpMethod)getMethod, LOG_NAME, HEADER_FILTER_CLIENT_TO_SERVER);
HttpServletUtility.configureUsernamePassword(this.httpClient, uRI.getUserInfo(), uRI.getPort());
n = this.httpClient.executeMethod((HttpMethod)getMethod);
HttpServletUtility.copyHttpHeadersResponseToClient(httpServletResponse, (HttpMethod)getMethod, HEADER_FILTER_SERVER_TO_CLIENT);
inputStream = getMethod.getResponseBodyAsStream();
if (PATCH_CHUNKED_GET_ZERO_BYTE) {
HttpServletUtility.patchEmptyChunkedTransfer(getMethod.getResponseHeader("Transfer-Encoding"), inputStream, (OutputStream)object2);
}
|
From line 220 to 228, the code issues the HTTP request after copying all the headers from the received one (httpServletRequest
). Notice that the body is handled as part of the headers and, therefore, copied.
Then the response to the request issued by the proxy function is stored inside the inputStream
variable.
Based on the proxy request response code, the response is built in the following code snippet.
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
| try {
if (n == 200 || n == 201 || n == 204 || n == 100) {
StreamUtil.copy((InputStream)inputStream, (OutputStream)object2, (int)STREAMING_BUFFER_SIZE_BYTES, (boolean)false);
this.logOutput(httpServletRequest, "Proxying successfull");
} else {
object = getMethod.getResponseBodyAsString();
appendable = new StringBuilder();
((StringBuilder)appendable).append("Download from the MeetMeConferencingRepository failed with an error code of: ");
int n2 = getMethod.getStatusCode();
((StringBuilder)appendable).append(n2);
if (n2 == 304) {
((StringBuilder)appendable).append(". File not modified, nothing is sent");
} else {
((StringBuilder)appendable).append(" and the response body was: ");
((StringBuilder)appendable).append((String)(object == null ? "null" : object));
}
this.logOutput(httpServletRequest, ((StringBuilder)appendable).toString());
}
}
|
BONUS:
If both the parameters download
and deleteAfterDownload
are set, then the servlet will issue a DELETE
HTTP request.
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
| boolean bl = false;
try {
if (httpServletRequest.getParameter("download") != null) {
bl = Boolean.parseBoolean(httpServletRequest.getParameter("download"));
}
}
catch (Exception exception) {
bl = false;
}
boolean bl2 = false;
if (bl) {
try {
if (httpServletRequest.getParameter("deleteAfterDownload") != null) {
bl2 = Boolean.parseBoolean(httpServletRequest.getParameter("deleteAfterDownload"));
}
}
catch (Exception exception) {
bl2 = false;
}
}
|
At line 171, bl2
is populated, and later used at line 253, to choose if issue the request with DELETE
method.
Notice that this code path is taken after the initial GET, POST, or HEAD request has been alredy executed.
253
254
255
256
257
258
259
260
261
262
263
264
| if (!bl2) break;
deleteMethod = new DeleteMethod(uRI.toURL().toString());
HttpServletUtility.copyHttpHeadersClientToRequest(httpServletRequest, (HttpMethod)deleteMethod, LOG_NAME, HEADER_FILTER_CLIENT_TO_SERVER);
n = this.httpClient.executeMethod((HttpMethod)deleteMethod);
deleteMethod.getResponseBody();
deleteMethod.abort();
deleteMethod.releaseConnection();
deleteMethod = null;
if (n == 204) break;
httpServletResponse.setStatus(n);
this.logOutput(httpServletRequest, "Proxy DELETE failed on " + uRI.toURL().toString() + " with http return code " + n);
break;
|
Proof of Concept
- Visit the login page without logging in (to obtain a valid
JSESSIONID
) - Visit the following URL, after replacing the
<domain>
placeholder with the domain or ip of the application, to force the server into performing a GET request to http://localhost/rewrite-status
and obtain the response: https://<domain>/servlet/HttpProxy?download=false&target=http://localhost/rewrite-status
- Notice that the output of the request done by the server is reflected in the response
Impact
An attacker without a valid user could force the server into performing HTTP, HTTPS, and FTP request and obtain their responses.
Upgrade Cisco BroadWorks CommPilot Application to CommPilot-23 version 2022.10_1.313 or CommPilot-24 version 2022.10_1.313 or CommPilot-25 version 2022.10_1.313 or higher.
Official reference: https://www.cisco.com/c/en/us/support/docs/csa/cisco-sa-broadworks-ssrf-BJeQfpp.html
Disclosure Timeline
This report was subject to Shielder’s disclosure policy:
- 09/09/2022: Shielder’s team detects the vulnerability during a Security Assessment for one of its customers
- 09/09/2022: Shielder’s customer opens a ticket to Cisco
- XX/09/2022: Cisco acknowledges the security issue
- XX/09/2022: Cisco releases a patch for Shielder’s customer
- 02/11/2022: Cisco advisory is made public (https://www.cisco.com/c/en/us/support/docs/csa/cisco-sa-broadworks-ssrf-BJeQfpp.html)
- 21/12/2022: Shielder’s advisory is made public
Credits
This advisory was first published on https://www.shielder.com/advisories/cisco-broadworks-commpilot-ssrf/