Semicolon's relation with reverse proxy



  • What is the relation of semicolon with reverse proxy and how is it able to escape context/servlet mapping in reverse proxies such as IIS, NGINX, Apache?

    Like I've seen multiple times where people have reported the use of semicolon to escape context/servlet mapping in reverse proxies.

    For example- in a website 
    
        1. /health = 200 OK
        2. /admin/resource/ = 403 Forbidden
        3. /health/;admin/resource/ =  loads the path /admin/resource 200 OK 
        
    

    My question is why does this happen and what is the relation of semicolon with escaping the context/servlet mapping?



  • Java Servlet and JAX-WS have support for so-called Matrix Parameters: each component path can have a series of parameters.

    For example:

    /foo;param1=a;param2=b/bar;param3=c;param4=d
    

    In the servlet specification, these are referenced to as “path parameters”:

    Path parameters that are part of a GET request (as defined by HTTP 1.1) are not exposed by these APIs. They must be parsed from the String values returned by the getRequestURI method or the getPathInfo method.

    [...]

    The path used for mapping to a servlet is the request URL from the request object minus the context path and the path parameters.

    [...]

    The session ID must be encoded as a path parameter in the URL string. The name of the parameter must be jsessionid. Here is an example of a URL containing encoded path information:

    http://www.example.com/catalog/index.html;jsessionid=1234

    This usage is somewhat documented in the URI RFC3986:

    the semicolon (";") and equals ("=") reserved characters are often used to delimit parameters and parameter values applicable to that segment. [...] For example, one URI producer might use a segment such as "name;v=1.1" to indicate a reference to version 1.1 of "name",

    SpringWeb

    Let's try this basic Spring (Boot, Web) applications:

    package com.example.demo;
    
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.bind.annotation.PostMapping;
    
    @RestController
    public class DemoController {
        @PostMapping("/api/reboot")
        String newEmployee() {
            // TODO, do the actual reboot
            return "Rebooting";
        }
    }
    

    We can trigger the reboot() endpoint by either:

    curl -XPOST 'http://127.0.0.1:8080/api/reboot' 
    curl -XPOST 'http://127.0.0.1:8080/api/reboot;whatever' 
    curl -XPOST 'http://127.0.0.1:8080/api;whatever/reboot'
    curl -XPOST 'http://127.0.0.1:8080/api/;whateher/reboot'
    curl -XPOST 'http://127.0.0.1:8080/;/api/;/reboot'
    curl -XPOST 'http://127.0.0.1:8080/;/api/reboot'
    

    Now imagine that we have protected this /reboot endpoint in our reverse proxy. For example with nginx:

    location /api/ {
        auth_basic “Protected”;
        #...
    }
    

    This protects any resource starting the the "/api/" string using some basic authentication.

    However when using 'http://127.0.0.1:8080/api;whatever/reboot':

    • nginx does not apply the protection;
    • the Java Servlet interprets ;whatever as matrix parameters of the /api/reboot path and triggers the reboot() endpoint.

    This way we can bypass the protection implemented in the reverse proxy.

    In this example, we exploited a discrepancy between how the Java applications and the reverse proxy interprets URI to bypass a protection in the reverse proxy.

    In the same way a buggy Java authentication middleware (ServletFilter) could possibly be bypassed using such approach.

    Servlet API

    Let's look at how, Matrix parameter work with Servlet:

    package com.example.demo;
    
    import java.io.IOException;
    import java.io.PrintWriter;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.annotation.WebServlet;
    
    @WebServlet("/api/foo")
    public class DemoServlet extends HttpServlet {  
     
        @Override
        protected void doPost(
          HttpServletRequest request, 
          HttpServletResponse response) throws IOException {
            PrintWriter out = response.getWriter();
            out.print("contextPath=" + request.getContextPath() + '\n');
            out.print("requestURI=" + request.getRequestURI() + '\n');
            out.print("servletPath=" + request.getServletPath() + '\n');
            out.print("pathInfo=" + request.getPathInfo() + '\n');
        }
    
    }
    

    Let's test this servlet:

    $ curl -XPOST 'http://127.0.0.1:8080/api/foo'
    contextPath=
    requestURI=/api/foo
    servletPath=/api/foo
    pathInfo=null
    
    $ curl -XPOST 'http://127.0.0.1:8080/api;whatever/foo'
    contextPath=
    requestURI=/api;whatever/foo
    servletPath=/api/foo
    pathInfo=null
    
    $ curl -XPOST 'http://127.0.0.1:8080/api/foo;whatever'
    contextPath=
    requestURI=/api/foo;whatever
    servletPath=/api/foo
    pathInfo=null
    
    $ curl -XPOST 'http://127.0.0.1:8080/api/;/foo'
    contextPath=
    requestURI=/api/;/foo
    servletPath=/api/foo
    pathInfo=null
    
    $ curl -XPOST 'http://127.0.0.1:8080/;/api/foo'
    contextPath=
    requestURI=/;/api/foo
    servletPath=/api/foo
    pathInfo=nul
    

    A ServletFilter enforcing some authorization/authentication based .getRequestURI() could for example be bypassed using this technique.



Suggested Topics

  • 2
  • 2
  • 2
  • 2
  • 2
  • 2
  • 2
  • 2