On August 29th, 2023, Qlik issued a patch for two vulnerabilities we identified in Qlik Sense Enterprise, CVE-2023-41265 and CVE-2023-41266. These vulnerabilities allowed for unauthenticated remote code execution via path traversal and HTTP request tunneling. As part of our standard operating procedure, we performed a diff of the issued patch to identify potential bypasses for the fix for these vulnerabilities. Unfortunately, in this case, we identified a bypass for the original fix for CVE-2023-41265 which allowed for unauthenticated remote code execution even after applying the patches for CVE-2023-41265 and CVE-2023-41266.

Qlik has issued a second patch to address this workaround. The new patch implements a more robust filtering mechanism that is less prone to CL.TE and TE.CL request tunneling attacks. A new CVE, CVE-2023-48365, tracks this vulnerability. Praetorian worked with Qlik to analyze the patch for CVE-2023-Pending to harden the Proxy service and identify any potential additional workarounds. In this article, we provide technical detail regarding the bypass for the original fix for CVE-2023-41265 along with our analysis of the subsequent fix for CVE-2023-48365.

Summarizing the Original Fix for CVE-2023-41265

The original fix for CVE-2023-41265 attempts to address the CL.TE HTTP request tunneling vulnerability by removing the “Content-Length” header if both the “Content-Length” and “Transfer-Encoding” headers are specified and the “Transfer-Encoding” header is set to “chunked”. We will demonstrate how an attacker could bypass this by specifying non-exact values for the “Transfer-Encoding: chunked” header that wouldn’t exactly match the value “chunked”, but that the backend server would still interpret as being semantically equivalent to “Transfer-Encoding: chunked”.

Analyzing the Fix for CVE-2023-41265

To analyze what changed between the vulnerable and patched versions of Qlik Sense Enterprise, we performed patch diffing using the JustAssembly utility from Telerik (Progress Software). We identified several new classes that performed additional verification of headers within the “Proxy.Util” namespace. These classes were called HttpHeaderExtensions and HttpHeaderValidation. Figure 1 shows some of the newly added classes to the “Proxy.Util” namespace within Proxy.exe.

Figure 1: We observed several classes with names related to header validation that Qlik’s developers added to the patched version of the Proxy service.

The developers added a function named Validate as part of the new HttpHeaderValidation class. This function checked that both the Transfer-Encoding and the Content-Length headers were specified in the request and performed a call to IsValidTransferEncoding from the newly added HttpHeaderExtensions class. Figure 2 shows the source code for the Validate function.

Figure 2: The source code of the Validate function from the HttpHeaderValidation class that was added to the Proxy.Util namespace in the patched Proxy.exe binary.

If we examine the source code for the IsValidTransferEncoding function (see Figure 3) we observe that it reads the value of the Transfer-Encoding header and splits that value into an array of strings based on a comma-separated list of values. It then compares each of these values to the value “chunked” (see Figure 4) and returns a “false” result if any of the values exactly match the chunked keyword. Essentially, this function implements a denylist comparing Transfer-Encoding header values with a list of denied keywords.

Figure 3: The source code for the IsValidTransferEncoding function that was added to the HttpHeaderExtensions class in the Proxy.Util namespace within the patched Proxy.exe binary.

Figure 4: The list of encoding types that were considered invalid by the proxy service. This list included a single entry for the keyword chunked.

We also observed that the patch modified the IsAnonymousAccessAllowed function to only return “true” for HEAD and GET requests (see Figure 5). This prevents an attacker from sending a POST request to the backend service anonymously. However, the process still forwards the bodies of GET and HEAD requests to the backend (this will be important later).

Figure 5: We observed that the IsAnonymousAccessAllowed function was modified to only allow anonymous access if the HTTP request method corresponds to a HEAD or GET request.

Our Hypothesis

At this point, we hypothesized that we could craft a message that the backend will interpret as being chunked encoding, while the front-end proxy interprets it as not being associated with chunked encoding. We based our hypothesis on the exact comparison the Proxy service makes against the keyword “chunked”. This would cause the IsValidTransferEncoding function to return false and thus the Content-Length header would remain in the request. As a result, the Proxy service would then again forward the request using the Content-Length header and this would reintroduce the CL.TE request tunneling issue we identified previously as part of CVE-2023-41265.

Breaking the Original Fix

Examining Accepted Values for Transfer-Encoding Chunked

We connected directly to the backend repository service using Burp Repeater to perform testing on what values the backend would interpret as valid “chunked” encoding parameters, but which also would not match an exact case-insensitive string comparison for the keyword “chunked” performed by the proxy. We determined that when we submitted a value of ,\tchunked,” in the Transfer-Encoding header the Repository service would interpret this value as “Transfer-Encoding: chunked”. Figure 6 shows a request we sent directly to the Repository service running on port 4242/TCP. Figure 7 shows a response indicating that the request was processed using “Transfer-Encoding: chunked”.

Figure 6: A test payload sent directly to the Repository service to enumerate values that would be interpreted as chunked encoding within the Transfer-Encoding header while also not exactly matching the chunked keyword.

Figure 7: A response from the Repository service indicating that the smuggled request was processed with a value of “\tchunked” being interpreted as “chunked”.

We then leveraged a debugger to inspect the validation logic within the Proxy service and verified that it was comparing “\tchunked” directly to “chunked”. The check would fail due to these values not matching exactly. Figure 8 shows the comparison check in a call to the Equals function in the System.OrdinalComparer class.

