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
|
然后进入父类DefaultEntrySource
的extractToDisk
方法,直接进行了写文件操作。
路径:
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_callback
,Bitbucket
在导入过程中,没有对获得的路径进行一个验证,使得我们可以将导入的脚本写入到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. 参考链接