Analysis of Confluence File Read Vulnerability (CVE-2019-3394)

Author: Badcode@Knows the date of Chuangyu 404 Laboratory: English Version 2019/08/29: https://paper.seebug.org/1026/

Preface

Afternoon @fnmsd The master sent me an early warning from Confluence. I saw the patch and reproduced the vulnerability. This article records the emergency process of the vulnerability.

Look at the description below. Confluence Server and Data Center have local file leak vulnerabilities in page export: remote attackers with "add page" space privileges can read any file in the <install-directory>/confluence/WEB-INF/directory. The directory may contain configuration files for integration with other services and may leak authentication credentials, such as LDAP authentication credentials or other sensitive information. Like a previous vulnerability, we can't jump out of the WEB directory, because the web directory and data directory of confluence are generally separated, and the user's configuration is generally stored in the data directory, so the harm is limited.

Vulnerability impact

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

Patch comparison

Looking at the vulnerability description, the trigger point is to find this function of the page on the export Word operation.

Then look at the code level, where is the patch being patched?

6.13.7 is the latest version of 6.13.x, so I downloaded 6.13.6 and 6.13.7 for comparison.

Remove the interference of some version number changes and focus on confluence-6.13.x.jar.

Comparing the two jar packages, you can see that there is a content change in the importexport directory. Combining with the previous vulnerability description, it is due to the vulnerability triggered by the export Word, so the patch rate is probably here. Under the importexport directory, a PackageResourceManager has changed. Unscramble and compare.

See the key function getResourceReader, resource = this.resourceAccessor.getResource(relativePath);, it seems to be getting file resources, the value of relativePath is / WEB-INF splicing resourcePath.substring(resourcePath.indexOf(BUNDLE_PLUGIN_PATH_REQUEST_PREFIX)), and resourcePath is passed in externally. Seeing this, you can probably guess that it should be resourcePath controllable, splicing / WEB-INF, and then calling getResource to read the file.

Process analysis

Finding the ultimate trigger point of the vulnerability, the next step is to find the path of the trigger point. Then I tried to insert various things into the page, export Word, and try to jump to this place, all of which failed. Finally, I tracked the insertion of images and found that I jumped to a similar place. Finally, I succeeded in jumping to the trigger point by constructing image links.

First, see the service method of com. atlassian. confluence. servlet. ExportWordPage Server.

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) {
            response.sendError(404, "Page not found: " + pageId);
        }
    } else {
        response.sendError(404, "A valid page id was not specified");
    }

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

}


When exporting Word, the pageId of the imported page will be obtained first, then the content of the page will be obtained, and then the viewing authority will be judged. Output Word Document will be followed up.

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());
        ......


