Impact

In Bitbucket the four different user roles Bitbucket User, Project Creator, Admin and System Admin exist. An attacker with the permissions of the role Admin can abuse Bitbucket’s Data Center Migration tool to drop an executable shell script in an arbitrary directory. This is caused by a directory traversal within a TAR archive. In order to gain remote code execution, the attacker can drop a Git hook which is executed if a special event occurs in the repository e.g. a pull or push request. The vulnerable Data Center Migration tool was introduced in version 5.14 of Bitbucket Server and can be exploited with a Bitbucket Data Center license.

The truncated analysis results are available in our RIPS demo application. Please note that we limited the results to the issues described in this post in order to ensure a fix is available.

Bitbucket’s Migration Endpoint

The Data Center Migration tool allows Admins or System Admins to migrate Git repositories from Bitbucket Server to Bitbucket Data Center. To start the migration process the admin has to export the repositories from the Bitbucket Server instance first. During the export process a TAR archive with the following structure is being created.

Example TAR archive

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
_/repository/hierarchy_begin/c3b3efc5cb93609ad4fc
_/repository/hierarchy_end/c3b3efc5cb93609ad4fc
com.atlassian.bitbucket.server.bitbucket-instance-migration_instanceDetails/instance-details.json.atl.gz
com.atlassian.bitbucket.server.bitbucket-instance-migration_metadata/project_68/project.json.atl.gz
com.atlassian.bitbucket.server.bitbucket-instance-migration_metadata/project_68/repository_59.json.atl.gz
com.atlassian.bitbucket.server.bitbucket-instance-migration_permissions/project/68/all-permissions.json.atl.gz
com.atlassian.bitbucket.server.bitbucket-instance-migration_permissions/project/68/permissions.json.atl.gz
com.atlassian.bitbucket.server.bitbucket-instance-migration_permissions/repository/59/permissions.json.atl.gz
com.atlassian.bitbucket.server.bitbucket-git_git/repositories/59/hooks/hooks.atl.tar.atl.gz
com.atlassian.bitbucket.server.bitbucket-git_git/repositories/59/contents/objects.atl.tar
com.atlassian.bitbucket.server.bitbucket-git_git/repositories/59/metadata/metadata.atl.tar.atl.gz
com.atlassian.bitbucket.server.bitbucket-git-lfs_gitLfsSettings/59/git-lfs-settings.json.atl.gz

As we can see, the exported TAR archive contains multiple GZIP and TAR compressed files. Especially the file hooks.atl.tar.atl.gz looks suspicious since it contains Git hooks which are scripts that are executed every time a paticular event occurs in a Git repository. Manipulating such a TAR archive entry with the ../../ notation and starting the import process leads to a Remote Code Execution vulnerability as described in the next section.

Insecure Archive extraction

During the import process of a repository the Git hooks from the file hooks.atl.tar.atl.gz are stored in the directory ${BITBUCKET_DATA}/shared/data/repositories/${REPO_ID}/imported-hooks/ and are therefore ignored from being executed since the regular hooks of a repository are stored in the directory ${BITBUCKET_DATA}/shared/data/repositories/${REPO_ID}/hooks/. However, if an attacker controls the contents of the file hooks.atl.tar.atl.gz it is possible to traverse out of the intended directory and drop a hook in an arbitrary directory. This is caused by insecure extraction of the GZip compressed TAR file.

The following code snippet shows the simplified function extractToDisk which takes the path to the file hooks.atl.tar.atl.gz as parameter target. Then the function read is called with the lambda expression in line 4-9. This lambda expression implements the function accept of the interface IoConsumer<T>.

Listing 1: Insecure Extraction of Archives

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public void extractToDisk(@Nonnull Path target, @Nonnull Predicate<String> filter) throws IOException {
	
        this.read((entrySource) -> {
            Path entryPath = entrySource.getPath();
            String filename = entryPath.getFileName().toString();
	    
            entrySource.extractToDisk(target.resolve(entryPath));
            
        }, filter);
    }

The following code snippet shows the function declaration of the function read. This function iterates over all archive entries and calls the function accept on an object of the class TarEntrySource containing the user input. We can see that the unsanitized user input from the source org.apache.commons.compress.archivers.tar.TarArchiveEntry.getName() (Listing 2, line 7) reaches the sensitive sink java.nio.Paths.get() (Listing 2, line 9) indicating a Path Traversal vulnerability.

Since the function accept is implemented by the above defined lambda expression, we can track the user input to the to the function call of TarEntrySource.extractToDisk() (Listing 1 line 7).

Listing 2: Reading the TAR archive

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public void read(@Nonnull IoConsumer<EntrySource> reader,
                 @Nonnull Predicate<String> filter) throws IOException {
    
    TarArchiveEntry entry;
    while ((entry = (TarArchiveEntry) inputStream.getNextEntry()) != null) {
        InputStream entryInputStream = new CloseShieldInputStream(inputStream);
        String name = entry.getName();
        if (filter.test(name)) {
            reader.accept(new TarEntrySource(entryInputStream, Paths.get(name), entry));
        }
    }
 }

The following Listing shows the function extractToDisk of the class TarEntrySource which takes the unsanitized path as function parameter. We can see that all sub directories of the path are created (Listing 3, line 5) and the file is copied into that directory (Listing 3, line 8).

Listing 3: Dropping the File

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
private static class TarEntrySource extends DefaultEntrySource {
      
    public void extractToDisk(@Nonnull Path target) throws IOException {
      
     Files.createDirectories(target.getParent());
     OutputStream out = new FileOutputStream(target.toFile());
      
     IoUtils.copy(this.inputStream, out, 32768);
      
     PosixFileAttributeView fileAttributeView = (PosixFileAttributeView)Files.getFileAttributeView(target, PosixFileAttributeView.class);
     fileAttributeView.setPermissions(FilePermissionUtils.toPosixFilePermissions(this.tarArchiveEntry.getMode()));

    }
  }

This path traversal vulnerability enables an attacker to drop a Git hook in an attacker controlled Bitbucket repository. However, if the file permissisons of the shell script are not set properly e.g. the execute bit is not set the Git hook is not being executed. Interesting to mention is that a TAR archive contains meta information of a file entry like the modification date, the user name, the group name and the file mode (file permissions). In line 11 of Listing 3 the file permissions are set to the corresponding permissions of the archive entry.

Timeline

Date What
2019/02/27 Reported the Path Traversal vulnerability to Atlassian.
2019/03/11 Atlassian confirmed the vulnerability and assigned issue BSERV-11706.
2019/04/01 Atlassian fixed the issue in Bitbucket 6.1.2.
2019/05/22 Atlassian published a security advisory for Bitbucket.


Summary

In this post we have analysed how the insecure server-side extraction of a TAR archive leads to a critical vulnerability in Bitbucket. Bitbucket is used by millions of developers world-wide which introduces a special interest for attackers. There are different attack scenarios to exploit this issue. A Bitbucket User could lure a user of the role Admin (not System Admin) to import a malicious TAR archive in order to gain control of the remote Bitbucket server or a malicious Admin can exploit this issue by himself. It is highly recommended to update Bitbucket Data Center to the most recent version. We would like to thank the Atlassian team for the professional collaboration on fixing this issue.