Figure 8: A comparison of “chunked” to “\tchunked” would return false within the Proxy service as these values are not exactly equal even though the Repository service interprets them as being semantically equivalent.

Triggering the Request Smuggling Using a HEAD Request

In our previous article, we leveraged CVE-2023-41266 to perform path traversal and invoke an internal REST endpoint with a POST request that would normally require authentication. This allowed us to trigger request smuggling using a POST handler. However, subsequent testing also indicated CVE-2023-41265 could be exploited using a HEAD request, because the Proxy service would forward request bodies for HEAD and GET requests.

Figure 9 shows a HEAD request sent directly to the Repository service on port 4242/TCP. This request includes the obfuscated Transfer-Encoding header and the Content-Length header. We received two responses from the server.

Figure 9: The Repository service on port 4242/TCP processed the HEAD request containing an obfuscated request tunneling payload and returned two responses.

Interestingly, we observed that we could trigger processing of multiple requests over the same connection for HEAD requests, but sending a GET request resulted in the processing of only the first request. At this time, we have not identified why we observed this behavior. Figure 10 shows an example GET request containing the same payload as in Figure 9; however, the service closed the connection after processing only a single request.

Figure 10: We observed that leveraging a GET request instead of a HEAD request would always result in the Repository service processing the first request and ignoring the second smuggled request. We have not identified why the behavior differs between GET and HEAD requests in this scenario.

Adding a New Administrative User

In Video 1 we leverage the bypass for the fix for CVE-2023-41265 to add a new administrative user named Praetorian. As the exploit uses the same technique as we documented in our previous post to create a new administrative account, we don’t discuss it further here. The only differences in this case are as follows:

  1. We replace the value “chunked” in the Transfer-Encoding header with the value “\tchunked” to bypass the added header validation security mechanism.
  2. We leverage a HEAD request to “/resources/qmc/fonts/test.ttf” to perform request smuggling as an anonymous user instead of leveraging CVE-2023-41266 to invoke a REST endpoint within the Repository service using a POST request.

Video 1: Adding a new administrative user.

Achieving Remote Code Execution via External Tasks

In Video 2 we leverage the bypass for the fix for CVE-2023-41265 to perform remote code execution as an unauthenticated attacker. To do so, we use the external program task’s functionality to run arbitrary commands within Qlik Sense. Since we documented  the same technique in our previous post to create an external program task, we don’t discuss it further here. The only differences in this case are as follows:

  1. We replaced the value “chunked” in the Transfer-Encoding header with the value “\tchunked” to bypass the added header validation security mechanism.
  2. We leverage a HEAD request to “/resources/qmc/fonts/test.ttf” to perform request smuggling as an anonymous user instead of leveraging CVE-2023-41266 to invoke a REST endpoint within the Repository service using a POST request.

Video 2: Achieving RCE via external tasks.

Nuclei Detection Template for CVE-2023-Pending

We built a detection for CVE-2023-48365 based on differences in behavior we observed between patched and unpatched versions of the system. This detection attempts to perform request smuggling, but includes an invalid chunked encoding length which causes the backend system to return a distinct response when it processes the message.

However, after patching, the frontend proxy returns a different error message that doesn’t match the one we expect to receive when the system is vulnerable. Figure 11 shows an example request sent to test for the vulnerability and the expected response when the system is vulnerable. Figure 12 shows an example request/response pair for a patched system that is not vulnerable.

Users should note that this template could fail to detect vulnerable systems that are behind a load balancer, as this could in some cases break the request smuggling payload and cause the issue not to trigger properly for the expected error response. We have created a new repository, doubleqlik-detect, which contains a Nuclei template that leverages this behavior to detect unpatched instances of this vulnerability. We have also submitted a pull request to ProjectDiscovery so that they add this detection to the nuclei-templates repository.

Figure 11: We observed that when the backend system processed the message with an invalid chunked length it returned a “400 Bad Request” response and returned a Set-Cookie header to set a cookie named X-Qlik-Session.

Figure 12: We observed that when we attempted our exploit against a system patched against CVE-2023-Pending, that the system returned a different error message as the Proxy performs validation of the Transfer-Encoding header and returns a response indicated that the value “\tchunked” is unsupported.


In this article we discussed a bypass we found for the original fix for CVE-2023-41265. Praetorian urges impacted customers to update their Qlik Sense instances immediately to address this vulnerability. Additionally, it may be worth performing retroactive threat hunting leveraging the guidance we provided in a previous article to detect potential signs of previous exploitation of these vulnerabilities.

Remediating security vulnerabilities within software applications often can be difficult due to the large number of nuances and special edge cases that developers must consider when addressing a security vulnerability. This is particularly true with technically complex vulnerabilities such as HTTP request smuggling/tunneling that result from the interaction between different software stacks that may implement various aspects of the HTTP protocol specification differently.

Unfortunately, every application that you choose to expose to the Internet comes with some degree of risk as vulnerabilities within those applications could provide an attacker with a foothold into your network. Understanding your attack surface better and taking proactive steps to reduce exposures and better assess the impact of new vulnerabilities is a critical step of this process. If you’d like to know how the Chariot offensive security platform can help you stay one step ahead of attackers, please don’t hesitate to contact us for a demo.