Reverse Proxy
Reverse Proxy¶
URL:
http://example.com/foo;name=orange/bar/
GET //test/../%2e%2e%2f<>.JpG?a1=”&?z#/admin/
Sources:
- https://www.acunetix.com/blog/articles/a-fresh-look-on-reverse-proxy-related-attacks/
- https://github.com/GrrrDog/weird_proxies
Nginx¶
- case-sensitive for verb (400 error)
- doesn't treat // as a directory (
/images/1.jpg/..//../1.jpg
->/1.jpg
) - doesn't allow in the path:
%00 0x00 %
- doesn't allow
%2f
as the first slash - doesn't path normalize
/..
- doesn't allow underscore (
_
) in header name (doesn't forward it)
Fingerprint¶
400 error:
<html>
<head><title>400 Bad Request</title></head>
<body bgcolor="white">
<center><h1>400 Bad Request</h1></center>
<hr><center>nginx/1.15.3</center>
</body>
</html>
403 error:
<html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx</center>
</body>
</html>
Absolute-URI¶
- supports Absolute-URI with higher priority under host header
- any scheme in Absolute-URI
- doesn't allow
@
in Absolute-URI (400 error)
Location match rules¶
(none)
: If no modifiers are present, the location is interpreted as a prefix match. This means that the location given will be matched against the beginning of the request URI to determine a match.=
: If an equal sign is used, this block will be considered a match if the request URI exactly matches the location given.~
: If a tilde modifier is present, this location will be interpreted as a case-sensitive regular expression match.~*
: If a tilde and asterisk modifier is used, the location block will be interpreted as a case-insensitive regular expression match.^~
: If a carat and tilde modifier is present, and if this block is selected as the best non-regular expression match, regular expression matching will not take place.
Understanding nginx server and location block selection algorithms
proxy_pass¶
- backend (URL to origin) is uncontrollable
- parses, url-decodes, normalizes, finds location
- cut off
#fragment
- doesn't normalize
/..
- // -> /
- if trailing slash is in proxy_pass(
proxy_pass http://backend/
), it forwards the processed request(path) %01-%FF
in path ->!"$&'()*+,-./:;<=>@[\]^_`{|}~
,0-9
,a-Z
,%23 %25 %3F
,%01-20
, =>%7F
%2f
to/
, which useful for%2f..
<> ' "
- useful for xss
- if no trailing slash is in proxy_pass (
proxy_pass http://backend
), it forwards the initial request(path) /!"$&'()*+,-./:;<=>@[\]^_`{|}~?a#z
->/!"$&'()*+,-./:;<=>@[\]^_`{|}~?a#z
%01-%FF
->%01-%FF
proxy_pass http://$host/
(with ending/
) doesn't proxy path-partproxy_pass http://192.168.78.111:9999 -> http://192.168.78.111:9999/path_from_location/
- forwards raw bytes (
0x01-0x20
, >0x80
) in path as-is - set HTTP/1.0 by default
$host
- from the request'sHost
header ;$http_host
- host from config (default)- allows >1
Host
header - forwards only the first one
- doesn't forward headers with space symbols in name (
AnyHeader:
orAnyHeader :
) - no additional headers to backend
rewrite¶
- similar to proxy_pass with trailing slash
%0a
cuts the path/rewrite_slash/123%0a456?a=b
->/rewrite_slash/123?a=b
location /rewrite_slash/ { rewrite /rewrite_slash/(.*) /$1 break; proxy_pass http://backend:9999/; }
Caching¶
- Nginx only caches GET and HEAD requests
- It respects the Cache-Control and Expires headers from origin server
- It does not cache responses with Cache-Control set to Private, No-Cache, or No-Store or with Set-Cookie in the response header.
- Does not honor the
Pragma
and the client'sCache-Control
- Doesn't care about
Vary
header - key for cache: host header and path+query
#
- is ordinary symbol here
Caching detections:
- X-Cache-Status: MISS - custom header which shows caching
- If caching is enabled, the header fields “If-Modified-Since”, “If-Unmodified-Since”, “If-None-Match”, “If-Match”, “Range”, and “If-Range” from the original request are not passed to the origin server.
- doesn't care If-Match for uncached content
- cares If-Match for cached content:
- W/"0815" - returns 412 Precondition Failed
- If-Match: * returns body
- doesn't care Range headers
Vulnerable configs¶
- one level traversal
/host_noslash_path../something/
->/lala/../something/
location /host_noslash_path { proxy_pass http://192.168.78.111:9999/lala/; }
- no first /
/without/slash/here
->GET without/slash/here HTTP/1.1
- (absolute uri?)
rewrite /(.*) $1 break;
- other examples
- https://github.com/yandex/gixy
Apache¶
- case-sensitive for verb (
get != GET
) - insensitive with PHP
- treats // as a directory (
/images/1.jpg/..//../2.jpg
->/images/2.jpg
) - doesn't allow in path:
# % %00
- doesn't allow
%2f
in path (default config:AllowEncodedSlashes Off
) - %2f is always 404 (
/%2f/../index.php/
or/index.php/%2f
) - can be the forward-proxy
- support this request (points to root)
GET ? HTTP/1.1
- cares about cache check headers (If-Range/Match/*)
- doesn't care in case of PHP
- If-Range + Range -> returns part of content only if If-Range correct
- No
Accept-Ranges: bytes
in case of php - doesn't allow underscore (
_
) in headers (skips)
Fingerprinting¶
400 error:
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>400 Bad Request</title>
</head><body>
<h1>Bad Request</h1>
<p>Your browser sent a request that this server could not understand.<br />
</p>
</body></html>
403 error:
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>403 Forbidden</title>
</head><body>
<h1>Forbidden</h1>
<p>You don't have permission to access /
on this server.<br />
</p>
</body></html>
Absolute-URI:
- supports Absolute-URI with higher priority than host header
- any scheme in Absolute-URI
- doesn't like @ in Absolute-URI (400 error)
Location match rules (the same works for ProxyPass)¶
http://httpd.apache.org/docs/current/mod/core.html#location
- Every type is case-sensitive
- <Directory>
Is used to enclose a group of directives that will apply only to the named directory, sub-directories of that directory, and the files within the respective directories. Any directive that is allowed in a directory context may be used. Directory-path is either the full path to a directory, or a wild-card string using Unix shell-style matching. In a wild-card string, ? matches any single character, and * matches any sequences of characters. You may also use [] character ranges.
<Location "/private1">
The specified location matches exactly the path component of the URL.
The specified location, which ends in a forward slash, is a prefix of the path component of the URL (treated as a context root). The specified location, with the addition of a trailing slash, is a prefix of the path component of the URL (also treated as a context root)./private1
->/private1
,/private1/
and/private1/file.txt
/private2/
->/private2/
,/private2/file.txt
- The
<LocationMatch>
directive and the regex version of<Location>
require you to explicitly specify multiple slashes if that is your intention. <LocationMatch>
==<Location ~ "/(extra|special)/data">
- Location with support of RegExps
<LocationMatch "^/abc">
would match the request URL/abc
but not the request URL//abc
. The (non-regex)<Location>
directive behaves similarly when used for proxy requests. But when (non-regex)<Location>
is used for non-proxy requests it will implicitly match multiple slashes with a single slash. For example, if you specify<Location "/abc/def">
and the request is to/abc//def
then it will match.- FilesMatch and Files to set rules for extensions, but works for only inside current location (
<FilesMatch \.php$>
in virt host ->/test.php
- OK,/anything/test.php
- no)
ProxyPass¶
- backend (URL to origin) is controllable
- doesn't care about the verb
- parses, url-decodes, normalizes, finds location, url-encodes
- /.. - > /../
- // -> // (except the first / symbol)
//path
->/path
/path//
->/path//
!"$&'()*+,-./:;<=>@[\]^_`{|}~
-> rev proxy ->!%22$&'()*+,-./:;%3C=%3E@%5B%5C%5D%5E_%60%7B%7C%7D~
%01-%FF
in path ->!$&'()*+,-.:;=@_~
, 0-9, a-Z, others are URL-encoded- doesn't allow >1
Host
header - doesn't forward with trailing space
AnyHeader :
- support line folding for headers (
Header:zzz
-> it is concatenated with the previous header) - doesn't forward
Host
, sets value from ProxyPass - adds headers to request to origin:
X-Forwarded-For: , X-Forwarded-Host: , X-Forwarded-Server:
- we can send our values in request and it will be added to proxy's request (
examplezzz.com, example2.com
) - adds Content-Type depending on extension, if there is no CT from origin server
Rewrite¶
- flags https://httpd.apache.org/docs/2.4/rewrite/flags.html
- similar to ProxyPas, but:
- url decodes values, flag B encodes them again
- url decodes, normalizes, then put in url and parse it
%0a
cuts the path/lala/123%0a456?a=b
->/lala/123?a=b
%01-%FF
in path ->!$&'()*+,-.:;=?@[\]^_`{|}~
, a-Z, 0-9, >0x7F, others are URL encoded%3f
decoded to?
, but%3faa=1?bb=2 -> ?aa=1
- inside (.*),
/lala/path/%2e%2e -> /path/..
(it's not normalized, but/path/%2e%2e/
- is)
!"$&'()*+,-./:;<=>@[\]^_`{|}~
-> rev proxy ->!%22$&'()*+,-./:;%3C=%3E@%5B%5C%5D%5E_%60%7B%7C%7D~
<VirtualHost *:80> ServerName example1.com RewriteEngine On RewriteRule /lala/(.*) http://192.168.78.111:9999/$1 [P,L] </VirtualHost>
Vulnerable configs¶
- multiple / bypass
http://lab.io:8080/asdasd/..///../neighborhood/a/feed -> //neighborhood/a/feed
RewriteEngine On RewriteCond %{REQUEST_URI} ^/neighborhood/[^/]+/feed$ [NC] RewriteRule ^.*$ - [F,L]
- No ending slash SSRF (incorrect config)
/@evil.com/index.php
/.evil.com/index.php
<VirtualHost *:80> ServerName example0.com ProxyPass / http://192.168.78.111 </VirtualHost>
Caching¶
not tested
HProxy and Nuster¶
Basics¶
- case-insensitive for verb
- allows any path/query values (except 0x00-0x20, >0x80):
GET !i?lala=#anything HTTP/1.1
- doesn't url-decode and normalize the path before applying rules
- support converters:
- url_dec - url decodes (but sends undecoded to origin server), but spoils path_begin
- path_* extracts the path, which starts at the first slash and ends before the first question mark
- allows >1
Host:
- forwards all of them
- doesn't forward
AnyHeader :
- 400 error - support line folding for headers (
Header:zzz
-> concatenate with previous header) - no additional headers to backend
Fingerprint¶
- no special headers
400 error:
<html><body><h1>400 Bad request</h1>
Your browser sent an invalid request.
</body></html>
403 error:
<html><body><h1>403 Forbidden</h1>
Request forbidden by administrative rules.
</body></html>
Absolute-URI¶
- doesn't support (parse) Absolute-URI
- forwards it as is
GET http://backend.com/q?name=X&type=Y HTTP/1.1
->GET http://backend.com/q?name=X&type=Y HTTP/1.1
Caching¶
Cache's been partly implemented in this version of HAproxy. It was not tested. Nuster was tested instead
- default key of CACHE: method.scheme.host.uri
- default key of NoSQL: GET.scheme.host.uri
http://www.example.com/q?name=X&type=Y -> GET.http.www.example.com./q?name=X&type=Y
- only 200 response is cached
- doesn't respect Cache-Control, Expire headers from the origin
- Does not honor the Pragma and the client's Cache-Control
Vulnerable configs¶
- Bypass
//admin/
/Admin/
/%61dmin/
acl restricted_page path_beg /admin
- Bypass
/log/
- any trailing symbol (e.g. /)
acl restricted_page path_beg,url_dec /log
Varnish¶
Basics¶
- backend (URL to origin) is uncontrollable
- allows any value for the verb
- allows any path/query values (except 0x00-0x20): GET !i?lala=#anything HTTP/1.1
- doesn't normalize, url-decode request before applying rules
- doesn't forward
AnyHeader :
- 400 - support line folding for headers (
Header:zzz
-> concatenate with previous header) - doesn't allow >1
Host:
- forwards
Host:
header - adds
X-Forwarded-For:
to req to the origin server- we can send our values in request and it will be added to proxy's request (
examplezzz.com, example2.com
)
- we can send our values in request and it will be added to proxy's request (
- req includes query string (no path part)
req.url ~ "\.jpg$" == ?random=.jpg
Fingerprint¶
Via: 1.1 varnish (Varnish/5.0)
X-Varnish: 7
X-Varnish-Host: ip-address-here
X-Varnish-Backend: ip address
X-Varnish-Esi-Method
Accept-Range: bytes
(for all requests)- 400 error:
HTTP/1.1 400 Bad Request
Absolute-URI¶
- support Absolute-URI with higher priority under host header
- "http" only in Absolute-URI
Caching¶
- it caches GET and HEAD requests
- key for cache: host header and uri
- doesn't cache reqs with cookie(!) or Authorization header, or Set-Cookie (default)
- often practice to cut all cookie headers before sending to origin
- It respects the Cache-Control and Expires headers from origin server (depending on version)
- it respects CC flags
- it cares about the max-age parameter and uses it to calculate the TTL for an object.
- it ignores "Cache-Control: no-cache" by default, but cares about "max-age" (Before V4.0.0?)
- Does not honor the Pragma and the client's Cache-Control
- doesn't care about Vary, by default
Caching detection¶
- X-Varnish: has two figures in case of hit, and one in case of miss. Age is also changed (0 -> \d+ )
X-Varnish: 65563 29 X-Varnish: 65563
Age: 0
- doesn't care about If-* headers
- support Range header
- If-Range + Range -> returns part of content only (always)
Vulnerable configs¶
- Misrouting
/../admin/
if (req.http.host == "sport.example.com") { set req.http.host = "example.com"; set req.url = "/sport" + req.url; }
- Blacklist bypass
Post //wp-login.php HTTP/1.1
if(req.method == "POST" || req.url ~ "^/wp-login.php$" || req.url ~ "^/wp-admin") { return(synth(503)); }