前言
前段时间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
方法打断点查看执行流程。
1 2 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
处理。
1 2 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
中做了处理。
1 2 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
中,配置如下:
1 2 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
方法。
1 2 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
对象
1 2 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内容。
1 2 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
属性中。
1 2 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
。
1 2 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
方法
1 2 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
方法。
1 2 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
对象中返回。
1 2 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发起请求。
1 2 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
中。
1 2 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
1 2 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
。
1 2 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才会被重写并转发。
1 2 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
属性中。
1 2 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
方法
1 2 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
中处理。
1 2 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/
,是在哪一步对请求的路径做了处理呢?
1 2 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
下的内容。
1 2 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
中首先对路径规范化,这个过程会将我们的/;/
去掉。
1 2 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
处理。
1 2 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
中。
1 2 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
中路径规范化操作在解码操作之前,所以可以正确修复漏洞。