前言
    前段时间Confluence发布了CVE-2021-26085补丁,刚好之前分析过Confluence的漏洞,免去了搭建漏洞分析环境的麻烦,因此分析下这个漏洞。
 
分析过程
漏洞点定位
    这个漏洞爆出来已经有一段时间了,所以已经有公开的POC了
| 1
 | /s/123cfx/_/;/WEB-INF/web.xml
 | 
    首先大致测了一下,除了123cfx部分可以修改为其他内容,其他的部分修改或者删除后都会导致无法读取,/s/这部分比较特殊,所以猜测可能是由于以/s/开始会被当作静态文件处理。在web.xml中找/s/部分的Filter或者Servlet。
    在/WEB-INF/web.xml中对/s/对应的servlet做了配置,所以理论上来讲可以在ConfluenceNoOpServlet#service方法打断点查看执行流程。
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | <servlet><servlet-name>noop</servlet-name>
 <servlet-class>com.atlassian.confluence.servlet.ConfluenceNoOpServlet</servlet-class>
 <load-on-startup>0</load-on-startup>
 </servlet>
 <servlet-mapping>
 <servlet-name>noop</servlet-name>
 <url-pattern>/s/*</url-pattern>
 </servlet-mapping>
 
 | 
    但是当执行payload后并没断下来,将url改为/s/12xxxx则执行到了ConfluenceNoOpServlet,所以在Tomcat程序Filter到Servlet的必经之路ApplicationFilterChain#internalDoFilter方法this.servlet.service(request, response);打断点,发现当我们执行payload时最后是由DefaultServlet来处理的,而DefaultServlet按理说是只处理根目录的请求,为什么我们的payload会被DefaultServlet处理。
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | <servlet><servlet-name>default</servlet-name>
 <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
 ...
 </servlet>
 <servlet-mapping>
 <servlet-name>default</servlet-name>
 <url-pattern>/</url-pattern>
 </servlet-mapping>
 
 | 
    设置servlet的代码在ApplicationFilterChain#setServlet中,再次运行测试,发现程序会两次进入setServlet方法,第一次是ConfluenceNoOpServlet,第二次是DefaultServlet。所以猜测是当程序在Filter中对请求做了转发,查看调用链,果然在UrlRewriteFilter中做了处理。

| 12
 3
 4
 5
 6
 7
 8
 
 | <filter><filter-name>UrlRewriteFilter</filter-name>
 <filter-class>org.tuckey.web.filters.urlrewrite.UrlRewriteFilter</filter-class>
 </filter>
 <filter-mapping>
 <filter-name>UrlRewriteFilter</filter-name>
 <url-pattern>/s/*</url-pattern>
 </filter-mapping>
 
 | 
UrlRewriteFilter入门
    这里使用了UrlRewriteFilter组件,所以我们有必要先对这个组件简单了解。
UrlRewriteFilter是一个改写URL的Java Web过滤器,可见将动态URL静态化。适用于任何Java Web服务器(Resin,Jetty,JBoss,Tomcat,Orion等)。与其功能类似的还有Apache的mod_rewrite。
将动态URL转化为伪静态URL的好处主要有三个:
- 便于搜索引擎收录。
- 屏蔽url结构和参数信息,更安全。
- 可以将冗杂的URL改写得简而美。
    一般在web.xml中配置后还需要配置一个urlrewriter.xml,在Confluence中,配置如下:
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | <?xml version="1.0" encoding="utf-8"?><!DOCTYPE urlrewrite PUBLIC "-//tuckey.org//DTD UrlRewrite 4.0//EN" "http://tuckey.org/res/dtds/urlrewrite4.0.dtd">
 <urlrewrite>
 <class-rule class='com.atlassian.confluence.servlet.rewrite.ConfluenceResourceDownloadRewriteRule' />
 <rule>
 <from>/images/icons/attachments/file.gif</from>
 <to type="permanent-redirect">%{context-path}/images/icons/contenttypes/attachment_16.png</to>
 </rule>
 </urlrewrite>
 
 | 
    这个标签中的内容比较好理解,大概是当访问呢images/icons/attachments/file.gif会被重定向到%{context-path}/images/icons/contenttypes/attachment_16.png中,但<class-rule >中配置的类是如何工作的?
    查了官网的文档,当我们要扩展基本规则时,可以继承RewriteRule类并实现matches方法。

UrlRewriteFilter解析流程分析
初始化
    初始化init主要完成urlrewriter.xml的解析,这里会从FilterConfig中保存的配置中首先解析一些属性,这里需要注意,当没有配置modRewriteConf属性时,则会判断modRewriteStyleConf的值,这个值默认为False,所以会将confPath属性设置为/WEB-INF/urlrewrite.xml,再往下会判断modRewriteConfText属性是否在FilterConfig中配置,如果没有则通过loadUrlRewriter方法。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 
 |  private boolean modRewriteStyleConf = false;public void init(FilterConfig filterConfig) throws ServletException {
 ...
 String confPathStr = filterConfig.getInitParameter("confPath");
 ...
 
 String modRewriteConf = filterConfig.getInitParameter("modRewriteConf");
 if (!StringUtils.isBlank(modRewriteConf)) {
 this.modRewriteStyleConf = "true".equals(StringUtils.trim(modRewriteConf).toLowerCase());
 }
 
 if (!StringUtils.isBlank(confPathStr)) {
 this.confPath = StringUtils.trim(confPathStr);
 } else {
 this.confPath = this.modRewriteStyleConf ? "/WEB-INF/.htaccess" : "/WEB-INF/urlrewrite.xml";
 }
 ...
 
 String modRewriteConfText = filterConfig.getInitParameter("modRewriteConfText");
 if (!StringUtils.isBlank(modRewriteConfText)) {
 ModRewriteConfLoader loader = new ModRewriteConfLoader();
 Conf conf = new Conf();
 loader.process(modRewriteConfText, conf);
 conf.initialise();
 this.checkConf(conf);
 this.confLoadedFromFile = false;
 } else {
 this.loadUrlRewriter(filterConfig);
 }
 }
 }
 }
 
 | 
    loadUrlRewriter中主要通过调用loadUrlRewriterLocal完成实际的加载逻辑。
- 通过confPath作为路径加载内容到inputStream
- 将资源路径转换为URL并保存到confUrlStr中
- 通过文件内容,URL,modRewriteStyleConf等属性构建Conf对象
- checkConf检查- Conf对象
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 
 | private void loadUrlRewriterLocal() {InputStream inputStream = this.context.getResourceAsStream(this.confPath);
 if (inputStream == null) {
 inputStream = ClassLoader.getSystemResourceAsStream(this.confPath);
 }
 URL confUrl = null;
 try {
 confUrl = this.context.getResource(this.confPath);
 } catch (MalformedURLException var5) {
 log.debug(var5);
 }
 
 String confUrlStr = null;
 if (confUrl != null) {
 confUrlStr = confUrl.toString();
 }
 
 if (inputStream == null) {
 log.error("unable to find urlrewrite conf file at " + this.confPath);
 if (this.urlRewriter != null) {
 log.error("unloading existing conf");
 this.urlRewriter = null;
 }
 } else {
 Conf conf = new Conf(this.context, inputStream, this.confPath, confUrlStr, this.modRewriteStyleConf);
 this.checkConf(conf);
 }
 
 }
 
 | 
    首先看下Conf对象创建的过程,前面的是一些属性赋值的操作,在下面的If语句中判断modRewriteStyleConf的值用不同的解析方式,这个也可以理解.htaccess和urlrewrite.xml本来就应该用不同的方式解析,由于我们这里是使用urlrewrite.xml配置,因此会通过loadDom加载XML内容。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 
 | public Conf(ServletContext context, InputStream inputStream, String fileName, String systemId, boolean modRewriteStyleConf) {...
 if (modRewriteStyleConf) {
 this.loadModRewriteStyle(inputStream);
 } else {
 this.loadDom(inputStream);
 }
 
 if (this.docProcessed) {
 this.initialise();
 }
 
 this.loadedDate = new Date();
 }
 
 
 | 
    loadDom主要通过Dom方式解析XML内容,解析完成后通过processConfDoc处理解析后的内容,这里会根据标签的不同做不同的处理,由于我们这里只用了rule和rule-class标签,所以其他部分的代码先忽略。
- 标签为rule时则创建NormalRule对象 ,并将属性封装到这个对象中。
- 标签为class-rule创建ClassRule对象,并将class和method属性设置到这个对象中。
- 通过标签构造完对象后都会通过addRule将创建好的对象放到Conf.rules属性中。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 
 |   protected void processConfDoc(Document doc) {Element rootElement = doc.getDocumentElement();
 ...
 NodeList rootElementList = rootElement.getChildNodes();
 
 for(int i = 0; i < rootElementList.getLength(); ++i) {
 Node node = rootElementList.item(i);
 Element ruleElement;
 Node toNode;
 if (node.getNodeType() == 1 && ((Element)node).getTagName().equals("rule")) {
 ruleElement = (Element)node;
 NormalRule rule = new NormalRule();
 this.processRuleBasics(ruleElement, rule);
 procesConditions(ruleElement, rule);
 processRuns(ruleElement, rule);
 toNode = ruleElement.getElementsByTagName("to").item(0);
 rule.setTo(getNodeValue(toNode));
 rule.setToType(getAttrValue(toNode, "type"));
 rule.setToContextStr(getAttrValue(toNode, "context"));
 rule.setToLast(getAttrValue(toNode, "last"));
 rule.setQueryStringAppend(getAttrValue(toNode, "qsappend"));
 if ("true".equalsIgnoreCase(getAttrValue(toNode, "encode"))) {
 rule.setEncodeToUrl(true);
 }
 processSetAttributes(ruleElement, rule);
 this.addRule(rule);
 } else if (node.getNodeType() == 1 && ((Element)node).getTagName().equals("class-rule")) {
 ruleElement = (Element)node;
 ClassRule classRule = new ClassRule();
 if ("false".equalsIgnoreCase(getAttrValue(ruleElement, "enabled"))) {
 classRule.setEnabled(false);
 }
 
 if ("false".equalsIgnoreCase(getAttrValue(ruleElement, "last"))) {
 classRule.setLast(false);
 }
 
 classRule.setClassStr(getAttrValue(ruleElement, "class"));
 classRule.setMethodStr(getAttrValue(ruleElement, "method"));
 this.addRule(classRule);
 }
 }
 this.docProcessed = true;
 }
 
 | 
    最后我们再看下checkConf方法,这个方法通过checkConfLocal完成具体的检测,主要是通过Conf对象的一些属性检测是否加载成功,如果加载成功则通过Conf构建UrlRewriter对象并赋值给this.urlRewriter。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 
 | private void checkConfLocal(Conf conf) {...
 this.confLastLoaded = conf;
 if (conf.isOk() && conf.isEngineEnabled()) {
 this.urlRewriter = new UrlRewriter(conf);
 log.info("loaded (conf ok)");
 } else {
 if (!conf.isOk()) {
 log.error("Conf failed to load");
 }
 
 if (!conf.isEngineEnabled()) {
 log.error("Engine explicitly disabled in conf");
 }
 
 if (this.urlRewriter != null) {
 log.error("unloading existing conf");
 this.urlRewriter = null;
 }
 }
 
 }
 
 | 
拦截器处理过程
    拦截器的处理主要在UrlRewriteFilter#doFilter中,具体操作如下:
- 获取urlRewriter对象并封装到urlRewriteWrappedResponse中
- 判断servername是否为localhost,一般都不是所以先不看这里的处理逻辑
- urlRewriter不为Null,执行- processRequest方法
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 
 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {UrlRewriter urlRewriter = this.getUrlRewriter(request, response, chain);
 HttpServletRequest hsRequest = (HttpServletRequest)request;
 HttpServletResponse hsResponse = (HttpServletResponse)response;
 UrlRewriteWrappedResponse urlRewriteWrappedResponse = new UrlRewriteWrappedResponse(hsResponse, hsRequest, urlRewriter);
 if (this.statusEnabled && this.statusServerNameMatcher.isMatch(request.getServerName())) {
 String uri = hsRequest.getRequestURI();
 if (log.isDebugEnabled()) {
 log.debug("checking for status path on " + uri);
 }
 
 String contextPath = hsRequest.getContextPath();
 if (uri != null && uri.startsWith(contextPath + this.statusPath)) {
 this.showStatus(hsRequest, urlRewriteWrappedResponse);
 return;
 }
 }
 
 boolean requestRewritten = false;
 if (urlRewriter != null) {
 requestRewritten = urlRewriter.processRequest(hsRequest, urlRewriteWrappedResponse, chain);
 } else if (log.isDebugEnabled()) {
 log.debug("urlRewriter engine not loaded ignoring request (could be a conf file problem)");
 }
 
 if (!requestRewritten) {
 chain.doFilter(hsRequest, urlRewriteWrappedResponse);
 }
 
 }
 
 | 
    processRequest首先获取RuleChain,并执行doRules方法。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | public boolean processRequest(HttpServletRequest hsRequest, HttpServletResponse hsResponse, FilterChain parentChain) throws IOException, ServletException {
 RuleChain chain = this.getNewChain(hsRequest, parentChain);
 if (chain == null) {
 return false;
 } else {
 chain.doRules(hsRequest, hsResponse);
 return chain.isResponseHandled();
 }
 }
 
 | 
    getNewChain主要是从conf中获取rules,如果不为空,则将rules封装到RuleChain对象中返回。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 
 |   private RuleChain getNewChain(HttpServletRequest hsRequest, FilterChain parentChain) {String originalUrl = this.getPathWithinApplication(hsRequest);
 ...
 if (!this.conf.isOk()) {
 log.debug("configuration is not ok.  not rewriting request.");
 return null;
 } else {
 List rules = this.conf.getRules();
 if (rules.size() == 0) {
 log.debug("there are no rules setup.  not rewriting request.");
 return null;
 } else {
 return new RuleChain(this, originalUrl, parentChain);
 }
 }
 }
 }
 public RuleChain(UrlRewriter urlRewriter, String originalUrl, FilterChain parentChain) {
 this.finalToUrl = originalUrl;
 this.urlRewriter = urlRewriter;
 this.rules = urlRewriter.getConf().getRules();
 this.parentChain = parentChain;
 }
 
 | 
    下面分析比较重要的doRules方法,process主要是完成根据规则匹配URL,并重写URL。handleRewrite根据重写的URL发起请求。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 
 | public void doRules(ServletRequest request, ServletResponse response) throws IOException, ServletException {try {
 this.process(request, response);
 this.handleRewrite(request, response);
 } catch (InvocationTargetException var4) {
 this.handleExcep(request, response, var4);
 } catch (ServletException var5) {
 if (!(var5.getCause() instanceof InvocationTargetException)) {
 throw var5;
 }
 
 this.handleExcep(request, response, (InvocationTargetException)var5.getCause());
 }
 
 }
 
 | 
    下面分析这两个方法的操作过程
process
- 循环调用ruleChains中的matches方法,匹配成功则将结果赋值给RewrittenUrl对象,并将rewrittenUrl对象赋值给finalRewrittenRequest。将rewrittenUrl的URL保存到finalToUrl中。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 
 | public void process(ServletRequest request, ServletResponse response) throws IOException, ServletException, InvocationTargetException {while(this.ruleIdxToRun < this.rules.size()) {
 this.doRuleProcessing((HttpServletRequest)request, (HttpServletResponse)response);
 }
 }
 
 private void doRuleProcessing(HttpServletRequest hsRequest, HttpServletResponse hsResponse) throws IOException, ServletException, InvocationTargetException {
 int currentIdx = this.ruleIdxToRun++;
 Rule rule = (Rule)this.rules.get(currentIdx);
 RewrittenUrl rewrittenUrl = rule.matches(this.finalToUrl, hsRequest, hsResponse, this);
 if (rule.isFilter()) {
 this.dontProcessAnyMoreRules();
 }
 
 if (rewrittenUrl != null) {
 log.trace("got a rewritten url");
 this.finalRewrittenRequest = rewrittenUrl;
 this.finalToUrl = rewrittenUrl.getTarget();
 if (rule.isLast()) {
 log.debug("rule is last");
 this.dontProcessAnyMoreRules();
 }
 }
 }
 
 | 
- 下面到了我们分析这次漏洞的重点ClassRule的matches方法,主要是通过反射调用ConfluenceResourceDownloadRewriteRule#matches
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
 |  public RewrittenUrl matches(String url, HttpServletRequest hsRequest, HttpServletResponse hsResponse) throws ServletException, IOException {if (!this.initialised) {
 return null;
 } else {
 Object[] args = new Object[]{hsRequest, hsResponse};
 if (log.isDebugEnabled()) {
 log.debug("running " + this.classStr + "." + this.methodStr + "(HttpServletRequest, HttpServletResponse)");
 }
 
 if (this.matchesMethod == null) {
 return null;
 } else {
 Object returnedObj;
 try {
 returnedObj = this.matchesMethod.invoke(this.localRule, (Object[])args);
 ...
 }
 }
 
 | 
    这里我解释下matchesMethod为什么是ConfluenceResourceDownloadRewriteRule#matches,在初始化方法中,会通过反射获取method对象并赋值给matchesMethod,methodStr默认为matches。
| 12
 3
 4
 5
 6
 7
 
 |  private String methodStr = "matches";public boolean initialise(ServletContext context) {
 ...
 try {
 ruleClass = Class.forName(this.classStr);
 ...
 this.matchesMethod = ruleClass.getMethod(this.methodStr, methodParameterTypes);
 
 | 
- ConfluenceResourceDownloadRewriteRule#matches设置两个正则匹配,也就是说满足这两个任意一个正则,URL才会被重写并转发。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 
 | private static final Pattern NO_CACHE_PATTERN = Pattern.compile("^/s/(.*)/NOCACHE(.*)/_/((?i)(?!WEB-INF)(?!META-INF).*)");private static final Pattern CACHE_PATTERN = Pattern.compile("^/s/(.*)/_/((?i)(?!WEB-INF)(?!META-INF).*)");
 
 public RewriteMatch matches(HttpServletRequest request, HttpServletResponse response) {
 String url;
 try {
 
 url = this.getNormalisedPathFrom(request);
 } catch (URISyntaxException var8) {
 return null;
 }
 Matcher noCacheMatcher = NO_CACHE_PATTERN.matcher(url);
 Matcher cacheMatcher = CACHE_PATTERN.matcher(url);
 String rewrittenContextUrl;
 String rewrittenUrl;
 
 if (noCacheMatcher.matches()) {
 rewrittenContextUrl = "/" + this.rewritePathMappings(noCacheMatcher.group(3));
 rewrittenUrl = request.getContextPath() + rewrittenContextUrl;
 return new DisableCacheRewriteMatch(rewrittenUrl, rewrittenContextUrl);
 
 } else if (cacheMatcher.matches()) {
 rewrittenContextUrl = "/" + this.rewritePathMappings(cacheMatcher.group(2));
 rewrittenUrl = request.getContextPath() + rewrittenContextUrl;
 return new CachedRewriteMatch(rewrittenUrl, rewrittenContextUrl, cacheMatcher.group(1));
 } else {
 return null;
 }
 }
 
 | 
     执行我们的payload后当然会进入cacheMatcher的匹配,会获取/;/WEB-INF/web.xml设置给rewrittenContextUrl,将rewrittenContextUrl和request.getContextPath() 拼接得到rewrittenUrl,在Confluence中request.getContextPath()为空,所以rewrittenContextUrl=rewrittenUrl,下面将这些属性赋值到CachedRewriteMatch属性中。
| 12
 3
 4
 5
 
 | public CachedRewriteMatch(String rewrittenUrl, String rewrittenContextUrl, String staticHash) {this.rewrittenUrl = rewrittenUrl;
 this.rewrittenContextUrl = rewrittenContextUrl;
 this.staticHash = staticHash;
 }
 
 | 
handleRewrite
    下面我们分析handleRewrite方法
- 判断overiddenRequestParameters和overiddenMethod是否为空,为空则对request包装
- finalRewrittenRequest中保存了- rewrittenUrl,所以这里会进入IF语句,执行- doRewrite方法
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 
 | private void handleRewrite(ServletRequest request, ServletResponse response) throws ServletException, IOException {if (!this.rewriteHandled) {
 this.rewriteHandled = true;
 if (response instanceof UrlRewriteWrappedResponse && request instanceof HttpServletRequest) {
 HashMap overiddenRequestParameters = ((UrlRewriteWrappedResponse)response).getOverridenRequestParameters();
 String overiddenMethod = ((UrlRewriteWrappedResponse)response).getOverridenMethod();
 if (overiddenRequestParameters != null || overiddenMethod != null) {
 request = new UrlRewriteWrappedRequest((HttpServletRequest)request, overiddenRequestParameters, overiddenMethod);
 }
 }
 
 if (this.finalRewrittenRequest != null) {
 this.responseHandled = true;
 this.requestRewritten = this.finalRewrittenRequest.doRewrite((HttpServletRequest)request, (HttpServletResponse)response, this.parentChain);
 }
 
 if (!this.requestRewritten) {
 this.responseHandled = true;
 this.parentChain.doFilter((ServletRequest)request, response);
 }
 
 }
 }
 
 | 
    下面分析doRewrite方法, 执行CachedRewriteMatch.execute方法,这里可以看到将请求转发到/;/WEB-INF/web.xml中处理。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | public boolean doRewrite(HttpServletRequest hsRequest, HttpServletResponse hsResponse, FilterChain chain) throws IOException, ServletException {return this.rewriteMatch.execute(hsRequest, hsResponse);
 }
 
 public boolean execute(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
 ResourceDownloadUtils.addPublicCachingHeaders(request, response);
 request.setAttribute("_statichash", this.staticHash);
 request.getRequestDispatcher(this.rewrittenContextUrl).forward(request, response);
 return true;
 }
 
 | 
思考
    上面我们已经分析了我们的请求如何被UrlRewriteFilter处理并转发,但是我还有一些问题?
为什么不能直接访问;/WEB-INF/web.xml触发漏洞?
    当我直接访问/;/WEB-INF/web.xml则返回404,但在目标通过Forward转发到这个请求却可以读取文件,这是为什么?
直接访问过程
    在StandardContextValve中会判断当前的路径是否以/WEB-INF/或/META-INF/开始,如果是则返回404,不会执行后面的请求。那么有同学可能就要问了,我请求的地址明明是/;WEB-INF/,为什么到这里就变成了/WEB-INF/,是在哪一步对请求的路径做了处理呢?
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 
 | final class StandardContextValve extends ValveBase {private static final StringManager sm = StringManager.getManager(StandardContextValve.class);
 
 public StandardContextValve() {
 super(true);
 }
 
 public final void invoke(Request request, Response response) throws IOException, ServletException {
 MessageBytes requestPathMB = request.getRequestPathMB();
 if (!requestPathMB.startsWithIgnoreCase("/META-INF/", 0) && !requestPathMB.equalsIgnoreCase("/META-INF") && !requestPathMB.startsWithIgnoreCase("/WEB-INF/", 0) && !requestPathMB.equalsIgnoreCase("/WEB-INF")) {
 Wrapper wrapper = request.getWrapper();
 if (wrapper != null && !wrapper.isUnavailable()) {
 try {
 response.sendAcknowledgement();
 } catch (IOException var6) {
 this.container.getLogger().error(sm.getString("standardContextValve.acknowledgeException"), var6);
 request.setAttribute("javax.servlet.error.exception", var6);
 response.sendError(500);
 return;
 }
 
 if (request.isAsyncSupported()) {
 request.setAsyncSupported(wrapper.getPipeline().isAsyncSupported());
 }
 
 wrapper.getPipeline().getFirst().invoke(request, response);
 } else {
 response.sendError(404);
 }
 } else {
 response.sendError(404);
 }
 }
 }
 
 
 | 
    在CoyoteAdapter#postParseRequest中,会对传入的路径进行URL解码和规范化,并判断路径是否为web-inf,所以正常请求无法访问WEB-INF下的内容。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 
 | MessageBytes decodedURI = req.decodedURI();if (undecodedURI.getType() == 2) {
 decodedURI.duplicate(undecodedURI);
 this.parsePathParameters(req, request);
 
 try {
 req.getURLDecoder().convert(decodedURI, false);
 } catch (IOException var19) {
 response.sendError(400, "Invalid URI: " + var19.getMessage());
 }
 
 if (!normalize(req.decodedURI())) {
 response.sendError(400, "Invalid URI");
 }
 
 this.convertURI(decodedURI, request);
 if (!checkNormalize(req.decodedURI())) {
 response.sendError(400, "Invalid URI");
 }
 } else {
 decodedURI.toChars();
 CharChunk uriCC = decodedURI.getCharChunk();
 int semicolon = uriCC.indexOf(';');
 if (semicolon > 0) {
 decodedURI.setChars(uriCC.getBuffer(), uriCC.getStart(), semicolon);
 }
 }
 
 | 
转发访问过程
    上面我们分析了正常请求下无法访问WEB-INF下文件的原因,那么我们再思考一下,为什么转发过去的URL就可以访问web-inf下的内容呢?首先我们可以猜测一下,是否是因为转发过的请求不会再经过StandardContextValve的处理导致的?
    答案是肯定的,StandardContextValve只会在我们请求时处理一次,转发的请求不会再经过StandardContextValve的处理,这也是转发请求可以绕过限制访问WEB-INF下的内容的原因。
为什么转发请求会被DefaultServlet处理?
    我们分析过转发请求的地址时,转发的地址是/;/WEB-INF/web.xml,而DefaultServlet匹配的地址应该是/,为什么这个请求会被DefaultServlet进行处理?
    在CachedRewriteMatch#execute中,通过request.getRequestDispatcher(this.rewrittenContextUrl).forward(request, response);完成转发操作,而执行request.getRequestDispatcher(this.rewrittenContextUrl)后wrapper.instance已经被赋值为DefaultServlet。

在ApplicationContext#getRequestDispatcher中首先对路径规范化,这个过程会将我们的/;/去掉。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 
 | public RequestDispatcher getRequestDispatcher(String path) {if (path == null) {
 return null;
 } else if (!path.startsWith("/")) {
 throw new IllegalArgumentException(sm.getString("applicationContext.requestDispatcher.iae", new Object[]{path}));
 } else {
 int pos = path.indexOf(63);
 String uri;
 String queryString;
 if (pos >= 0) {
 uri = path.substring(0, pos);
 queryString = path.substring(pos + 1);
 } else {
 uri = path;
 queryString = null;
 }
 
 String uriNoParams = stripPathParams(uri);
 String normalizedUri = RequestUtil.normalize(uriNoParams);
 ...
 this.service.getMapper().map(this.context, uriMB, mappingData);
 ...
 
 | 
    在map方法中获取Wrapper保存到mappingData中。在Mapper#internalMapWrapper中将获取Wrapper,首先会根据路径匹配获取Wrapper,如果没有匹配到则默认由DefautlWrapper处理。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 
 |    public void map(Context context, MessageBytes uri, MappingData mappingData) throws IOException {...
 this.internalMapWrapper(contextVersion, uricc, mappingData);
 }
 
 private final void internalMapWrapper(Mapper.ContextVersion contextVersion, CharChunk path, MappingData mappingData) throws IOException {
 ...
 
 if (mappingData.wrapper == null && !checkJspWelcomeFiles) {
 if (contextVersion.defaultWrapper != null) {
 mappingData.wrapper = (Wrapper)contextVersion.defaultWrapper.object;
 mappingData.requestPath.setChars(path.getBuffer(), path.getStart(), path.getLength());
 mappingData.wrapperPath.setChars(path.getBuffer(), path.getStart(), path.getLength());
 mappingData.matchType = MappingMatch.DEFAULT;
 }
 }
 
 | 
为什么DefaultServlet会读取web.xml中的内容?
    在DefaultServlet#service会根据请求的类型调用不同的方法, 由于我们使用的GET请求,所以会调用doGet处理请求,而doGet又通过serveResource完成具体的处理操作,这里为了能让大家看的比较清晰,我对代码做了很多简化,大致可以看出根据我们传入的路径加载资源,通过copy将读取的内容输出到response中。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | protected void serveResource(HttpServletRequest request, HttpServletResponse response, boolean content, String inputEncoding) throws IOException, ServletException {String path = this.getRelativePath(request, true);
 WebResource resource = this.resources.getResource(path);
 InputStream source = resource.getInputStream();
 ServletOutputStream ostream = null;
 ostream = response.getOutputStream();
 OutputStreamWriter osw = new OutputStreamWriter(ostream, charset);
 PrintWriter pw = new PrintWriter(osw);
 this.copy((InputStream)source, (PrintWriter)pw, (String)inputEncoding);
 pw.flush();
 }
 
 | 
漏洞修复
    修复版本:
- 7.4.10
- 7.12.3
- 7.13.0
- 7.14.0  
对比修复版本的补丁,主要在ConfluenceResourceDownloadRewriteRule中,在matches之前,首先循环对URL解码,并将;替换为%3b,那么为什么把; URL编码后可以修复漏洞呢?

    是因为在ApplicationContext#getRequestDispatcher中路径规范化操作在解码操作之前,所以可以正确修复漏洞。
