目录

学习evilpan研究url解析

目录

这篇文章是写shiro代码的中间梳理的,因为我觉得,单从shiro的角度来挖掘url解析流程,不够整体化,再加之看到evilpan写的文章,于是想要复现一下他学习的思路。就是自己写文章,然后卡主了看一下。 浅谈 URL 解析与鉴权中的陷阱 URL 解析与鉴权中的陷阱 —— Spring 篇 我们的目标是贯穿整个解析流程,并且将每个类细致的解释清楚。

本文介绍了使用 URL 进行鉴权的一类威胁模型,并以两个符合标准的 Servlet 容器 Tomcat 和 Resin 为例介绍了二者的路由查找方法,根据路由查找的过程提出了一系列可能的 URL 变异方式;然后对几个现实中的鉴权案例进行分析,包括某典型应用手搓的鉴权代码以及成熟的鉴权方案 Shiro,其中都存在或者出现过鉴权绕过的场景,从中我们可以加深对 URL 鉴权的理解,从而写出更加健壮和安全的代码。 另外,本文只是介绍了 Servlet 标准应用的路由特性,而现代 Java Web 应用中更多是基于 Spring 生态的全家桶方案,Spring MVC 包揽了 Web 容器的所有路由并使用自身的寻址实现,且配合 Spring Security 也形成一套 URL 鉴权方案。这部分限于篇幅原因并未在本文中提及,后续有机会的话会另起一篇文章进行介绍。

Servlet容器

Tomcat

什么是Servlet[1]?什么又是Servlet容器?Servlet是JavaEE框架的一个组件,用于web开发。Servlet是运行在容器之中的基本的java应用程序。Servlet的职责是用于接受请求,处理,并且发送一个响应。container先调用servlet的init方法将其实例化并注册到container。servlet初始化之后,容器将请求重定向到其service方法,然后再逐步委托给恰当的doGet或者doPost方法。 关于Container,最开始会创建ServletContext,用于充当server或者Container的内存,他会记住所有servlet,filter和listener,其生命周期和Container相同。 当访问浏览器时,容器会创建HttpServletRequest/HttpServletResponse,其中携带其他信息,比如headers,parameters和请求体,我们可以在doXXX中设置Response。 请求结束之后Request被垃圾回收,HttpSession用来维持客户端请求和连接,容器创建用户会话(session),返回一个JSESSIONID存储到用户cookie中,作为用户的唯一标识。 我们可以通过将变量放在ServletContext来实现共享数据。 一个标准的Servlet接口如下

public interface Servlet {
    void init(ServletConfig var1) throws ServletException;
    ServletConfig getServletConfig();
    void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
    String getServletInfo();
    void destroy();
}

Servlet容器有如下:Tomcat,Jetty,Resin

### CoyoteAdapter 一个请求处理器的实现,将处理委托给Coyote处理器。我们需要关心其service函数。

@Override
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception {
    ...
        postParseSuccess = postParseRequest(req, request, res, response);
        connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
    ...
}

postParseRequest():在解析HTTP报头之后执行此函数,其内部需要关心的是将访问的url映射成一个mappingData,其中还包括了host、context、wrapper等。具体路径如下

CoyteAdapter.postParseRequest()
 Mapper.map()
  Mapper.internalMap()

internalMap函数的映射逻辑如下

    private void internalMap(CharChunk host, CharChunk uri, String version, MappingData mappingData)
            throws IOException {
                // Context mapping
                ContextList contextList = mappedHost.contextList;
                MappedContext[] contexts = contextList.contexts;
                int pos = find(contexts, uri);

        // Wrapper mapping
        if (!contextVersion.isPaused()) {
            internalMapWrapper(contextVersion, uri, mappingData);
        }
}

