漏洞分析 - Atlassian Bitbucket Data Center目录穿越漏洞到RCE(CVE-2019-3397)

2019-09-12 约 356 字 预计阅读 2 分钟

声明:本文 【漏洞分析 - Atlassian Bitbucket Data Center目录穿越漏洞到RCE(CVE-2019-3397)】 由作者 arr0w1 于 2019-09-12 08:50:43 首发 先知社区 曾经 浏览数 157 次

感谢 arr0w1 的辛苦付出!

基础概念

Atlassian Bitbucket Data Center:一个本地的Git仓库管理系统,具有高可用性等优点,适合大型企业使用。


  • Bitbucket中的四种用户角色(user roles):
    • Bitbucket User
    • Project Creator
    • Admin
    • System Admin

Git Hooks:Git具有在特定事件发生之前或之后执行特定脚本代码功能,类似于监听事件、触发器之类的东西。
Git Hooks就是当Git执行特定事件(如commit、push等)所触发执行的那些脚本。

  • 按照Git Hooks脚本所在的位置可以分为两类:
    • 1.本地Hooks,触发事件如pull、push、commit、merge等。
    • 2.服务端Hooks,触发事件如receive等。

TAR压缩包文件(TAR archive)包含了压缩包中每个文件条目(a file entry)的元信息:
修改日期(modification date)
用户名(user name)
组名(group name)
文件权限(file permissions)
...

漏洞信息

漏洞描述:
Atlassian Bitbucket Data Center目录穿越漏洞(CVE-2019-3397),具体在系统中的Data Center migration tool功能,存在目录穿越漏洞。能够实现Path Traversal to RCE。实际上是对“使用GZip压缩过的TAR存档文件”的不安全提取(解压)造成的,即提取的过程中未做足够的安全处理。

漏洞影响:
一个远程攻击者(具有管理员权限的已授权的用户)可以利用这个路径遍历漏洞,将文件写入任意位置,从而导致在运行Bitbucket Data Center的某些版本的系统上,可以执行远程代码。
没有Data Center license的Bitbucket Server版本,不受影响。

Bitbucket Data Center受影响的版本:
5.13.0 <= version < 5.13.6
5.14.0 <= version < 5.14.4
5.15.0 <= version < 5.15.3
5.16.0 <= version < 5.16.3
6.0.0 <= version < 6.0.3
6.1.0 <= version 6.1.2

修复建议:
升级版本 Bitbucket Data Center >= 6.1.2

漏洞评级:
CVSS v3 score: 9.1 => Critical severity

Exploitability Metrics

Attack Vector:Network
Attack Complexity:Low
Privileges Required:High
User Interaction:None

利用条件:具有Admin角色权限的用户(攻击者),即可使用Data Center Migration tool将任意文件(如webshell)上传到任意目录。

引入版本:存在该漏洞的功能是Data Center Migration tool,它是在Bitbucket Server的5.14版本中引入的,可以利用Bitbucket Data Center license进行利用。

RCE原理:利用该目录穿越漏洞,看起来只能任意文件上传,所以攻击者借助Git Hooks可以释放(解压)一个Git hook,当这个存储库中发生了特定事件(如pull或push请求),会执行该Git hook,即执行脚本,从而实现RCE。

更多信息参考 https://jira.atlassian.com/browse/BSERV-11706

漏洞分析

数据中心迁移工具(Data Center Migration tool),能够使管理员(Admins)或系统管理员(System Admins)将若干个Git仓库从Bitbucket Server迁移到Bitbucket Data Center。

  • 迁移过程
    • 1.把仓库导出:管理员必须先从Bitbucket Server实例中导出Git仓库。【得到一个TAR存档压缩包】
    • 2.把仓库导入:把Git仓库导入到目标系统(Bitbucket Data Center)。Importing - Atlassian Documentation
      【漏洞利用】

在导出过程中,会创建具有以下结构的TAR存档压缩包:

_/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

可见这个TAR存档压缩包中包含了多个压缩包文件(GZIP和TAR)。
尤其要注意这个文件hooks.atl.tar.atl.gz,它包含了Git hooks,这些hook其实是脚本,每当Git仓库中发生特定事件时执行对应脚本。
通过构造包含../的TAR压缩包,即可在导入过程中导致远程代码执行(下面具体解释)。

正常情况

