During a recent Chariot customer pilot we identified an interesting method to bypass the cross-site scripting (XSS) filtering functionality within the Akamai Web Application Firewall (WAF) solution. Chariot had identified a Carriage Return and Line Feed (CRLF) injection vulnerability during an automated scan, and we discovered the bypass during our exploitation phase. In this article, our goals are to explain some of the risks associated with CRLF injection as well as discuss the technique we leveraged to bypass the Akamai WAF filtering.
Before We Get Started…
Please note that while we use example.com as the vulnerable application within this article, the site itself wasn’t actually vulnerable to CRLF injection. Instead, we have substituted example.com for the customer domain and redacted other potentially identifying information. This allows us to include details on technique without exposing any information that could be linked to the customer network environments. In some cases, we provide simulated screenshots to show what we would have observed when performing the attack, and have manually recreated the scenario using example.com to avoid exposing sensitive information.
We also do not intend for this article to single out Akamai in particular. Rather, we suspect that attackers could use similar bypass techniques against just about every web application firewall product on the market.
What is a CRLF injection vulnerability?
First, let’s discuss a bit more about what a CRLF injection vulnerability is and why it could be useful to an attacker. CRLF injection occurs when an application does not perform proper filtering and returns an HTTP response header that includes attacker-controlled user-input. This type of vulnerability allows an attacker to insert carriage return (abbreviated CR and often represented as ‘\r’) and line feed (abbreviated LF and often represented as ‘\n’) characters into the HTTP response body.
Figure 1 shows an example HTTP response in which the CRLF (‘\r\n’) character sequence both separates the HTTP headers and delineates between the HTTP headers and response body. In this case, we observe that the separator between the HTTP headers and body is simply a single line containing only the CRLF sequence.
Figure 1: An example HTTP response from example.com that uses a CRLF sequence delineated between HTTP headers and the response body.
This HTTP protocol means that an attacker who can inject CRLF characters into the HTTP response body can in many cases end up controlling both HTTP headers and data within the HTTP response body. This issue can then lead to other vulnerabilities such as cross-site scripting and open redirection vulnerabilities. Additionally, other attack vectors can become viable, such as session fixation attacks if the attacker can inject a Set-Cookie header within a HTTP response the victim system returns.
One of the most common root-causes of CRLF injection is when an HTTP header within a server’s response body reflects attacker-controlled input from an URL in a HTTP request.
What did Chariot discover?
In this case, Chariot performed testing for CRLF injection vulnerabilities using the Nuclei scanning utility. Figure 2 shows an example request in the finding Chariot generated.
Figure 2: An example CRLF injection vulnerability Chariot discovered automatically. Praetorian engineers manually adjusted the risk rating from low-risk to high-risk.
In this case, we observe that a HTTP request was sent to the victim server example.com running a HTTPS server on port 443/TCP. At this point, our managed services team performed manual verification of the issue before escalating to the customer.
We began by confirming the existence of the CRLF injection vulnerability. We used the URL https://example.com/%0ASet-Cookie:test=test to trigger the CRLF injection vulnerability. We included a %0A character to represent a newline character that reflects in the HTTP response the client received. It allowed us to inject a Set-Cookie header into the HTTP response, as Figure 3 shows. The “vulnerable header” field reflects the attacker-controlled user input.
Figure 3: An example where CRLF injection allows us to inject a Set-Cookie header into the HTTP response body.
We observe two things of particular note from this response. The first is that the injected Set-Cookie header is not located directly after the vulnerable header despite our expectation that it would be after we injected a new-line after the vulnerable header. The second observation is that while we injected only a newline character (‘\n’) after the vulnerable-header header, the response clearly contained a ‘\r\n’ character sequence (both a carriage return and line feed) after the vulnerable-header header.
Ultimately, this gets into the complex nature of modern web applications where an application must route multiple levels of requests. For example, imagine a scenario where we deploy a service within Kubernetes using Istio as a service mesh, exposed externally through a cloud-based load balancer, hosted behind an external web application firewall service, served through a content delivery network such as Akamai. In such a scenario, an external request made by a client could take the following path to hit an internal application or service running in Kubernetes:
- From client browser to https://example.com/
- Through content delivery network
- Web application firewall examination
- To external load balancer
- To internal Kubernetes load balancer
- To Istio sidecar running on pod in Kubernetes cluster
- Ending at the application service running on the Kubernetes pod
Each step presents an opportunity for an intermediary system to transform the ultimate HTTP response, which explains why headers can be reordered. Many systems also include flexible parsing logic to make them capable of handling responses that differ slightly from the RFC, so an intermediate system’s formal parsing also could transform the “\n” injection to the “\r\n” injection we see in the example.
A reader might wonder why we didn’t include ‘\r\n’ so that our payload is compliant with the specification. Figure 4 demonstrates that the Akamai WAF actually blocks the ‘\r\n’ sequence when specified in a URL (in this case the URL encoded version of ‘\r\n’ is ‘%0d%0a’). Simply specifying ‘\n’ bypasses the filter.
Figure 4: Chariot automatically bypassed the Akamai WAF when testing for CRLF injection by specifying both ‘\n’ and ‘\r\n’ as potential CRLF injection payloads. This allowed Chariot to bypass Akamai’s rule to detect testing for CRLF injection issues.
An Initial Exploitation Attempt
After verifying the presence of the CRLF injection issue we attempted to exploit the vulnerability in a naive way by simply injecting the standard <script>alert(“XSS”)</script> payload into the response body. However, the Akamai WAF blocked this attempt based on the script payload tag within the GET parameter sent to the server. Figure 5 shows the response we received from Akamai as a result.
Figure 5: An example response received from the Akamai WAF solution when we attempted to trigger a cross-site scripting issue using CRLF injection.
At this point we attempted several different common cross-site scripting payloads using standard Akamai WAF bypass techniques. We largely focused on modifying the cross-site scripting payload injected into the HTTP response body. This is the standard approach for attempting to bypass a WAF when exploiting a reflected cross-site scripting vulnerability.
Researching the Content-Encoding Header
One simple option to bypass the web application firewall would be to continually modify our cross-site scripting payload until it bypasses the firewall’s signatures. However, we decided instead to bypass the WAF using the header injection primitive the CRLF injection vulnerability provided.
We hypothesized that we could inject a header using CRLF injection to specify that the HTTP response body was compressed. Next, we would compress the HTTP response payload injected into the response body. In this case, we didn’t need to modify the core XSS payload as we are simply relying on the compression to bypass the WAF rules used to inspect the GET malicious parameters specified in the request the application sends.
Our research indicated that the Content-Encoding header supported several different compression algorithms such as the gzip, compress, deflate, and br algorithms (see Figure 6 and Figure 7 ).
Figure 6: An initial review of the Mozilla HTTP documentation revealed four different supported compression algorithms.
Figure 7: A section from the Mozilla documentation describing the four compression algorithms listed in Figure 6.
Bypassing WAF Filtering Using Response Compression
Somewhat arbitrarily, we picked the deflate algorithm and injected a Content-Encoding header specifying it. We then injected a malicious compressed cross-site scripting payload that decompressed to <script>alert(“XSS”)</script> as shown in Figure 8.
Figure 8: A HTTP response containing compressed data and residual header data from the split HTTP response.
Unfortunately, we can see that, while our compressed data is present, the response body also contains some headers from the modified request since we injected a new header and response body into an existing HTTP request. This is because we essentially specified that everything past a specific injection point should be considered as part of the HTTP request body, which included any headers that came after the injection point. Fortunately, the simple solution to this is to inject a malicious Content-Length header with which we can specify that only the compressed data should be interpreted as being part of the request body.
Adding a Content-Length Header to the Payload
In this case, the compressed version of our cross-site scripting payload is twenty-six bytes, so we injected a Content-Length header specifying a length of twenty-six bytes. This means that everything coming after our compressed data is ignored, including the headers mentioned previously. Thus, the response body can be properly decompressed and rendered. Figure 9 shows the updated payload with the injected headers now including a Content-Length header and the compressed data in the response body.
Figure 9: An updated payload including a Content-Length header so only the compressed data containing the XSS payload is considered to be part of the response body.
Figure 10 shows an example of what the executed payload would have looked like when triggered while targeting the victim system. Please note that example.com is simply being used as a placeholder for the targeted application so as to avoid including information on the client domain.
Figure 10: An example screenshot showing what would have been observed on the targeted site. Please note example.com wasn’t impacted by a CRLF injection issue; this is just an example screenshot showing what execution of the payload looked like.
How is this different from a traditional bypass technique?
You may be wondering how the technique we leverage in this article is different from other previously published bypass techniques that aim to bypass the cross-site scripting (XSS) functionality within the Akamai WAF.
Ordinarily, we wouldn’t write about a web application firewall bypass if it just used a standard bypass technique. However, the combination of chaining a CRLF injection issue to trigger cross-site scripting and bypassing a web application firewall by injecting compressed data into the response body is a novel approach. We are not aware of another article that discusses this method.
In this article, we discussed the basic theory behind CRLF injection vulnerabilities and then moved into discussing web application firewall bypass techniques we leveraged during a customer pilot to demonstrate the risk of CRLF injection by leveraging these issues to perform cross-site scripting.