Context代表我们部署的应用名称,比如Tomcat部署后会在前缀增加一个xxx_war_wxploded 匹配上后进行包装,调用internalMapWrapper,下文进行拆解。 ### Mapper 上一步先定位到了Context,即我们应用的容器,这一步是叫做Wrapper mapping.是Wrapper的映射。 wrapper是context的子容器,他的职责是:加载它表示的 servlet 并分配它的一个实例。注意,每个Servlet都有一个映射的url进行匹配,并且都需要重写doGet、doPost等其中函数之一。

private void internalMapWrapper(ContextVersion contextVersion, CharChunk path, MappingData mappingData) throws IOException {
    // Rule 1 -- Exact Match
    MappedWrapper[] exactWrappers = contextVersion.exactWrappers;
    internalMapExactWrapper(exactWrappers, path, mappingData);
    // Rule 2 -- Prefix Match
    MappedWrapper[] wildcardWrappers = contextVersion.wildcardWrappers;
    if (mappingData.wrapper == null) {
        internalMapWildcardWrapper(wildcardWrappers, contextVersion.nesting, path, mappingData);
    }
    // Rule 3 -- Extension Match
    MappedWrapper[] extensionWrappers = contextVersion.extensionWrappers;
    if (mappingData.wrapper == null && !checkJspWelcomeFiles) {
        internalMapExtensionWrapper(extensionWrappers, path, mappingData, true);
    }
    // Rule 4 -- Welcome resources processing for servlets
        // Rule 4a -- Welcome resources processing for exact macth
        // Rule 4b -- Welcome resources processing for prefix match
        // Rule 4c -- Welcome resources processing

    /*
    * welcome file processing - take 2 Now that we have looked for welcome files with a physical backing, now look
    * for an extension mapping listed but may not have a physical backing to it. This is for the case of index.jsf,
    * index.do, etc. A watered down version of rule 4
    */

    // Rule 7 -- Default servlet
}

我们需要从一些匹配规则中找到wrapper,然后会将wrapper赋值给mappingData,如何匹配?匹配的细致规则怎么找?evilpan给出了,看官方文档Java Servlet Specification 12章节[2]。

  1. 首先尝试查找 Servlet 路由的 精确匹配
  2. 递归查找最长前缀匹配,以 / 字符为每一级目录树的分隔,即前缀匹配的单位是目录;
  3. 如果 URL 路径的最后一个片段(Segment)包含后缀,容器会尝试使用后缀匹配对应的 Servlet,比如对于 .jsp 后缀使用 JspServlet;
  4. 如果上述规则都没有成功匹配,容器将会尝试根据请求的 URL 去匹配对应的资源,这通常会使用一个容器自带的默认 Servlet 去处理。

在标准中还提到了几个值得注意的点:

  • 在匹配 ContextRoot 的时候也是使用最长前缀匹配;
  • 在 URL 进行匹配时候都是 大小写敏感的

