Confluence 未授權 RCE(CVE-2019-3396) 作者:Badcode@知道創宇404實驗室時間:2019年4月8日原文鏈接:https://paper.seebug.org/884/ 看到官方發布了預警,於是開始了漏洞應急。漏洞描述中指出Confluence Server與Confluence Data Center中的Widget Connector存在服務端模板注入漏洞,攻擊者能利用此漏洞能夠實現目錄穿越與遠程代碼執行。 確認漏洞點是Widget Connector,下載最新版的比對補丁,發現在comatlassianconfluenceextrawidgetconnectorWidgetMacro.java 裡面多了一個過濾,這個應該就是這個漏洞最關鍵的地方。 可以看到this.sanitizeFields = Collections.unmodifiableList(Arrays.asList(VelocityRenderService.TEMPLATE_PARAM)); 而TEMPLATE_PARAM的值就是_template,所以這個補丁就是過濾了外部傳入的_template參數。public interface VelocityRenderService { public static final String WIDTH_PARAM = "width"; public static final String HEIGHT_PARAM = "height"; public static final String TEMPLATE_PARAM = "_template"; 翻了一下Widget Connector裡面的文件,發現TEMPLATE_PARAM就是模板文件的路徑。public class FriendFeedRenderer implements WidgetRenderer { private static final String MATCH_URL = "friendfeed.com"; private static final String PATTERN = "friendfeed.com/(\w+)/?"; private static final String VELOCITY_TEMPLATE = "com/atlassian/confluence/extra/widgetconnector/templates/simplejscript.vm"; private VelocityRenderService velocityRenderService; ...... public String getEmbeddedHtml(String url, Map<String, String> params) { params.put(VelocityRenderService.TEMPLATE_PARAM, VELOCITY_TEMPLATE); return velocityRenderService.render(getEmbedUrl(url), params); } 載入外部的鏈接時,會調用相對的模板去渲染,如上,模板的路徑一般是寫死的,但是也有例外,補丁的作用也說明有人突破了限制,調用了意料之外的模板,從而造成了模板注入。在了解了補丁和有了一些大概的猜測之後,開始嘗試。首先先找到這個功能,翻了一下官方的文檔,找到了這個功能,可以在文檔中嵌入一些視頻,文檔之類的。 看到這個,有點激動了,因為在翻補丁的過程中,發現了幾個參數,url,width,height正好對應著這裡,那_template是不是也從這裡傳遞進去的?隨便找個Youtube視頻插入試試,點擊預覽,抓包。 在params中嘗試插入_template參數,好吧,沒啥反應。。 開始debug模式,因為測試插入的是Youtube視頻,所以調用的是com/atlassian/confluence/extra/widgetconnector/video/YoutubeRenderer.class public class YoutubeRenderer implements WidgetRenderer, WidgetImagePlaceholder { private static final Pattern YOUTUBE_URL_PATTERN = Pattern.compile("https?://(.+\.)?youtube.com.*(\?v=([^&]+)).*$"); private final PlaceholderService placeholderService; private final String DEFAULT_YOUTUBE_TEMPLATE = "com/atlassian/confluence/extra/widgetconnector/templates/youtube.vm"; ...... public String getEmbedUrl(String url) { Matcher youtubeUrlMatcher = YOUTUBE_URL_PATTERN.matcher(this.verifyEmbeddedPlayerString(url)); return youtubeUrlMatcher.matches() ? String.format("//www.youtube.com/embed/%s?wmode=opaque", youtubeUrlMatcher.group(3)) : null; } public boolean matches(String url) { return YOUTUBE_URL_PATTERN.matcher(this.verifyEmbeddedPlayerString(url)).matches(); } private String verifyEmbeddedPlayerString(String url) { return !url.contains("feature=player_embedded&") ? url : url.replace("feature=player_embedded&", ""); } public String getEmbeddedHtml(String url, Map<String, String> params) { return this.velocityRenderService.render(this.getEmbedUrl(url), this.setDefaultParam(params)); } 在getEmbeddedHtml下斷點,先會調用getEmbedUrl對用戶傳入的url進行正則匹配,因為我們傳入的是個正常的Youtube視頻,所以這裡是沒有問題的,然後調用setDefaultParam函數對傳入的其他參數進行處理。private Map<String, String> setDefaultParam(Map<String, String> params) { String width = (String)params.get("width"); String height = (String)params.get("height"); if (!params.containsKey("_template")) { params.put("_template", "com/atlassian/confluence/extra/widgetconnector/templates/youtube.vm"); } if (StringUtils.isEmpty(width)) { params.put("width", "400px"); } else if (StringUtils.isNumeric(width)) { params.put("width", width.concat("px")); } if (StringUtils.isEmpty(height)) { params.put("height", "300px"); } else if (StringUtils.isNumeric(height)) { params.put("height", height.concat("px")); } return params; } 取出width和height來判斷是否為空,為空則設置默認值。關鍵的_template參數來了,如果外部傳入的參數沒有_template,則設置默認的Youtube模板。如果傳入了,就使用傳入的,也就是說,aaaa是成功的傳進來了。 大概翻了一下Widget Connector裡面的Renderer,大部分是不能設置_template的,是直接寫死了,也有一些例外,如Youtube,Viddler,DailyMotion等,是可以從外部傳入_template的。 能傳遞_template了,接下來看下是如何取模板和渲染模板的。跟進this.velocityRenderService.render,也就是com/atlassian/confluence/extra/widgetconnector/services/DefaultVelocityRenderService.class裡面的render方法。public String render(String url, Map<String, String> params) { String width = (String)params.get("width"); String height = (String)params.get("height"); String template = (String)params.get("_template"); if (StringUtils.isEmpty(template)) { template = "com/atlassian/confluence/extra/widgetconnector/templates/embed.vm"; } if (StringUtils.isEmpty(url)) { return null; } else { Map<String, Object> contextMap = this.getDefaultVelocityContext(); Iterator var7 = params.entrySet().iterator(); while(var7.hasNext()) { Entry<String, String> entry = (Entry)var7.next(); if (((String)entry.getKey()).contentEquals("tweetHtml")) { contextMap.put(entry.getKey(), entry.getValue()); } else { contextMap.put(entry.getKey(), GeneralUtil.htmlEncode((String)entry.getValue())); } } contextMap.put("urlHtml", GeneralUtil.htmlEncode(url)); if (StringUtils.isNotEmpty(width)) { contextMap.put("width", GeneralUtil.htmlEncode(width)); } else { contextMap.put("width", "400"); } if (StringUtils.isNotEmpty(height)) { contextMap.put("height", GeneralUtil.htmlEncode(height)); } else { contextMap.put("height", "300"); } return this.getRenderedTemplate(template, contextMap); } } _template取出來賦值給template,其他傳遞進來的參數取出來經過判斷之後放入到contextMap,調用getRenderedTemplate函數,也就是調用VelocityUtils.getRenderedTemplate。protected String getRenderedTemplate(String template, Map<String, Object> contextMap){ return VelocityUtils.getRenderedTemplate(template, contextMap); } 一路調用,調用鏈如下圖,最後來到/com/atlassian/confluence/util/velocity/ConfigurableResourceManager.class的loadResource函數,來獲取模板。 這裡調用了4個ResourceLoader去取模板。com.atlassian.confluence.setup.velocity.HibernateResourceLoader org.apache.velocity.runtime.resource.loader.FileResourceLoader org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader com.atlassian.confluence.setup.velocity.DynamicPluginResourceLoader 這裡主要看下Velocity自帶的FileResourceLoader和ClasspathResourceLoaderFileResourceLoader會對用戶傳入的模板路徑使用normalizePath函數進行校驗 可以看到,過濾了/../,這樣就導致沒有辦法跳目錄了。 路徑過濾後調用findTemplate查找模板,可看到,會拼接一個固定的path,這是Confluence的安裝路徑。 也就是說現在可以利用FileResourceLoader來讀取Confluence目錄下面的文件了。嘗試讀取/WEB-INF/web.xml文件,可以看到,是成功的載入到了該文件。 但是這個無法跳出Confluence的目錄,因為不能用/../。再來看下ClasspathResourceLoaderpublic InputStream getResourceStream(String name) throws ResourceNotFoundException { InputStream result = null; if (StringUtils.isEmpty(name)) { throw new ResourceNotFoundException("No template name provided"); } else { try { result = ClassUtils.getResourceAsStream(this.getClass(), name); ...... } 跟進ClassUtils.getResourceAsStreampublic static InputStream getResourceAsStream(Class claz, String name) { while(name.startsWith("/")) { name = name.substring(1); } ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); InputStream result; if (classLoader == null) { classLoader = claz.getClassLoader(); result = classLoader.getResourceAsStream(name); } else { result = classLoader.getResourceAsStream(name); if (result == null) { classLoader = claz.getClassLoader(); if (classLoader != null) { result = classLoader.getResourceAsStream(name); } } } return result; } 會跳到/org/apache/catalina/loader/WebappClassLoaderBase.class 跟進,發現會拼接/WEB-INF/classes,而且其中也是調用了normalize對傳入的路徑進行過濾。 這裡還是可以用../跳一級目錄。嘗試讀取一下../web.xml,可以看到,也是可以讀取成功的,但是仍然無法跳出目錄。 我這裡測試用的版本是6.14.1,而後嘗試了file://,http://,https://都沒有成功。後來我嘗試把Cookie刪掉,發現還是可以讀取文件,確認了這個漏洞不需要許可權,但是跳不出目錄。應急就在這裡卡住了。而後的幾天,有大佬說用file://協議可以跳出目錄限制,我驚了,我確定當時是已經試過了,沒有成功的。看了大佬的截圖,發現用的是6.9.0的版本,我下載了,嘗試了一下,發現真的可以。問題還是在ClasspathResourceLoader上面,步驟和之前的是一樣的,斷到/org/apache/catalina/loader/WebappClassLoaderBase.class的getResourceAsStream方法前面拼接/WEB-INF/classes獲取失敗後,繼續往下進行。 跟進findResource,函數前面仍然獲取失敗 關鍵的地方就在這裡,會調用super.findResource(name),這裡返回了URL,也就是能獲取到對象。 不僅如此,這裡還可以使用其他協議(https,ftp等)獲取遠程的對象,意味著可以載入遠程的對象。 獲取到URL對象之後,繼續回到之前的getResourceAsStream,可以看到,當返回的url不為null時,會調用url.openStream()獲取數據。 最終獲取到數據給Velocity渲染。 嘗試一下 至於6.14.1為啥不行,趕著應急,後續會跟,如果有新的發現,會同步上來,目前只看到ClassLoader不一樣。6.14.1 6.9.0 這兩個loader的關係如下 現在可以載入本地和遠程模板了,可以嘗試進行RCE。 關於Velocity的RCE,基本上payload都來源於15年blackhat的服務端模板注入的議題,但是在Confluence上用不了,因為在調用方法的時候會經過velocity-htmlsafe-1.5.1.jar,裡面多了一些過濾和限制。但是仍然可以利用反射來執行命令。用python -m pyftpdlib -p 2121開啟一個簡單的ftp伺服器,將payload保存成rce.vm,保存在當前目錄。將_template設置成ftp://localhost:2121/rce.vm,發送,成功執行命令。 對於命令回顯,同樣可以使用反射構造出payload,執行ipconfig的結果。 漏洞影響 根據 ZoomEye 網路空間搜索引擎對關鍵字 "X-Confluence" 進行搜索,共得到 61,856 條結果,主要分布美國、德國、中國等國家。 全球分布(非漏洞影響範圍) 中國分布(非漏洞影響範圍) 漏洞檢測 2019年4月4日,404實驗室公布了該漏洞的檢測PoC,可以利用這個PoC檢測Confluence是否受該漏洞影響。 參考鏈接 漏洞檢測PoC Remote code execution via Widget Connector macro - CVE-2019-3396 漏洞預警 | Confluence Server 遠程代碼執行漏洞 本文由 Seebug Paper 發布,如需轉載請註明來源。歡迎關注我和專欄,我將定期搬運技術文章~也歡迎訪問我們:知道創宇雲安全 推薦閱讀: 相关文章 {{#data}} {{title}} {{/data}}