Some header s will be set up before, and then the content of the page will be rendered, returned to renderedContent, and then handed over to this.extractImagesFromPage for processing.

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 of this function is to extract the pictures in the page. When the exported page contains the pictures, the link of the pictures is extracted and handed over to this. createInputStream FromRelativeUrl for processing.

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);
            DownloadableResource resource = this.pluginResourceLocator.getDownloadableResource(decodedUri, queryParams);

            try {
                ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                resource.streamResource(outputStream);
                return new ByteArrayInputStream(outputStream.toByteArray());
            } catch (DownloadException var11) {
                log.error("Unable to serve plugin resource to word export : uri " + uri, var11);
            }
        } else if (this.downloadResourceManager.matches(decodedUri)) {
            String userName = AuthenticatedUserThreadLocal.getUsername();
            String strippedUri = this.stripQueryString(decodedUri);
            DownloadResourceReader downloadResourceReader = this.getResourceReader(decodedUri, userName, strippedUri);
            if (downloadResourceReader == null) {
                strippedUri = this.stripQueryString(relativeUri);
                downloadResourceReader = this.getResourceReader(relativeUri, userName, strippedUri);
            }

            if (downloadResourceReader != null) {
                try {
                    return downloadResourceReader.getStreamForReading();
                } 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 process the links in different formats. The focus here is this. download ResourceManager. matches (decodedUri). When you follow here, this. download ResourceManager is Delegator Download ResourceManager, and there are six downloads below. DResourceManager, which includes the PackageResourceManager we want.

Follow the matches method of DelegatorDownload ResourceManager.

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

......

    private List<DownloadResourceManager> managersForResource(String resourcePath) {
    return (List)this.downloadResourceManagers.stream().filter((manager) -> {
        return manager.matches(resourcePath) || manager.matches(resourcePath.toLowerCase());
    }).collect(Collectors.toList());
}


The matches method calls the managersForResource method, calling the matches method of each download ResourceManager to match resourcePath, and returns true as long as one download ResourceManager matches. Take a look at PackageResourceManager's matches method

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

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

static {
    BUNDLE_PLUGIN_PATH_REQUEST_PREFIX = DownloadResourcePrefixEnum.PACKAGE_DOWNLOAD_RESOURCE_PREFIX.getPrefix();
}


resourcePath starts with BUNDLE_PLUGIN_PATH_REQUEST_PREFIX before returning true. Look at BUNDLE_PLUGIN_PATH_REQUEST_PREFIX, which is PACKAGE_DOWNLOAD_RESOURCE_PREFIX in Download Resource Prefix Enum, that is, packages/ages.

Public enum Download Resource Prefix Enum {ATTACHMENT_DOWNLOAD_RESOURCE_PREFIX ("/download/attachments"), THUMBNAIL_DOWNLOAD_RESOURCE_PREFIX ("/download/thumbnails"), ICON_DOWNLOAD_RESOURCE_PREFIX ("/images/ns"), PACKAGE_WNLOAD_RESOURCE_PREFIX ("/packages"); The head will return true.

Back in the createInputStream FromRelativeUrl method, when a download ResourceManager matches decodedUri, it goes into a branch. Continue calling Download ResourceReader download ResourceReader = this. getResourceReader (decodedUri, userName, strippedUri);

private DownloadResourceReader getResourceReader(String uri, String userName, String strippedUri) {
    DownloadResourceReader downloadResourceReader = null;

    try {
        downloadResourceReader = this.downloadResourceManager.getResourceReader(userName, strippedUri, UrlUtil.getQueryParameters(uri));
    } catch (UnauthorizedDownloadResourceException var6) {
        log.debug("Not authorized to download resource " + uri, var6);
    } catch (DownloadResourceNotFoundException var7) {
        log.debug("No resource found for url " + uri, var7);
    }

    return downloadResourceReader;
}


Jump to getResourceReader in Delegator Download Resource Manager

public DownloadResourceReader getResourceReader(String userName, String resourcePath, Map parameters) throws DownloadResourceNotFoundException, UnauthorizedDownloadResourceException {
    List<DownloadResourceManager> matchedManagers = this.managersForResource(resourcePath);
    return matchedManagers.isEmpty() ? null : ((DownloadResourceManager)matchedManagers.get(0)).getResourceReader(userName, resourcePath, parameters);
}


This will continue to call managersForResource to call the matches method of each download ResourceManager to match resourcePath, and if it matches, continue to call the getResourceReader method of the corresponding download ResourceManager. If we match the matches method in PackageResourceManager with resourcePath, we will continue to call the getResourceReader method in PackageResourceManager, which is the ultimate trigger of the vulnerability. So to get in here, resourcePath must start with / packages.

The whole flow chart is roughly as follows

structure

The process analysis is clear, and now there is how to construct it. We want to insert a link with a picture that starts with / packages.

Create a new page and insert a web image

It can't be saved directly. If saved directly, the insertion of image links will automatically stitch the site address, so burpsuite should be used to remove the automatic stitching site address when saved.

When publishing, grab the package

Remove the website

After publishing, you can see that the image links have been successfully saved.

Finally, click Export Word to trigger the vulnerability. Successful reading of the data will be saved to the picture, and then put into the Word document, because it can not be displayed properly, so use burp to view the returned data.

The content of / WEB-INF/web.xml was read successfully.

Other

This vulnerability is unable to jump out of the web directory to read files. getResource will eventually be transferred to the getResource method in org.apache.catalina.webresources.StandardRoot. There is a validate function in it, which restricts and filters the path, resulting in the inability to jump to the upper directory of/WEB-INF/and jump to the same directory at most. If you are interested, you can follow.

Reference link

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

Tags: Operation & Maintenance xml Apache

Posted on Tue, 03 Sep 2019 02:52:14 -0700 by paruby