Bitbucket 目录穿越导致RCE漏洞(CVE-2019-3397)分析

CVE-2019-3397的漏洞分析,第一次调试Java,Java的可读性是真的好:p

1. 漏洞简介

Bitbucket 数据中心版存在一个数据迁移工具,可以将其他服务器上的仓库导入到本机中,导入的仓库数据是一个Tar包,当攻击者拥有服务器的admin权限,可以构造恶意的Tar包并导入,由于系统在处理Tar包时,没有对获取到的路径进行有效的验证,就直接进行的文件创建操作,这就导致了目录穿越漏洞,而Tar包又是可控的,里面的hooks脚本也是可控的。我们通过目录穿越,可以将恶意的hooks脚本导入到某个仓库中,当该仓库进行git push或者git pull操作时,恶意脚本会执行,从而实现远程代码执行。

2. 测试环境简介

  • Ubuntu 16.04

  • Bitbucket 6.1.1 数据中心版

3. 利用效果

在服务端执行一个id命令。

4. 漏洞分析

4.1 漏洞原理分析

路径:

1
/WEB-INF/lib/bitbucket-service-impl-6.1.1.jar!/com/atlassian/stash/internal/migration/TarArchiveSource.class

在第46-67行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void extractToDisk(@Nonnull Path target, @Nonnull Predicate<String> filter) throws IOException {
Objects.requireNonNull(target, "target");
Objects.requireNonNull(filter, "filter");
this.read((entrySource) -> {
Path entryPath = entrySource.getPath();
String filename = entryPath.getFileName().toString();
if (filename.endsWith(".atl.deleted")) {
Path fileToDelete = entryPath.resolveSibling(filename.substring(0, filename.length() - ".atl.deleted".length()));
log.debug("Deleting entry '{}' in '{}'", fileToDelete, target);
Path path = target.resolve(fileToDelete);

try {
Files.delete(path);
} catch (NoSuchFileException var7) {
log.debug("Deleting entry '{}' in '{}' failed", new Object[]{fileToDelete, target, var7});
}
} else {
entrySource.extractToDisk(target.resolve(entryPath));
}

}, filter);
}

可以看到,代码中存在一个递归调用。

在导入时,目标为

1
/home/mengchen/atlassian/application-data/bitbucket/shared/data/repositories/153/imported-hooks

继续跟下去,很明显,在处理hooks脚本所在的数据包时,使用getPath方法获取到脚本的路径。此刻为

1
../hooks/pre-receive.d/233_bitbucket_callback

然后直接进入到了第63

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

后面又调用了第108行的extractToDisk方法。

在图中我们可以看到,传入到父类extractToDisk方法的target值为

1
/home/mengchen/atlassian/application-data/bitbucket/shared/data/repositories/153/imported-hooks/../hooks/pre-receive.d/233_bitbucket_callback

等价于

1
/home/mengchen/atlassian/application-data/bitbucket/shared/data/repositories/153/hooks/pre-receive.d/233_bitbucket_callback

然后进入父类DefaultEntrySourceextractToDisk方法,直接进行了写文件操作。

路径:

1
/Users/mengchen/IdeaProjects/BitBucket/WEB-INF/lib/bitbucket-service-impl-6.1.1.jar!/com/atlassian/stash/internal/migration/DefaultEntrySource.class

在这个过程中,没有对获得的路径进行任何的过滤,这就导致了一个目录穿越漏洞。目录穿越本身危害并不是太大,那么是如何造成RCE的呢?
其实此处的RCE是结合了Git仓库的本身功能,从某种意义上来说,算是打了一个combo吧。

在Git仓库中,有一个特殊的功能,当客户端或者服务端发生某种操作时,会调用特定的脚本来执行,一般分为两大类,客户端钩子和服务端钩子。在这里我们利用到的就是服务端钩子。在默认情况下,导入的仓库的钩子脚本会导入到imported-hooks目录下,而生效的脚本是在同级目录的hooks下。我们构造了一个恶意的Tar包,在其中存储的hooks脚本的路径为../hooks/pre-receive.d/233_bitbucket_callbackBitbucket在导入过程中,没有对获得的路径进行一个验证,使得我们可以将导入的脚本写入到hooks文件夹下,在该仓库发生git push或者git pull操作时,我们写入的恶意脚本就会执行了。

4.2 漏洞补丁分析

我们来看一下6.1.2版本的TarArchiveSource

1
路径: atlassian-bitbucket-6.1.2/app/WEB-INF/lib/bitbucket-service-impl-6.1.2.jar!/com/atlassian/stash/internal/migration/TarArchiveSource.class

可以看到,在源码中添加了一个方法

1
2
3
4
5
private static boolean isRelative(@Nonnull Path entryPath) {
IntStream var10000 = IntStream.range(0, entryPath.getNameCount());
entryPath.getClass();
return var10000.mapToObj(entryPath::getName).map(Path::toString).anyMatch(".."::equals);
}

当获得的路径中存在..时,抛出异常,不进行导入操作,这样就修复了目录穿越的漏洞。

5. 修复方式

  • 升级Bitbucket 服务器到最新版本
  • 缓解措施:禁用Bitbucket的导入功能,将bitbucket.properties文件中feature.data.center.migration.import的值设置为false,然后重启服务器使配置生效。如果还需运行导入任务,请在隔离的集群节点中运行。

6. 参考链接