Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save atxsinn3r/a74abb69ecb5a4afa8a8a54cdd6e6123 to your computer and use it in GitHub Desktop.
Save atxsinn3r/a74abb69ecb5a4afa8a8a54cdd6e6123 to your computer and use it in GitHub Desktop.

Jira Universal Plugin Manager PluginCollectionResource Class Upload Vulnerability

Jira is a Java application for the purposes of bug tracking and agile project management developed by Atlassian. According to Atlassian, Jira is used by over 75,000 customers in 122 countries. Some high-profile organizations include Fedora Commons, NASA, Skype Technogies, Twitter, United States Deaprtment of Defense, etc.

The upload feature in the The Universal Plugin Manager (UPM) would allow a Jira user to upload a malicious plugin (add-on), and acheive remote code exeuction. This also implies authentication is required in order to acheive this.

Originally, an exploit module was submitted to Metasploit by Alex Gonzalez, and this is how our analysis started.

Vulnerable Application

The latest Jira should be vulnerable to this.

After installation, you should also create a plugin. Instructions can be found here.

Review Details

Initial Analysis

As previously mentioned, our review began with a pull request on Metasploit. The module description or the markdown documentation doesn't really explain the details of the vulnerability, but it does consistently point out that it's the UPM component being the root cause, so that's our starting point. Tools from SysInternalSuite are used for the initial analysis.

After installing Jira, we learned that the default port is 2990 (HTTP), and the TCPView utility tells us this is a java.exe process. So that's what we should focus on.

Next, Process Monitor is used to monitor the operations occured when an exploit is fired against java.exe. We know that the exploit attempts to upload a Metasploit Java payload to Jira, so you should expect some activities related to a Payload.class file. On Windows, the actual path the payload gets uploaded to this location:

C:\Users\TestUser\myPlugin\target\container\tomcat8x\cargo-jira-home\temp\~spawn6080520409872505521.tmp.dir\metasploit\Payload.class

Most importantly, Process Monitor also reveals the location of the jar file for Universal Plugin Manager:

C:\Users\TestUser\myPlugin\target\container\tomcat8x\cargo-jira-home\webapps\jira\WEB-INF\atlassian-bundled-plugins\atlassian-universal-plugin-manager-plugin-2.22.9.jar

Analysing UPM's Jar File

The FileUploadBase Class

To take a closer look of the atlassian-universal-plugin-manager-plugin Jar file, a Java decompiler is needed. There are many out there, and in this case we happened to pick JD-GUI for this task. This Jar file has quite a few interesting classes in there, and the one that stands out the most for is the FileUploadBase class (in package org.apache.commons.fileupload) because of the Streams.copy statement in function parseRequest:

fileName = ((FileUploadBase.FileItemIteratorImpl.FileItemStreamImpl)item).name;
FileItem fileItem = fac.createItem(item.getFieldName(), item.getContentType(), item
          .isFormField(), fileName);
items.add(fileItem);
try
{
    Streams.copy(item.openStream(), fileItem.getOutputStream(), true);
}

... more code ...

To confrim we're on the right path, typically we would need a log file or debugger and set a breakpoint to tell us that; the second one is normally way better. Fortunately for us, Jira supports remote debugging in a rather convenient way. A documentation on Atlassian's website describes how to do this with either IntelliJ IDEA, or Eclipse.

So we set up a Java method breakpoint in IntelliJ, specifically:

Class Pattern Method Name
org.apache.commons.fileupload.FileUploadBase parseRequest

By firing the exploit again, we hit our breakpoint, which confrims we're on the right track. The Streams.copy statement specifically copies our payload to the following directory:

C:\Users\TestUser\myPlugin\target\container\tomcat8x\cargo-jira-home\temp\plug_[token]_[payloadname].jar

The thread dump from IntelliJ also tells the code path to reach the FileUploadBase class (output is modified to fit the screen):

org.apache.commons.fileupload.util.Streams.copy
org.apache.commons.fileupload.FileUploadBase.parseRequest
com.atlassian.plugins.rest.common.multipart.fileupload.CommonsFileUploadMultipartHandler.getForm
com.atlassian.plugins.rest.common.multipart.fileupload.CommonsFileUploadMultipartHandler.getForm(CommonsFileUploadMultipartHandler.java:66)
com.atlassian.plugins.rest.common.multipart.fileupload.CommonsFileUploadMultipartHandler.getFilePart(CommonsFileUploadMultipartHandler.java:32)
com.atlassian.upm.core.rest.resources.PluginCollectionResource.installFromFileSystem(PluginCollectionResource.java:254)

The last line of the dump tells us the com.atlassian.upm.core.rest.resources.PluginCollectorResource.installFromFileSystem is handling the installation on the REST API level, so we investigate.

The PluginCollectionResource Class

The installFromFileSystem function in the PluginCollectionResource tells us a lot. First off, the following is the decompiled code:

@POST
@Consumes({"multipart/form-data", "multipart/mixed"})
@XsrfProtectionExcluded
public Response installFromFileSystem(@Context MultipartHandler multipartHandler, @Context HttpServletRequest request, @DefaultValue("jar") @QueryParam("type") String type, @QueryParam("token") String token)
{
  this.permissionEnforcer.enforcePermission(Permission.MANAGE_IN_PROCESS_PLUGIN_INSTALL_FROM_FILE);
  
  UpmResources.validateToken(token, this.userManager.getRemoteUserKey(), "text/html", this.tokenManager, this.representationFactory);
  try
  {
    FilePart filePart = multipartHandler.getFilePart(request, "plugin");
    File plugin = copyFilePartToTemporaryFile(filePart, type);
    
    AsyncTask task = new InstallFromFileTask(Option.option(filePart.getName()), plugin, this.pluginInstaller, this.selfUpdateController, this.uriBuilder, this.appManager, this.i18nResolver);
    
    AsyncTaskInfo taskInfo = this.taskManager.executeAsynchronousTask(task);
    Response response = this.taskRepresentationFactory.createLegacyAsyncTaskRepresentation(taskInfo).toNewlyCreatedResponse(this.uriBuilder);
    
    String acceptHeader = request.getHeader("Accept");
    if ((acceptHeader != null) && (
      (acceptHeader.contains("text/html")) || (acceptHeader.contains("*")))) {
      return Response.fromResponse(response).type("text/html").build();
    }
    return response;
  }
  catch (IOException e)
  {
    return 
      Response.serverError().entity(this.representationFactory.createErrorRepresentation(e.getMessage())).type("application/vnd.atl.plugins.error+json").build();
  }
}

The first things you will notice are probably the method annotations at the beginning:

  • @POST - POST request is needed for this function to kick in.
  • @Consumes - Content-type such as multipart/form-data and multipart/mixed are needed in the POST request.
  • @XsrfProtectionExcluded - No XSRF protection. This is enabled by default in the Atlassian REST 3.0 API.

Second, notice from the arguments, we know the function needs a type, and by default a Jar file is expected. It also needs a token, which needs to obtained after login.

The very first thing the function does is enforcing the MANAGE_IN_PROCESS_PLUGIN_INSTALL_FROM_FILE permission, this plus token validation mean the API requires authentication to get us this privileage. While exploring Jira, we realized this pretty much means admin. It is worth mentioning that the default password for Jira is username admin, and password admin, which is asking for a compromise. However, to give Atlassian credit, a password policy feature (as well as CAPTCHA) does exist (disabled by default), which would probably save the organazation from an attack.

The rest of the code basically uses the FileUploadBase to perform the upload:

FilePart filePart = multipartHandler.getFilePart(request, "plugin");

saving the jar file:

File plugin = copyFilePartToTemporaryFile(filePart, type);

and install it (which relies on PluginInstallHandler):

AsyncTask task = new InstallFromFileTask(Option.option(filePart.getName()), plugin, this.pluginInstaller, this.selfUpdateController, this.uriBuilder, this.appManager, this.i18nResolver);

AsyncTaskInfo taskInfo = this.taskManager.executeAsynchronousTask(task);

To properly build an Atlassian plugin for a servlet, this is properly documented on the website. First, create an atlassian-plugin.xml similiar to the following:

<atlassian-plugin name="Hello World Servlet" key="example.plugin.helloworld" plugins-version="2">
    <plugin-info>
        <description>A basic Servlet module test - says "Hello World!</description>
        <vendor name="Atlassian Software Systems" url="http://www.atlassian.com"/>
        <version>1.0</version>
    </plugin-info>

    <servlet name="Hello World Servlet" key="helloWorld" class="com.example.myplugins.helloworld.HelloWorldServlet">
        <description>Says Hello World, Australia or your name.</description>
        <url-pattern>/helloworld</url-pattern>
        <init-param>
            <param-name>defaultName</param-name>
            <param-value>Australia</param-value>
        </init-param>
    </servlet>
</atlassian-plugin>

Your servlet will be accessiable within the Atlassian web applicatio via each url-pattern you specify, beneath the /plugins/servlet parent path. For example, if you specify a url-pattern of /helloworld as above, and your Atlassian application was deployed at localhost/jira, then your servlet would be accessed at localhost/jira/plugins/servlet/helloworld - simply send a GET request to this path to execute the malicious servlet.

Metasploit Module

To build a Metasploit module for this, the HttpClient mixin should be more than enough to accomplish most tasks, including logging in, obtaining a token, upload, and executing the servlet.

To build the plugin (add-on), it would have be packaged this way, as the author Alexander demonstrates:

zip = payload.encoded_jar
zip.add_file('atlassian-plugin.xml', atlassian_plugin_xml)
servlet = MetasploitPayloads.read('java', '/metasploit', 'PayloadServlet.class')
zip.add_file('/metasploit/PayloadServlet.class', servlet)
contents = zip.pack

Summary

Technically speaking, uploading will probably be looked at as a feature from the Universal Plugin Manager component, not an unintentional thing. However, since Jira does not enforce any password policy by default, plus it relies on a static password for admin right out of the box, the intial settings for UPM seem rather dangrous, which would be like magnet for attackers. And a Metasploit is perfect to test that theory.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment