导语:Alpine是一款面向安全应用的轻量级Linux发行版本,常作为Docker镜像使用。在研究过程中,我们发现了Alpine Linux默认包管理器APK的一些漏洞。该漏洞允许网络中间人(或恶意包镜像)在用户的机器上实现任意代码执行。

概述

Alpine是一款面向安全应用的轻量级Linux发行版本,常作为Docker镜像使用。在研究过程中,我们发现了Alpine Linux默认包管理器APK的一些漏洞。该漏洞允许网络中间人(或恶意包镜像)在用户的机器上实现任意代码执行。这一漏洞的后果尤为严重,因为在使用默认仓库(Repositories)的过程中,并不会通过TLS的方式提供包。目前,这一漏洞已经得到修复,并且Alpine的基础镜像已经更新。受漏洞影响的用户需要及时对镜像进行更新。

在获得代码执行漏洞利用后,我想出了一个很酷的方法,通过写入/proc/<pid>/mem,使原始apk进程退出,并返回状态码(Exit Code)0,且该方法不需要SYS_PTRACE功能。经过尝试,结果证明安装带有apk的包的Dockerfile后,漏洞仍然能够成功利用,并且能够创建成功。

以下是我在Alpine环境下,通过中间人攻击的方式,对Docker容器进行漏洞利用的过程视频:

https://justi.cz/assets/apkpoc.mp4

漏洞详情:任意文件创建导致远程代码执行

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,其中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将不会发生变化。原因在于,这个目录中已经包含我们的Payload,所以取消/etc/apk/commit_hooks.d/的链接时将会失败,出现ENOTEMPTY的错误。

修改状态码

现在,在apk退出之前,我们能够在客户端上运行任意代码。需要解决的问题就是,如何找到一种方法能让apk进程完美的退出。如果在Dockerfile创建的步骤使用apk,那么apk将会返回非0的状态码(Exit Code),这一步骤也会失败。

如果我们什么都不做,那么apk会返回一个等于它无法安装的软件包数量的状态码。目前为止,无法安装的软件包至少有一个。但有趣的是,这个值存在溢出漏洞,如果错误数量%256==0,那么这个过程就会返回0,也就是我们想要的状态码返回值。目前,该漏洞已经修复,详见:https://github.com/alpinelinux/apk-tools/commit/7b654e125461b00bc26e52b25e6a7be3a32c11b9

我首先尝试使用gdb附加到进程,并且只调用exit(0)。但不幸的是,Docker容器默认没有SYS_PTRACE功能,所以我们不能这样做。但是,由于我们是root用户,所以我们可以为apk进程读取和写入/proc/<pid>/mem。

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、编写Shellcode,最终将exit(0)直接写入内存。这一步能够成功我觉得非常惊讶,我本来以为会写入失败。

当我们的提交挂钩退出后,apk恢复执行,就会运行我们的Shellcode。

总结

如果你在生产环境中使用Alpine Linux,你应该首先重建镜像,然后考虑向该项目组捐款表示支持(https://wiki.alpinelinux.org/wiki/Alpine_Linux:Developers)。原因在于,apk项目的一个主要开发人员只花费了不到一周时间就修复了这一漏洞(https://github.com/fabled),并且随后不久,Alpine的维护团队就发布了新的版本(https://github.com/ncopa)。考虑到目前可能有数百个组织在生产环境中使用了Alpine Linux,因此这些组织可能已经受到了该漏洞的威胁,我们建议尽快开展自查,并及时更新。

源链接

Hacking more

...