Confluence 檔案讀取漏洞(CVE-2019-3394)分析
作者: Badcode@知道創宇404實驗室
日期: 2019/08/29
英文版本:
前言
下午 @fnmsd 師傅發了個 的預警給我,我看了下補丁,復現了這個漏洞,本篇文章記錄下這個漏洞的應急過程。
看下描述,Confluence Server 和 Data Center 在頁面匯出功能中存在本地檔案洩露漏洞:具有“新增頁面”空間許可權的遠端攻擊者,能夠讀取
<install-directory>/confluence/WEB-INF/
目錄下的任意檔案。該目錄可能包含用於與其他服務整合的配置檔案,可能會洩漏認證憑據,例如 LDAP 認證憑據或其他敏感資訊。和之前應急過的一個漏洞一樣,跳不出WEB目錄,因為 confluence 的 web 目錄和 data 目錄一般是分開的,使用者的配置一般儲存在 data 目錄,所以感覺危害有限。
漏洞影響
- 6.1.0 <= version < 6.6.16
- 6.7.0 <= version < 6.13.7
- 6.14.0 <= version < 6.15.8
補丁對比
看到漏洞描述,觸發點是在匯出 Word 操作上,先找到頁面的這個功能。
接著看下程式碼層面,補丁是補在什麼地方。
6.13.7是6.13.x的最新版,所以我下載了6.13.6和6.13.7來對比。
去除一些版本號變動的干擾,把目光放在
confluence-6.13.x.jar
上,比對一下
對比兩個jar包,看到有個 importexport 目錄裡面有內容變化了,結合之前的漏洞描述,是由於匯出Word觸發的漏洞,所以補丁大機率在這裡。 importexport 目錄下面有個
PackageResourceManager
發生了變化,解開來對比一下。
看到關鍵函式
getResourceReader
,
resource = this.resourceAccessor.getResource(relativePath);
,看起來就是獲取檔案資源的,
relativePath
的值是
/WEB-INF
拼接
resourcePath.substring(resourcePath.indexOf(BUNDLE_PLUGIN_PATH_REQUEST_PREFIX))
而來的,而
resourcePath
是外部傳入的,看到這裡,也能大概猜出來了,應該是
resourcePath
可控,拼接
/WEB-INF
,然後呼叫
getResource
讀取檔案了。
流程分析
找到了漏洞最終的觸發點,接下來就是找到觸發點的路徑了。之後我試著在頁面插入各種東西,然後匯出 Word,嘗試著跳到這個地方,都失敗了。最後我在跟蹤插入圖片時發現跳到了相近的地方,最後透過構造圖片連結成功跳到觸發點。
首先看到
com.atlassian.confluence.servlet.ExportWordPageServer
的
service
方法。
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); } ...... }
在匯出 Word 的時候,首先會獲取到被匯出頁面的
pageId
,之後獲取頁面的內容,接著判斷是否有檢視許可權,跟進
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()); ......
前面會設定一些 header 之類的,然後將頁面的內容渲染,返回
renderedContent
,之後交給
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); ......
這個函式的功能是提取頁面中的圖片,當被匯出的頁面包含圖片時,將圖片的連結提取出來,交給
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); 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.downloadResourceManager.matches(decodedUri)
,當跟到這裡的時候,此時的
this.downloadResourceManager
是
DelegatorDownloadResourceManager
,並且下面有6個
downloadResourceManager
,其中就有我們想要的
PackageResourceManager
。
跟到
DelegatorDownloadResourceManager
的
matches
方法。
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()); }
matches
方法會呼叫
managersForResource
方法,分別呼叫每個
downloadResourceManager
的
matches
方法去匹配
resourcePath
,只要有一個
downloadResourceManager
匹配上了,就返回 true。來看下
PackageResourceManager
的
matches
方法
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
要以
BUNDLE_PLUGIN_PATH_REQUEST_PREFIX
開頭才返回true,看下
BUNDLE_PLUGIN_PATH_REQUEST_PREFIX
,是
DownloadResourcePrefixEnum
中的
PACKAGE_DOWNLOAD_RESOURCE_PREFIX
,也就是
/packages
。
public enum DownloadResourcePrefixEnum { ATTACHMENT_DOWNLOAD_RESOURCE_PREFIX("/download/attachments"), THUMBNAIL_DOWNLOAD_RESOURCE_PREFIX("/download/thumbnails"), ICON_DOWNLOAD_RESOURCE_PREFIX("/images/icons"), PACKAGE_DOWNLOAD_RESOURCE_PREFIX("/packages");
所以,
resourcePath
要以
/packages
開頭才會返回true。
回到
createInputStreamFromRelativeUrl
方法中,當有
downloadResourceManager
匹配上了
decodedUri
,就會進入分支。繼續呼叫
DownloadResourceReader downloadResourceReader = 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; }
跳到
DelegatorDownloadResourceManager
中的
getResourceReader
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); }
這裡會繼續呼叫
managersForResource
去呼叫每個
downloadResourceManager
的
matches
方法去匹配
resourcePath
,如果匹配上了,就繼續呼叫對應的
downloadResourceManager
的
getResourceReader
方法。到了這裡,就把之前的都串起來了,如果我們讓
PackageResourceManager
中的
matches
方法匹配上了
resourcePath
,那麼這裡就會繼續呼叫
PackageResourceManager
中的
getResourceReader
方法,也就是漏洞的最終觸發點。所以要進入到這裡,
resourcePath
必須是以
/packages
開頭。
整個流程圖大概如下
構造
流程分析清楚了,現在就剩下怎麼構造了。我們要插入一張連結以
/packages
開頭的圖片。
新建一個頁面,插入一張網路圖片
不能直接儲存,直接儲存的話插入的影像連結會自動拼接上網站地址,所以在儲存的時候要使用 burpsuite 把自動拼接的網站地址去掉。
釋出時,抓包
去掉網址
釋出之後,可以看到,圖片連結成功儲存下來了
最後點選 匯出 Word 觸發漏洞即可。成功讀取資料後會儲存到圖片中,然後放到 Word 文件裡面,由於無法正常顯示,所以使用 burp 來檢視返回的資料。
成功讀取到了
/WEB-INF/web.xml
的內容。
其他
這個漏洞是無法跳出web目錄去讀檔案的,
getResource
最後是會調到
org.apache.catalina.webresources.StandardRoot
裡面的
getResource
方法,這裡面有個
validate
函式,對路徑有限制和過濾,導致無法跳到
/WEB-INF/
的上一層目錄,最多跳到同層目錄。有興趣的可以去跟一下。
參考連結
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69912109/viewspace-2655850/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Vmware Vcenter 任意檔案讀取漏洞
- 致遠AnalyticsCloud分析雲任意檔案讀取漏洞復現Cloud
- 宏景HCM 任意檔案讀取漏洞
- 新型任意檔案讀取漏洞的研究
- Hoverfly 任意檔案讀取漏洞(CVE-2024-45388)
- SpringBoot配置檔案讀取過程分析Spring Boot
- JEEVMS倉庫管理系統任意檔案讀取漏洞
- 藍凌OA前臺任意檔案讀取漏洞利用
- Grafana 任意檔案讀取漏洞 (CVE-2021-43798)學習Grafana
- 任意檔案讀取
- Java 讀取檔案Java
- Apache Tomcat檔案包含漏洞分析ApacheTomcat
- IOC - 讀取配置檔案
- python 讀取文字檔案Python
- 前端讀取excel檔案前端Excel
- 用友任意檔案讀取
- viper 讀取配置檔案
- matlab讀取npy檔案Matlab
- python小白檔案讀取Python
- cocos讀取plist檔案
- go配置檔案讀取Go
- python讀取大檔案Python
- springboot讀取配置檔案Spring Boot
- Springboot整合MongoDB儲存檔案、讀取檔案Spring BootMongoDB
- 讀取檔案流並寫入檔案流
- go 讀取.ini配置檔案Go
- 01 讀取模板HTML檔案HTML
- pg從磁碟讀取檔案
- 6.1檔案下載、讀取
- 讀取資料夾檔案
- python如何讀取大檔案Python
- Mysql溯源-任意檔案讀取?MySql
- Java系列:讀取XML檔案JavaXML
- MATLAB快速讀取STL檔案Matlab
- java中讀取配置檔案Java
- go–讀取檔案的方式Go
- C#讀取Xml檔案C#XML
- Spring之Property檔案讀取Spring