导入Git仓库到目标系统(Bitbucket Data Center)的过程中,文件hooks.atl.tar.atl.gz中的Git hooks按照预期都存储在目录${BITBUCKET_DATA}/shared/data/repositories/${REPO_ID}/imported-hooks/中,但是这些Git hooks都不会被执行,因为Git仓库的正常Git hooks都存储在目录${BITBUCKET_DATA}/shared/data/repositories/${REPO_ID}/hooks/中。

目录穿越

这个目录穿越漏洞,实际上是对“使用GZip压缩过的TAR存档文件”的不安全提取(解压)造成的。

在非正常情况下,攻击者控制了迁移过程中第一步的结果——导出的“TAR存档压缩包”,其实是控制了其中的关键文件hooks.atl.tar.atl.gz的内容,通过构造特制的TAR存档压缩包从而利用目录穿越漏洞,从预期的解压目标目录中做穿越,实现在任意目录中释放(解压出)一个文件,这个文件可以是Git hook。

看下Bitbucket Data Center如何对“使用GZip压缩过的TAR存档文件”做提取(解压)。

代码片段1: extractToDisk函数的具体实现

(具体实现经过一定简化)该函数将TAR存档压缩包中的关键文件压缩包hooks.atl.tar.atl.gz中具体条目TAR archive entry的“实际的文件路径”作为形参target的值。

这个lambda表达式(第4-9行)实现了接口IoConsumer<T>accept函数。

“Lambda 表达式”(lambda expression)是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。Lambda表达式可以表示闭包。

代码片段1:

public void extractToDisk(@Nonnull Path target, @Nonnull Predicate<String> filter) throws IOException {
        this.read((entrySource) -> {
            //lambda expression
            Path entryPath = entrySource.getPath();
            String filename = entryPath.getFileName().toString();

            entrySource.extractToDisk(target.resolve(entryPath));

        }, filter);
    }

代码片段2: read函数的具体实现

使用while循环得到所有的归档条目(archive entries);
最后一行,调用了reader对象的accept函数(传入的实参是一个TarEntrySource对象),TarEntrySource对象中的name是使用entry.getName();方法获得的,即org.apache.commons.compress.archivers.tar.TarArchiveEntry.getName()方法,这样得到的是“用户输入数据”且未做任何安全过滤,并作为实参直接传递给敏感的java.nio.Paths.get()

代码片段2:

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));
        }
    }
 }

因为accept函数是由定义的lambda表达式实现的,所以看下TarEntrySource.extractToDisk()函数的具体实现(代码片段3)。

可见,类TarEntrySourceextractToDisk函数的实参是:用户输入的未做任何安全过滤的“路径”。

对每一个压缩包中的每一个条目TarArchiveEntry(压缩包中的每个文件)所做的逻辑:
第5行 先使用java.io.File.getParent()获取该文件的父目录(即包含../的目录),Files.createDirectories会根据给定的“路径”,会依次创建所有不存在的目录。(如/xx/yy/zz则依次创建3个目录)
(补充对比 Files.createDirectory()方法只能创建一个目录,如果这个目录的上级目录是不存在的,就会报错)

第6行 java.nio.file.Path.toFile返回了一个File对象,表示这个真实路径的文件对象(空,待写入数据)。
参考 Java Code Examples java.nio.file.Path.toFile

第8行IoUtils.copy将压缩包中的某个文件的二进制流,写到对应目录中。(buffer大小为32768 bytes即32kb)

成功将压缩包中的某个文件的二进制流,“复制”到了对应目录。

代码片段3:

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()));

    }
  }

这个路径穿越漏洞,使攻击者能够在攻击者可控的Bitbucket repository中释放(解压)出一个文件,如Git hook。

注意文件权限,如果没有正确设置shell脚本的文件权限,如没有或无法设置文件的执行位(execute bit),那么Git Hook当然就无法被执行。

根据代码片段3可知,该系统在文件权限上,也直接信任了用户输入:
代码片段3的最后一行fileAttributeView.setPermissions函数,直接将文件权限设置为存档条目(archive entry)中原本的文件权限,即使用this.tarArchiveEntry.getMode()获取的元信息中的文件权限。

所以可以上传一个有执行权限的文件。

总结

Bitbucket被全球数以百万计的开发人员使用。有各种攻击场景可以利用这个漏洞。
场景一:Bitbucket用户可以诱导Admin角色的用户(不是System Admin角色)导入恶意的TAR存档文件,以便获得对远程Bitbucket server的控制。
场景二:获取Admin角色的用户登录凭证后,利用这个漏洞getshell。

参考 https://blog.ripstech.com/2019/bitbucket-path-traversal-to-rce/

关键词:[‘安全技术’, ‘漏洞分析’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now