作者:Badcode@知道創宇404實驗室
時間:2019年4月8日
English Version: http://www.bjnorthway.com/886/

看到官方發布了預警,于是開始了漏洞應急。漏洞描述中指出Confluence Server與Confluence Data Center中的Widget Connector存在服務端模板注入漏洞,攻擊者能利用此漏洞能夠實現目錄穿越與遠程代碼執行。

確認漏洞點是Widget Connector,下載最新版的比對補丁,發現在com\atlassian\confluence\extra\widgetconnector\WidgetMacro.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刪掉,發現在Linux環境下面還是可以讀取文件,Windows的6.14.1版本是需要登陸的,但是跳不出目錄。應急在這里卡住了。

而后的幾天,有大佬用file://協議可以跳出目錄限制,我驚了,我確定當時是已經試過了,沒有成功的。看了大佬的截圖,發現用的是6.9.0的版本,我下載了,嘗試了一下,發現真的可以。而且在6.9.0版本中,Windows和Linux環境都不需要登陸。

問題還是在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是否受該漏洞影響。

參考鏈接


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/884/