cyllective's blog

Creating a Malicious Atlassian Plugin

15. Aug 2024, #web #java #plugins #atlassian

Recent events around xz ↗ reminded everyone that supply chain attacks are a real threat and, in fact, possible on this scale. During a recent audit, we explored exactly that attack scenario. What would happen to our customer’s Confluence instance if either their own developers or a 3rd party developer gets hit by such an attack?

This post is the second part about our recent adventures in the lands of Atlassian products. The first part focused on the plugin ecosystem and the 53 vulnerabilities we found in plugins, similar to our past research around WordPress.

Lab Setup #

To program such a malicious plugin, you need the Atlassian SDK. For this, your Java environment needs to be set up first. Atlassian requires Java SE Development Kit (JDK) 8 or AdoptOpenJDK 8. We did not manage to get it to work with AdoptOpenJDK and only with a specific, quite old Java JDK.

We used JDK 8u202, which can be downloaded here ↗. The installation is done manually and requires that JAVA_HOME is set to the root of the JDK folder and that the bin folder of the JDK is in your path. Using the version above this should result in the following output:

$ java -version
java version "1.8.0_202"
Java(TM) SE Runtime Environment (build 1.8.0_202-b08)

$ javac -version
javac 1.8.0_202

Next, the Atlassian SDK can be downloaded and installed using the official guide ↗.

# for Debian/Ubuntu
sudo sh -c 'echo "deb https://packages.atlassian.com/debian/atlassian-sdk-deb/ stable contrib" >>/etc/apt/sources.list'

curl -sS https://packages.atlassian.com/api/gpg/key/public | sudo gpg --dearmour -o /etc/apt/trusted.gpg.d/atlassian-sdk.gpg

sudo apt-key add public   
rm public

sudo apt-get update
sudo apt-get install atlassian-plugin-sdk

To start, go into a folder and run atlas-create-confluence-plugin. After entering some values, a folder containing the code will be created.

...
[INFO] using stable data version: 6.14.0
Define value for groupId: : com.cyllective           
Define value for artifactId: : malfluence
Define value for version:  1.0.0-SNAPSHOT: : 1.3.3.7
Define value for package:  com.cyllective: : com.cyllective.malfluence
Use OSGi Java Config:  (Y/N/y/n) N: : N
Confirm properties configuration:
groupId: com.cyllective
artifactId: malfluence
version: 1.3.3.7
package: com.cyllective.malfluence
use OSGi Java Config: N
 Y: : y
[INFO] Generating project in Batch mode
...

If you have your own Confluence instance already, compile a plugin by executing atlas-mvn package in its root directory. After that, the compiled plugin can be found ./target/<NAME>-<VERSION>.jar.

To launch a demo/dev instance of Confluence, run atlas-run. This will take some time, but if it is successful, Confluence will be available at http://localhost:1990/confluence. If you get a 404 status code from Tomcat, try a Java 8 version older than 8u3xx. We had success with 8u202.

Warning: When you are programming a plugin and are accessing URLs of the Confluence instance, the local dev instance will have /confluence at the beginning. When using a self-hosted instance with the default parameters, this sub-directory will not be present. This is especially important when accessing REST endpoints under /rest as the base path changes from /rest to /confluence/rest.

Plugin Structure #

In the root of the plugin folder, there will be a pom.xml, which defines basic plugin settings like the name, version, and organization. More interesting for us is atlassian-plugin.xml inside ./src/main/ressources. You can read more about that file in our first part.

But the tl;dr is: Plugins are made up of modules ↗. Those are specified in that XML file and can be added either manually or via the command atlas-create-confluence-plugin-module. For this demo, we are gonna go a bit deeper into two modules.

Web ressources #

Web resources ↗ define files like JavaScript or CSS that will be loaded by users visiting Confluence. Such resources can be placed in the ./src/main/resources folder and then referenced in the XML file. To include a JavaScript file, add the following block to your XML:

<web-resource name="My JS Ressource" key="my-js-ressource">
    <resource type="download" name="my-js.js" location="/js/my-js.js"/>
    <context>atl.general</context>
</web-resource>

The location specifies where inside the resource folder you put the file. The context defines where the resource will get loaded. Here, the two main configurations to use are atl.general for loading resources almost anywhere as a normal user and atl.admin for loading resources in the admin section.

REST endpoints #

The best way for us to enable backdoor-like features is a REST endpoints ↗. Those will be exposed under /rest after being defined inside the XML file. You only need to define one node here, as the individual endpoints will be registered as sub-paths via Java annotations.

<rest name="My REST API" key="my-rest-api" path="/myrestapi" version="1.0">
</rest>

This API will be accessible at /rest/<PATH>/<VERSION>/ or in the dev instance at /confluence/rest/<PATH>/<VERSION>/. The <VERSION> can also be substituted for latest.

