Skip to content

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-part
  • proxy_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's Host 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: or AnyHeader :)
  • 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's Cache-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)
  • 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));
        }