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

  1. Visit the login page without logging in (to obtain a valid JSESSIONID)
  2. 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
  3. 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.

Remediation

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/it/advisories/cisco-broadworks-commpilot-ssrf/

Data

21 dicembre 2022