When writing Java, you can register endpoints by using annotations. Here is a template for a GET endpoint:

package com.cyllective.malfluence.rest;

import com.atlassian.plugins.rest.common.security.AnonymousAllowed;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

// Will be registered as GET /rest/<PATH>/<VERSION>/someendpoint
@Path("/someendpoint")
public class SomeEndpoint {
    @GET
    @AnonymousAllowed
    @Produces(MediaType.TEXT_PLAIN)
    public Response SomeEndpointResponse(@QueryParam("MyParam") String MyParam) {
        try {
            return Response
                    .status(Response.Status.OK)
                    .entity(MyParam)
                    .build();

        } catch (Exception e) {
            return Response
                    .status(Response.Status.INTERNAL_SERVER_ERROR)
                    .build();
        }
    }
}

Using @AnonymousAllowed will make an endpoint accessible without a session token. This can be a desired feature of our plugin. @Produces defines the content type of the response which must be javax.ws.rs.core.Response. The rest is just Java 8 code, so you are only limited by what your plugin can and cannot do by the aged Java version.

Stealing REST routes #

A quick detour: What would happen if two plugins register the same REST route? Imagine a Confluence administrator limiting access to certain REST paths behind a web application firewall. We, the attackers, then register the same path inside our atlassian-plugin.xml and use the same annotation inside our code. Did we just hijack the route? Well, that depends…

Confluence loads the plugins in alphabetical order based on the plugin’s package name (ex: com.company.package). If we change this inside a plugin, we can hijack routes of other plugins! But this happens only at boot. This means an instance must be restarted after we load our aaaa.aaaa.aaaa package, for example.

Malicious Features #

In our little experiment, we crammed a lot of features into this bad boy. Here are some highlights. The full list of features can be found in the Repo ↗.

bad meme

Hiding Plugins from Admins #

To hide plugins from the admin interface, a little JavaScript trickery was needed. First, inside the plugin, we created an endpoint to accept and store plugin names, as well as one that gives out this list of plugins.

Upon logging in as an admin, the JavaScript, which gets injected into any administrator’s session, fetches this list, and if a specific DOM element changes, the correct <div> in the plugin list gets simply removed. This is so fast that the user will not notice it and hides the plugin, at least in the web interface.

Exfiltrating Attachments and Pages #

Using SpaceManager and PageManger, we were able to iterate all the pages. This made it possible for us to identify a page’s ID and then download the content and all attachments without any form of authentication.

Downloading pages Downloading attachments

Stealing Session Tokens #

The session cookie has HttpOnly ↗ set. But this did stop us from exfiltrating it. We created an endpoint that reflects all HTTP headers back as base64, which means they ended up in the DOM. From there, we sent them to our headerserver.py. It would also be possible to send it directly from the backend. (the plugin)

Command Execution #

We could not leave out a simple RCE backdoor. This endpoint simply accepts commands and arguments via GET parameters and returns the output of the command as the HTTP response.

Executing commands

Reverse Shell #

In addition to the direct command execution backdoor, in true red teaming fashion, we created a simple TCP reverse shell.

TCP Reverse Shell

DOM Proxying #

What if you, the attacker, managed to install a malicious plugin via a supply chain attack or via social engineering, but the server is only internally available? How can you still interact with it?

Since Confluence does not offer a CSP ↗ guideline, nothing stops us from abusing the client’s browser session as a proxy - introducing DOM Proxying.

As a separate PoC, this small plugin will listen for commands (jobs) that a user’s browser session will first fetch from the attacker’s server and then forward to the plugin’s REST endpoint. The command output is then sent back to the attacker.

DOM Proxy

Conclusion #

What a plugin can and cannot do is up to the developer of that plugin. Atlassian does not offer any direct protection against malicious plugins. A plugin can access all content inside a Confluence instance, access the database directly, and execute arbitrary commands on the underlying Linux server. It is only limited by what the Java version can offer.

Our final malicious plugin can be found on GitHub ↗.

Defense #

To defend against malicious plugins is hard. As with most plugin ecosystems, if you install it, it will compromise your instance. There are, however, still a few points to consider:

Static Analysis

Third-party platforms and products cannot directly run plugins and see what they do at runtime but can statically analyze the plugin files (.jar) for known malicious content/patterns. If you are interested in the topic yourself, jar files can be compiled most of the time into readable code. You might find malicious content or vulnerabilities yourself.

Minimize Plugins

Filter and Monitor Ingress and Egress

Add a Content Security Policy

In our example, we often interacted with our attacker server via the browser. Communication done this way can be restricted by adding a Content Security Policy ↗, CSP in short.

This is not a silver bullet, but it can limit some form of post-exploitation, especially if your server cannot communicate with the outside world. If one of your plugins happens to have an XSS vulnerability, a CSP can also limit the impact here.

Table of Contents