Date: 2019/08/29
Chinese Version: https://paper.seebug.org/1025/

### 1 Foreword

In the afternoon @fnmsd sent me a Confluence warning. I studied this patch, recurred the vulnerability and recorded the process of the emergency.

As the description says, Confluence Server and Data Center had a local file disclosure vulnerability in the page export function. A remote attacker who has Add Page space permission would be able to read arbitrary files in the /confluence/WEB-INF/packages directory, which may contain configuration files used for integrating with other services, which could potentially leak credentials or other sensitive information such as LDAP credentials. Like one of my previous vulnerability, it can't jump out of the WEB directory as the web directory and data directory of confluence are generally separated. Users' configuration is generally stored in the data directory, so the damage is limited.

### 2 Vulnerability Impact

• 6.1.0 <= version < 6.6.16
• 6.7.0 <= version < 6.13.7
• 6.14.0 <= version < 6.15.8

### 3 Patch Comparison

According to the description, the trigger is at the Word export operation.

Look at the code to see where the patch is.

Let's focus on confluence-6.13.x.jar.

Comparing these two jars, I saw that there is a change in the importexport directory. Combined with the previous description which said the vulnerability is in the page export function, the patch is probably here. There is a change in the PackageResourceManager in the importexport directory.

See the key function getResourceReader, resource = this.resourceAccessor.getResource(relativePath);. It seems to get the file resource, the value of relativePath is /WEB-INF splicing resourcePath.substring(resourcePath. indexOf(BUNDLE_PLUGIN_PATH_REQUEST_PREFIX)) , and resourcePath is passed in. It is obvious that resourcePath is controllable. It splices /WEB-INF, then call getResource to read the file.

### 4 Process Analysis

The next step is to find the path of the trigger. I tried to insert various things on the page, then exported Word, tried to jump to this place, and all failed. Finally, when I tracked the inserted image, I found that I jumped to a similar place, and finally jumped to the trigger by constructing the image link.