对于配置映射的 ,有以下规则:

  • 映射值以 / 开头且以 /* 结尾的用于前缀匹配映射;
  • 映射值以 *. 开头的使用后缀匹配;
  • 空字符是一个特殊的映射值,指向 context-root;
  • 仅包含字符 / 的映射值表示对应应用的默认 Servlet;
  • 其他所有的值都被认为是精确匹配;

因此,对于应用中定义的不同映射,都可以根据寻址逻辑按照顺序找到最佳的匹配。

### Wrappers 回忆杀结束,继续回到上述 internalMapWrapper 的代码中。按照查找的顺序,Tomcat 会依次从下面的 Wrapper 中进行匹配:

  1. exactWrappers: 用于精确匹配的 Wrapper,比如值为 /api/flag 的 FlagServlet;
  2. wildcardWrappers: 用于(最长)前缀匹配的通配符 Wrapper,比如值为 /admin/* 的 Servlet;
  3. extensionWrappers: 用于后缀匹配的 Wrapper,比如值为 *.txt 的 Servlet。在 Tomcat 中默认有两个,分别是 jsp 和 jspx,都对应 org.apache.jasper.servlet.JspServlet;
  4. welcomeResources: 如果 URL 的最后一个字符是 /,则会尝试匹配欢迎页面,默认是 index.html、index.htm、index.jsp;对于没有物理文件的欢迎页面,比如 index.do、index.jsf 等,会根据后缀匹配的方式在内置资源文件中查找;
  5. defaultWrapper: 在前面都没有匹配的情况下,使用 Tomcat 的默认 Servlet 去进行处理,对应类是 org.apache.catalina.servlets.DefaultServlet,用于请求磁盘文件或者 jar 包中的文件。

大体来说,每一个 Servlet 对应一个 Wrapper 实例,而根据 URL 查找 Wrapper 的过程也就是对应 Servlet 路由查找的过程。

### decodeURI 上面是访问到映射添加相关资源的过程,下边是url编码相关

protected boolean postParseRequest(org.apache.coyote.Request req, Request request, org.apache.coyote.Response res,
        Response response) throws IOException, ServletException {

    MessageBytes decodedURI = req.decodedURI();

    // Parse (and strip out) the path parameters
    parsePathParameters(req, request);

    // URI decoding
    // %xx decoding of the URL
    req.getURLDecoder().convert(decodedURI.getByteChunk(), connector.getEncodedSolidusHandlingInternal());
    
        // Normalization
        if (normalize(req.decodedURI())) {
        // Character decoding
        convertURI(decodedURI, request);
    }
}

第一步parsePathParameters函数针对如下路径形式提取参数/path;name=value;name2=value2/org.apache.coyote.Request保存到org.apache.catalina.connector.Request中 第二步req.getURLDecoder().convert解码url中的% 第三步normalize函数校验

  1. url必须以/或者\开头
  2. 替换所有的\/
  3. Replace "//" with "/"
  4. If the URI ends with "/." or "/..", then we append an extra "/"
  5. Resolve occurrences of "/./" in the normalized path
  6. Resolve occurrences of "/../" in the normalized path

第四步convertURI(decodedURI, request);将ContextPath+RequestPath的bytes使用UTF-8转化成char类型 总结,经过上述转化之后的url才会进行Servlet路由查找

下面是evilpan思维的重点,他思考了路由寻址的变异方式!先充分理解Tomcat的过滤url方式,然后以最小单位找到绕过技巧。狠 ### Bypass Tricks 据此,我们可以得到一系列 “Bypass Tricks”,即用不同的方式路由到同一个 Servlet 中,比如目标路由是 /api/flag 的话,以下请求都能寻址到目标:

  • /api;a=b/flag: 通过 Path Parameter 路径参数变异;
  • /api/%66%6C%61%67: 通过 URL 编码进行变异;
  • : 通过 Normalization 1 变异,当前需要 Tomcat 配置 ALLOW_BACKSLASH 为 true;
  • //api/flag: 通过 Normalization 2/3 变异;
  • /api/./flag: 通过 Normalization 4 变异;
  • /foo/api/../api/flag: 通过 Normalization 5 变异;

这些变异方法可以相互组合进行使用,另外配合 DefaultServlet 针对磁盘文件和资源的路由也可以组合出其他的 URI。 ## Resin Resin执行servlet主要是通过执行ServletFilterChain,该chain为一系列FilterChain的末端。 一系列FilterChain于最开始的函数com.caucho.server.http.HttpRequest.handleRequest()生成,并设置在com.caucho.server.dispatch.Invocation中,Invocation又设置在request中,执行后续流程。 重点关注handleRequest ### handleRequest

public boolean handleRequest()
  throws IOException
{

  try {

    // 1
    startRequest();

    // 2
    if (! parseRequest()) {
      return false;
    }
    // 3
    CharSequence host = getInvocationHost();

    // 4
    Invocation invocation = getInvocation(host, _uri, _uriLength);

    requestFacade.setInvocation(invocation);
    // 5
    invocation.service(requestFacade, getResponseFacade());
  }...
}

  1. startRequest是清除之前请求的变量
  2. parseRequest是解析请求的第一行比如GET [http://www.caucho.com[:80]]/path [HTTP/1.x],并且解析请求的headers
  3. getInvocationHost拿到host的地址
  4. getInvocation通过host, uri, uriLength获取Invocation
  5. invocation.service()执行后续逻辑

先看映射servlet的部分getInvocation,这里的Invocation相当于Tomcat中的Wrapper

protected Invocation getInvocation(CharSequence host,
                                   byte []uri,
                                   int uriLength)
  throws IOException
{
  _invocationKey.init(isSecure(),
                      host, getServerPort(),
                      uri, uriLength);

  InvocationServer server = _server.getInvocationServer();
  
  Invocation invocation = server.getInvocation(_invocationKey);

  if (invocation != null)
    return invocation.getRequestInvocation(_requestFacade);

  return buildInvocation(invocation, uri, uriLength);
}

先根据_invocationKey从缓存中即_invocationCache获取Invocation,如果获取不到则执行buildInvocation新建一个

evilpan的额外分析 _invocationCache 是个 LRU 缓存,键为 com.caucho.server.http.InvocationKey 类型。InvocationKey 中包含 host、port、uri 三元组以及 isSecure 标志位,这么做的好处是节约路由查找时间,对于大型项目而言路由映射往往成百上千,每次请求都进行查找显然比较耗时。

buildInvocation时会先执行splitQueryAndUnescape逻辑如下 ### splitQueryAndUnescape

public void splitQueryAndUnescape(Invocation invocation,
                                  byte []rawURI, int uriLength)
  throws IOException
{
  for (int i = 0; i < uriLength; i++) {
    if (rawURI[i] == '?') {
      i++;

      // XXX: should be the host encoding?
      String queryString = byteToChar(rawURI, i, uriLength - i,
                                      "ISO-8859-1");
      invocation.setQueryString(queryString);

      uriLength = i - 1;
      break;
    }
  }

  String rawURIString = byteToChar(rawURI, 0, uriLength, "ISO-8859-1");
  invocation.setRawURI(rawURIString);

  String decodedURI = normalizeUriEscape(rawURI, 0, uriLength, _encoding);

  if (_sessionSuffix != null) {
    int p = decodedURI.indexOf(_sessionSuffix);

    if (p >= 0) {
      int suffixLength = _sessionSuffix.length();
      int tail = decodedURI.indexOf(';', p + suffixLength);
      String sessionId;

      if (tail > 0)
        sessionId = decodedURI.substring(p + suffixLength, tail);
      else
        sessionId = decodedURI.substring(p + suffixLength);

      decodedURI = decodedURI.substring(0, p);

      invocation.setSessionId(sessionId);

      p = rawURIString.indexOf(_sessionSuffix);
      if (p > 0) {
        rawURIString = rawURIString.substring(0, p);
        invocation.setRawURI(rawURIString);
      }
    }
  }
  else if (_sessionPrefix != null) {
    if (decodedURI.startsWith(_sessionPrefix)) {
      int prefixLength = _sessionPrefix.length();

      int tail = decodedURI.indexOf('/', prefixLength);
      String sessionId;

      if (tail > 0) {
        sessionId = decodedURI.substring(prefixLength, tail);
        decodedURI = decodedURI.substring(tail);
        invocation.setRawURI(rawURIString.substring(tail));
      }
      else {
        sessionId = decodedURI.substring(prefixLength);
        decodedURI = "/";
        invocation.setRawURI("/");
      }

      invocation.setSessionId(sessionId);
    }
  }

  String uri = normalizeUri(decodedURI);

  invocation.setURI(uri);
  invocation.setContextURI(uri);
}

  1. 先使用ISO-8859-1解码uri中的查询字符串setQueryString(queryString)原始urisetRawURI(rawURIString)并填充到Invocation
  2. normalizeUriEscape对原始uri进行过滤,返回decodedURI
  3. ;jsessionid=进行处理,截取;jsessionid=和其后第一个;之间的内容,否则如果后面没有;会截取后面所有内容作为jsessionid。原始uri会截取到;jsessionid之前。
  4. sessionPrefix的相关处理,默认为空不用操作。
  5. 使用normalizeUri对decodedURI进行过滤

### normalizeUriEscape

private static String normalizeUriEscape(byte []rawUri, int i, int len,
                                         String encoding)
  throws IOException
{
    converter.setEncoding(encoding);
  try {
    while (i < len) {
      int ch = rawUri[i++] & 0xff;

      if (ch == '%')
        i = scanUriEscape(converter, rawUri, i, len);
      else
        converter.addByte(ch);
    }

}

scanUriEscape

private static int scanUriEscape(ByteToChar converter,
                                 byte []rawUri, int i, int len)
  throws IOException
{
  int ch1 = i < len ? (rawUri[i++] & 0xff) : -1;

  if (ch1 == 'u') {
    ch1 = i < len ? (rawUri[i++] & 0xff) : -1;
    int ch2 = i < len ? (rawUri[i++] & 0xff) : -1;
    int ch3 = i < len ? (rawUri[i++] & 0xff) : -1;
    int ch4 = i < len ? (rawUri[i++] & 0xff) : -1;

    converter.addChar((char) ((toHex(ch1) << 12) +
                              (toHex(ch2) << 8) + 
                              (toHex(ch3) << 4) + 
                              (toHex(ch4))));
  }
  else {
    int ch2 = i < len ? (rawUri[i++] & 0xff) : -1;

    int b = (toHex(ch1) << 4) + toHex(ch2);;

    converter.addByte(b);
  }

  return i;
}

这里先判断了类似于%u1234这种编码类型进行解码,如果不是则对%dd这种类型进行解码 ### normalizeUri

public String normalizeUri(String uri, boolean isWindows)
  throws IOException
{
  CharBuffer cb = new CharBuffer();

  int len = uri.length();

  if (_maxURILength < len)
    throw new BadRequestException(L.l("The request contains an illegal URL because it is too long."));

  char ch;
  if (len == 0 || (ch = uri.charAt(0)) != '/' && ch != '\\')
    cb.append('/');

  for (int i = 0; i < len; i++) {
    ch = uri.charAt(i);

    if (ch == '/' || ch == '\\') {
    dots:
      while (i + 1 < len) {
        ch = uri.charAt(i + 1);

        if (ch == '/' || ch == '\\') {
          i++;
        }
        else if (ch == ';') {
          throw new BadRequestException(L.l("The request contains an illegal URL."));
        }
        else if (ch != '.') {
          break dots;
        }
        else if (len <= i + 2
                 || (ch = uri.charAt(i + 2)) == '/' || ch == '\\') {
          i += 2;
        }
        else if (ch == ';') {
          throw new BadRequestException(L.l("The request contains an illegal URL."));
        }
        else if (ch != '.')
          break dots;
        else if (len <= i + 3
                 || (ch = uri.charAt(i + 3)) == '/' || ch == '\\') {
          int j;

          for (j = cb.length() - 1; j >= 0; j--) {
            if ((ch = cb.charAt(j)) == '/' || ch == '\\')
              break;
          }
          if (j > 0)
            cb.setLength(j);
          else
            cb.setLength(0);
          i += 3;
        } else {
          throw new BadRequestException(L.l("The request contains an illegal URL."));
        }
      }

      while (isWindows && cb.getLength() > 0
             && ((ch = cb.getLastChar()) == '.' || ch == ' ')) {
        // server/0063
        // cb.setLength(cb.getLength() - 1);
        cb.setCharAt(cb.getLength() - 1, '_');

        if (cb.getLength() > 0
            && (ch = cb.getLastChar()) == '/' || ch == '\\') {
          cb.setLength(cb.getLength() - 1);
          // server/003n
          continue;
        }
      }

      cb.append('/');
    }
    else if (ch == 0)
      throw new BadRequestException(L.l("The request contains an illegal URL."));
    else
      cb.append(ch);
  }

url变异重点

  1. uri长度不超过1024
  2. uri为空、或者第一个位置没有分隔符则添加
  3. 默认分隔符为/\,下文用/简化,当遇到当前字符为分隔符时(下文包含当前)
  4. //,则前进一位
  5. /;则抛异常,除此之外如果不是/.则继续
  6. /./则前进两位
  7. /.;则抛异常,除此之外如果不是/..则继续
  8. /../,路径前移到上一个/
  9. windows情况下如果末尾字符是.或者`则设置其为_`

### servlet查找 路径流程两大分析方向1.url过滤2.servlet查找

mapServlet:215, ServletMapper (com.caucho.server.dispatch)
buildInvocation:4224, WebApp (com.caucho.server.webapp)
buildInvocation:798, WebAppContainer (com.caucho.server.webapp)
buildInvocation:753, Host (com.caucho.server.host)
buildInvocation:320, HostContainer (com.caucho.server.host)
buildInvocation:1068, ServletService (com.caucho.server.cluster)
buildInvocation:250, InvocationServer (com.caucho.server.dispatch)
buildInvocation:223, InvocationServer (com.caucho.server.dispatch)
buildInvocation:1610, AbstractHttpRequest (com.caucho.server.http)
getInvocation:1583, AbstractHttpRequest (com.caucho.server.http)
handleRequest:827, HttpRequest (com.caucho.server.http)
// ^
dispatchRequest:1395, TcpSocketLink (com.caucho.network.listen)
handleRequest:1351, TcpSocketLink (com.caucho.network.listen)
handleRequestsImpl:1335, TcpSocketLink (com.caucho.network.listen)
handleRequests:1243, TcpSocketLink (com.caucho.network.listen)
handleAcceptTaskImpl:1037, TcpSocketLink (com.caucho.network.listen)
runThread:117, ConnectionTask (com.caucho.network.listen)
run:93, ConnectionTask (com.caucho.network.listen)
handleTasks:175, SocketLinkThreadLauncher (com.caucho.network.listen)
run:61, TcpSocketAcceptThread (com.caucho.network.listen)
runTasks:173, ResinThread2 (com.caucho.env.thread2)
run:118, ResinThread2 (com.caucho.env.thread2)

com.caucho.server.dispatch.ServletMapper.mapServlet()内部

  1. 首先获取ContextPath,然后调用stripPathParameters将其过滤,保存变量cleanUri
  2. 使用_servletMap获取结果->ServletMapping servletMap = _servletMap.map(cleanUri, vars);
  3. 如果没找到则从META-INF/resources下查找相应的资源,如果找到则设置为resin-file的Servlet
  4. 没找到且contextURI以j_security_check为结尾则使用名字为j_security_check的servlet
  5. 查找welcomefile ``` public static String stripPathParameters(String value) { if (value == null) { return null; }

StringBuilder sb = null; int i = 0; int length = value.length();

for (; i < length; i++) { char ch = value.charAt(i);

if (ch == ';') {
  if (i > 0 && value.charAt(i - 1) == '/') {
    throw new IllegalArgumentException(L.l("{0} is an invalid URL.", value));
  }
  else if (i > 1 && value.charAt(i - 1) == '.' && value.charAt(i - 2) == '/') {
    throw new IllegalArgumentException(L.l("{0} is an invalid URL.", value));
  }
  
  if (sb == null) {
    sb = new StringBuilder();
    sb.append(value, 0, i);
  }
  
  int j = value.indexOf('/', i);
  int eq = value.indexOf('=', i);
  
  if (false && (j < 0 || eq < j)) {
    // #6308, but disabling because of potential security issues, and regressions
    sb.append(ch);
  }
  else if (j < 0) {
    return sb.toString();
  }
  else if (i > 0 && value.charAt(i - 1) == '/') {
    i = j;
  }
  else {
    i = j - 1;
  }
}
else if (sb != null) {
  sb.append(ch);
}

}

return sb != null ? sb.toString() : value; } ```

处理ContextPath中的;

  1. /;抛异常
  2. /.;抛异常
  3. 其他情况将;之前的内容加入
  4. 默认为false(考虑安全问题)->;之后没有/或者=/之前,则将;加入
  5. 如果;之后没有/则提前返回

_servletMap包含如下

- *.jsp->^.*\.jsp(?=/)|^.*\.jsp\z

表示字符串末尾,而 $ 表示行末,因此 可以匹配换行符而 $ 不能! 具体servlet的位置:file:/usr/local/resin-4.0.66/conf/app-default.xml:47:

  • resin-file->com.caucho.servlets.FileServlet
  • resin-jsp/jspx``com.caucho.jsp.JspServlet
  • resin-php->com.caucho.quercus.servlet.QuercusServlet
  • resin-xtp->com.caucho.jsp.XtpServlet ### Bypass Tricks 我直接就copy evilpan的了。我们版本不一样 类似于 Tomcat,根据对上述 Resin 源码的分析,我们也可以得出一些 Bypass Tricks,在原始 URI /api/flag 的基础上进行变异以实现正常路由到相同 Servlet 的目的:

  • hack/api/flag: 基于 readRequest 中判断 URI 的第一个字符不为 / 的变异;
  • /api/fla%67: 基于 normalizeUriEscape 解码的变异;
  • /api/fla%u0067: 基于 scanUriEscape 中特殊 URL 编码的变异;
  • /api//flag: 基于 normalizeUri 2.1 的变异;
  • /api/./flag: 基于 normalizeUri 2.2 的变异;
  • ../api/flag: 基于 normalizeUri 2.3 的变异;
  • /api\flag: 基于 normalizeUri 2.4 的变异,对于分隔符都会转换为 /;
  • /api/flag%20(空格),/api/flag.: 基于 normalizeUri 2.4 的变异,仅在 Windows 系统中有效;
  • /api/flag;a=b: 针对 stripPathParameters 的变异;
  • /api/flag%0a: 基于正则表达式 $ 不匹配换行的变异;

这些变异可以组合使用,从而形成更加丰富的 URI 结果。

# spring ## Springmvc 直接拿到evilpan的结论,参考https://evilpan.com/2023/08/19/url-gotchas-spring/ 假设存在资源文件 resources/static/secret.txt,对应的路由是 /secret.txt,可以使用下面的变体:

  1. /secret.txt/: 预处理时候会变成 secret.txt 删除前方以及末尾的分隔符;
  2. /;/secret.txt: 预处理时候删除连续的分隔符,和 // 的差别是处理阶段不同;
  3. /secret.txt;a=b: 预处理时候会删除路径参数;
  4. \secret.txt: 基于 processPath 中将  替换成 / 的变异;
  5. //secret.txt: 基于 cleanDuplicateSlashes 的变异,因此前面遗留的连续分隔符不会影响;
  6. / / // secret.txt: 基于 cleanLeadingSlash 的变异;
  7. /secret.%74%78%74: 寻找文件前会通过 encodeOrDecodeIfNecessary 进行 URL 解码;
  8. /foo/.././/secret.txt: createRelative 中会组合路径,并将其当做最终路径;

引用 [1] [https://www.baeldung.com/java-servlets-containers-intro](https://www.baeldung.com/java-servlets-containers-intro) [2] [https://jcp.org/aboutJava/communityprocess/final/jsr340/index.html](https://jcp.org/aboutJava/communityprocess/final/jsr340/index.html)

总结

  1. 大型中间件的代码只能通过其相关文档学习,只看代码或先看代码会不知所云