First see the service method of com.atlassian.confluence.servlet.ExportWordPageServer.

    public void service(SpringManagedServlet springManagedServlet, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String pageIdParameter = request.getParameter("pageId");
Long pageId = null;
if (pageIdParameter != null) {
try {
pageId = Long.parseLong(pageIdParameter);
} catch (NumberFormatException var7) {
}
} else {
response.sendError(404, "A valid page id was not specified");
}

if (pageId != null) {
AbstractPage page = this.pageManager.getAbstractPage(pageId);
if (page != null && page.isCurrent()) {
this.outputWordDocument(page, request, response);
} else {
response.sendError(404);
}
......

}

When exporting Word, it first get the pageId of the exported page, then get the content of the page, determine whether there is permission for viewing and follow up this.outputWordDocument

    private void outputWordDocument(AbstractPage page, HttpServletRequest request, HttpServletResponse response) throws IOException {
......

try {
ServletActionContext.setRequest(request);
ServletActionContext.setResponse(response);
String renderedContent = this.viewBodyTypeAwareRenderer.render(page, new DefaultConversionContext(context));
Map<String, DataSource> imagesToDatasourceMap = this.extractImagesFromPage(renderedContent);
renderedContent = this.transformRenderedContent(imagesToDatasourceMap, renderedContent);
Map<String, Object> paramMap = new HashMap();
paramMap.put("bootstrapManager", this.bootstrapManager);
paramMap.put("page", page);
paramMap.put("pixelsPerInch", 72);
paramMap.put("renderedPageContent", new HtmlFragment(renderedContent));
String renderedTemplate = VelocityUtils.getRenderedTemplate("/pages/exportword.vm", paramMap);
MimeMessage mhtmlOutput = this.constructMimeMessage(renderedTemplate, imagesToDatasourceMap.values());
mhtmlOutput.writeTo(response.getOutputStream());
......

There will be some headers in the front, and then the content of the page willget rendered. Then it will return renderedContent, and hand it to this.extractImagesFromPage

    private Map<String, DataSource> extractImagesFromPage(String renderedHtml) throws XMLStreamException, XhtmlException {
Map<String, DataSource> imagesToDatasourceMap = new HashMap();
Iterator var3 = this.excerpter.extractImageSrc(renderedHtml, MAX_EMBEDDED_IMAGES).iterator();

while(var3.hasNext()) {
String imgSrc = (String)var3.next();

try {
if (!imagesToDatasourceMap.containsKey(imgSrc)) {
InputStream inputStream = this.createInputStreamFromRelativeUrl(imgSrc);
if (inputStream != null) {
ByteArrayDataSource datasource = new ByteArrayDataSource(inputStream, this.mimetypesFileTypeMap.getContentType(imgSrc));
datasource.setName(DigestUtils.md5Hex(imgSrc));
imagesToDatasourceMap.put(imgSrc, datasource);
......

The function extracts the image in the page. When the exported page contains images, the link of the image is extracted and submitted to this.createInputStreamFromRelativeUrl.

    private InputStream createInputStreamFromRelativeUrl(String uri) {
if (uri.startsWith("file:")) {
return null;
} else {
Matcher matcher = RESOURCE_PATH_PATTERN.matcher(uri);
String relativeUri = matcher.replaceFirst("/");
String decodedUri = relativeUri;

try {
decodedUri = URLDecoder.decode(relativeUri, "UTF8");
} catch (UnsupportedEncodingException var9) {
log.error("Can't decode uri " + uri, var9);
}

if (this.pluginResourceLocator.matches(decodedUri)) {
Map<String, String> queryParams = UrlUtil.getQueryParameters(decodedUri);
decodedUri = this.stripQueryString(decodedUri);

try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
resource.streamResource(outputStream);
return new ByteArrayInputStream(outputStream.toByteArray());
log.error("Unable to serve plugin resource to word export : uri " + uri, var11);
}
String strippedUri = this.stripQueryString(decodedUri);
strippedUri = this.stripQueryString(relativeUri);
}

try {
} catch (Exception var10) {
log.warn("Could not retrieve image resource {} during Confluence word export :{}", decodedUri, var10.getMessage());
if (log.isDebugEnabled()) {
log.warn("Could not retrieve image resource " + decodedUri + " during Confluence word export :" + var10.getMessage(), var10);
}
}
}
} else if (uri.startsWith("data:")) {
return this.streamDataUrl(uri);
}
.....

This function is to get the image resources, and will handle different image links in different formats. We should look at this.downloadResourceManager.matches(decodedUri). The this.downloadResourceManager is DelegatorDownloadResourceManager, and there are 6 downloadResourceManager below. Among them is the PackageResourceManager we want.

Follow the matches method of DelegatorDownloadResourceManager.

    public boolean matches(String resourcePath) {
return !this.managersForResource(resourcePath).isEmpty();
}

......

return manager.matches(resourcePath) || manager.matches(resourcePath.toLowerCase());
}).collect(Collectors.toList());
}

The matches method will call the managersForResource method, calling the matches method of each downloadResourceManager to match resourcePath, and return true if there is a downloadResourceManager that matches. Look at the matches method of PackageResourceManager

    public PackageResourceManager(ResourceAccessor resourceAccessor) {
this.resourceAccessor = resourceAccessor;
}

public boolean matches(String resourcePath) {
return resourcePath.startsWith(BUNDLE_PLUGIN_PATH_REQUEST_PREFIX);
}

static {
}

resourcePath returns true when it starts with BUNDLE_PLUGIN_PATH_REQUEST_PREFIX. BUNDLE_PLUGIN_PATH_REQUEST_PREFIX is PACKAGE_DOWNLOAD_RESOURCE_PREFIX in DownloadResourcePrefixEnum, and is also called /packages.

public enum DownloadResourcePrefixEnum {
PACKAGE_DOWNLOAD_RESOURCE_PREFIX("/packages");

Therefore, resourcePath will return true when it starts with the /packages.

Back to createInputStreamFromRelativeUrl method. When downloadResourceManager matches decodedUri, it will enter the branch. Continue to call DownloadResourceReader downloadResourceReader = this.getResourceReader(decodedUri, userName, strippedUri);

    private DownloadResourceReader getResourceReader(String uri, String userName, String strippedUri) {

try {
log.debug("No resource found for url " + uri, var7);
}

}

Jump to getResourceReader in DelegatorDownloadResourceManager

    public DownloadResourceReader getResourceReader(String userName, String resourcePath, Map parameters) throws DownloadResourceNotFoundException, UnauthorizedDownloadResourceException {
}

Here we will continue to call managersForResource to call the matches method of each downloadResourceManager to match resourcePath. If there is a match, we will continue to call the getResourceReader method of the corresponding downloadResourceManager. So, if matches method in PackageResourceManager can match resourcePath, then we will continue to call the getResourceReader method in PackageResourceManager, which is the vulnerability. To get here, resourcePath must start with /packages.

The entire flow chart is as follows.

### 5 Construction

The process is clear, and now it's about how to construct it. We are going to insert a picture with a link starting with /packages.

Create a new page and insert a web image.

It can not be saved directly, cause if you do so, the inserted image link will be automatically spliced to the website address, so use the burpsuite to remove the automatically spliced website address when saving.

Capture the package when releasing.

Remove URL.

After the release, you can see that the image link was successfully saved.

Finally, click Export Word to trigger the vulnerability. After successfully reading the data, it will be saved to the image and then placed in the Word document. Since it cannot be displayed properly, I use burp to check the returned data.

The contents of /WEB-INF/web.xml have been successfully read.

### 6 What's More

This vulnerability is unable to jump out of the web directory to read the file. getResourcemethod will finally be transferred to org.apache.catalina.webresources.StandardRoot. There is a validate function in it and it has restrictions and filtration that prevent you from jumping to the upper directory of /WEB-INF/.

This video demonstrates the vulnerability with Pocsuite3:

### 7 Reference

Local File Disclosure via Word Export in Confluence Server - CVE-2019-3394