阿里云集群攻击:多租户下云原生数据库安全挑战

Pasted image 20250418142315

1. 攻击流程

1.1. 云原生数据仓库 AnalyticDB PostgreSQL 版

目前处于的位置:我们的 AnalyticDB PostgreSQL 实例在 K8S Pod 上运行。

1.1.1. 特权提升漏洞利用,

通过 cronjob 任务中的特权提升漏洞,成功提升了容器内的权限至 root。在数据库容器内发现一个 cronjob 任务,该任务以 root 权限每分钟运行一次二进制文件/usr/bin/tsar

$: ls -lah /etc/cron.d/tsar 
-rw-r--r-- 1 root root 99 Apr 19  2021 /etc/cron.d/tsar 

$: cat /etc/cron.d/tsar 
MAILTO="" 
* * * * * root /usr/bin/tsar --cron > /dev/null 2>&1

在二进制文件上执行命令 ldd 时,它从自定义位置加载了共享库,该目录 /u01 对当前数据库租户用户adbpgadmin 来说是可写的,

列出了 libgcc_s.so.1 的所有者,发现它归我们的用户 adbpgadmin 所有,可以被覆盖,

$: ls -alh /u01/adbpg/lib/libgcc_s.so.1 
-rwxr-xr-x 1 adbpgadmin adbpgadmin 102K Oct 27 12:22 /u01/adbpg/lib/libgcc_s.so.1 

这意味着,如果我们能用自己的共享库覆盖这个文件,那么下次cronjob 任务执行二进制文件时,我们库的代码就会以 root 身份执行

具体流程:编译了一个共享库,作用将 /bin/bash 复制到 /bin/dash 并将其设置为 SUID,以便以 root 权限执行代码;使用 PatchELF 实用程序向 libgcc_s.so.1 库添加了一个依赖项,当加载该库时,我们自己的库也会被加载;覆盖了原始的 libgcc_s.so.1 库;等待计划任务使得 /usr/bin/tsar被执行。

Warning

注:euid 是指进程当前有效的用户ID。当euid是root时,表示进程拥有超级用户(root)权限,可以访问系统中的敏感资源和执行特权操作。具有root euid的进程在系统中具有昀高级别的权限和访问权利。

1.1.2. 横向移动

利用共享的 PID 命名空间进行横向移动到了同一Pod中的一个特权容器,从而实现了对宿主机(Kubernetes 节点)的逃逸。

作者发现客户通过管理门户执行的操作通常会在托管环境中创建各种容器和进程,可能扩大了横向移动的攻击面。

通过在阿里云门户中执行特定操作(例如启用SSL加密)
Pasted image 20250415225611
观察到了多个进程的生成,例如SCP和SSH。
这些操作可能导致了新的进程和服务的生成,增加了攻击面并可能影响横向移动的能力。
Pasted image 20250415225734

Info
  1. 发现了一个新容器,和当前数据库容器相同的PID命名空间
    1. runc init 会执行容器的初始化步骤,为容器准备运行环境
  2. 当前数据库容器并没有 /opt/adbpgmgmt.py ,说明两个容器处于不同的挂载命名空间
  3. 两个容器都有目录 /home/adbpgadmin ,所以这个目录是一个共享卷
  4. 以 adbpgadmin 用户身份运行的 SCP 命令
# 使用 adbpgadmin 用户,执行 SCP 命令将本地的 xxx_ssl_files 目录下的文件传输到远程地址的 
/home/adbpgadmin/data/master/seg-1/ 目录下
su - adbpgadmin -c scp /home/adbpgadmin/xxx_ssl_files/* 
*REDACTED*:/home/adbpgadmin/data/master/seg-1/

# 使用 SSH 进行远程连接,通过 scp 命令将文件传输到远程地址的 
/home/adbpgadmin/data/master/seg-1/ 目录下
/usr/bin/ssh -x -oForwardAgent=no -oPermitLocalCommand=no -oClearAllForwardings=yes -- *REDACTED* scp -d -t /home/adbpgadmin/data/master/seg-1/

一些生成的进程在其命令行中包含的路径在本数据库容器中不存在,进而推断 这些进程是在与我们的容器共享 PID 命名空间的不同容器中生成的。
这段 Python 脚本的作用是用于监控正在运行的进程,并尝试访问这些进程的文件系统。使用这个脚本来验证上面的推断。

import psutil 
import os 
listed = set() 
while True: 
    for proc in psutil.process_iter(): 
        try: 
            processName = proc.name() 
            processID = proc.pid 
            cmdLine = proc.cmdline() 
            # 识别名为 "scp" 的进程
            if processID not in listed and processName == 'scp': 
                # 尝试访问这个进程的文件系统信息
                os.system('ls -alh /proc/{}/root/'.format(processID)) 
                listed.add(processID) 
        except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
          pass 

脚本监控了 scp 进程并访问了其文件系统,和本数据库容器的文件系统不一样,证明推断是正确的,这个进程确实是在另一个容器中运行的。虽然两个容器不同,但它们的家目录(/home/adbpgadmin)是相同的挂载点。
Pasted image 20250415224328
Pasted image 20250415224332

现在需要在第二个容器中执行代码,由于每次重新启用 SSL 操作时都会执行 SSH 命令,而且我们的家目录是共享的,我们可以修改本地 SSH 客户端配置文件(/home/adbpgadmin/.ssh/config)。通过这种方式,我们可以配置 LocalCommand 字段,在下一次 SSH 命令执行时执行我们自己的任意命令。

SSH 客户端配置Pasted image 20250415230329

覆盖 SSH 客户端配置后,通过阿里云门户再次调用了 SSL 操作。观察到 SSH 进程的生成,命令在第二个容器中以用户身份执行,然后将 SUID 二进制文件复制到共享主目录,以便能够在第二个容器中以 root 身份执行代码。Pasted image 20250415230334
Pasted image 20250415224352

在第二个容器中,发现它具有特权,而且可以访问 Docker 的 Unix socket(/run/docker.sock),这是一种用于容器逃逸的已知元素。
Pasted image 20250415230429

利用第二个容器的权限,通过暴露的 Docker Unix socket,运行了一个新的持久化特权容器。该容器与主机(K8s 节点)共享 PID、IPC、UTS、NET、USER 和 MOUNT 命名空间,并将主机根目录挂载到/mnt。这个容器将持续存在并接收来自我们非特权容器的命令,这些命令通过位于/home/adbpgadmin 的共享管道传递。

生成这个新的“超级”容器使我们能够逃逸到主机(K8s 节点),昀终达到 K8s API,因为我们现在共享了相同的网络命名空间。我们还通过在主机允许对外部连接的情况下调用反向 shell 来避免使用共享的命名管道。这是研究中的一个重要成果,成功突破了容器间隔离并获取了对 K8s API 的访问权限。

1.1.2.1. 横向移动总结
  1. 环境: 阿里云数据库资源、数据库跑在k8s node上、可以在数据库中执行命令 Pasted image 20250415231018
  2. 通过计划任务提权到pod容器的root权限,在阿里云上启动相关的功能(就会创建一些容器(可以被发现),而且这些容器与当前的数据库Pod容器是共享PID的)这些容器都是运行在一个pod上的
    Pasted image 20250415231309
  3. 通过 SCP 将恶意程序传播到相邻容器
    相邻容器中存在 docker.sock,可以通过Docker API生成特权容器
    Pasted image 20250415231505
  4. 通过特权容器成功获取节点(宿主机)的所有权限
    Pasted image 20250415231522

1.1.3. 利用 kubelet 凭证

在节点上获取了 kubelet 的强大凭证 /etc/kubernetes/kubelet.conf ,利用这些凭证访问了敏感资源,包括密钥、服务账户和 Pod。

通过访问 K8s API 服务器,利用节点的 kubelet 凭证来检查各种集群资源,包括密钥、服务帐户和Pod。在检查 Pod 列表时,发现属于同一集群中其他租户的 Pod。这表明阿里云将集群设计为多租户,这意味着有可能获得对这些 Pod 的跨租户访问。

1.1.4. 多租户环境的发现,

发现了来自同一集群中其他租户的 Pod,表明阿里云使用该集群作为多租户环境。这意味着可能有跨租户访问的风险。

1.1.5. 私有镜像库的访问

获取了访问私有容器镜像库所需的凭证,并对其权限进行了调查。
pods 配置的一个片段,存在专用容器注册表的地址,以及拉取镜像的机密,

"spec": { 
    "containers": [ 
        { 
            "image": "*REDACTED*.eu-central-
1.aliyuncs.com/apsaradb_*REDACTED*/*REDACTED*", 
            "imagePullPolicy": "IfNotPresent", 
...            
    "imagePullSecrets": [ 
        { 
            "name": "docker-image-secret" 
        } 
    ], 

获取该机密的详细信息

$: /tmp/kubectl get secret -o json docker-image-secret 
{ 
    "apiVersion": "v1", 
    "data": { 
        ".dockerconfigjson": "eyJhdXRoc*REDACTED*" 
    }, 
    "kind": "Secret", 
    "metadata": { 
        "creationTimestamp": "2020-11-12T14:57:36Z", 
        "name": "docker-image-secret", 
        "namespace": "default", 
        "resourceVersion": "2705", 
        "selfLink": "/api/v1/namespaces/default/secrets/docker-image-secret", 
        "uid": "6cb90d8b-1557-467a-b398-ab988db27908" 
    }, 
    "type": "kubernetes.io/dockerconfigjson" 
} 

提取此密钥解密后,可以访问这些凭据,并针对容器注册表对其进行测试,

{ 
    "auths": { 
        "registry-vpc.eu-central-1.aliyuncs.com": { 
            "auth": "*REDACTED*", 
            "password": "*REDACTED*", 
            "username": "apsaradb*REDACTED*" 
        } 
    } 
} 

1.1.6. 镜像写权限发现

测试镜像库凭证时发现,不仅具有读取权限,还拥有写入权限。这表示能够篡改容器镜像,可能实施供应链攻击,影响整个服务和其他服务的镜像。
Pasted image 20250415231758

1.1.7. 总结:容器镜像拉取密钥管理不当。

在真实业务场景中,业务集群有大量节点,节点为了保证pod的正常运行,会从远端容器注册表拉取私有镜像。为了保护私有镜像的安全,防止供应链攻击,容器注册表往往会有认证和授权机制。而pod用以访问容器注册表的凭据则可能存储在Secret资源中。

1.2. 云数据库 RDS PostgreSQL 版

1.2.1. 漏洞利用

利用共享目录下符号链接漏洞获取了相邻管理容器的源代码,审计代码后发现一个 RCE 漏洞,利用了这个漏洞进一步攻击了相邻的容器。

作者在浏览数据库容器中的文件时,偶然发现了一个奇怪的目录: /tmp/tools_log
Pasted image 20250415231807
这是一个操作日志,属于另一个容器,负责对我们的数据库容器执行某些操作。日志文件提供了关于容器性质的信息,包括文件路径等。

在阿里云平台界面了一个功能,使用管理功能中激活“吊销文件配置”功能后,在日志中看到了执行命令的日志,尽管这些命令是在第二个容器中执行的,但它们修改了与当前数据库容器共享的配置文件/data/pg_hba.conf 。红色突出显示的是显示命令执行的日志。
Pasted image 20250415232319

sed -i '/^host replication aurora0.0.0.0\/0 md5$/d' /data/pg_hba.conf
Info

这个sed命令的作用是删除 /data/pg_hba.conf 文件中以特定字符串开头的行。

sed -i 命令工作原理是首先将目标文件复制到临时位置,使用正则表达式进行所需的修改,然后将编辑后的文件移回其原始位置。此行为可通过符号链接攻击被利用,从第二个容器复制文件。

使用符号链接将配置文件 pg_hba.conf 替换为对第二个容器中所需文件 k8s_ctx.py 的引用,创建了一个符号链接, k8s_ctx.py 的路径是从上面 /tmp/tools_log/docker_tools.log 中发现的,这样, pg_hba.conf 将变成一个指向 k8s_ctx.py 的符号链接。

$: unlink pg_hba.conf; ln -s *REDACTED*/operator/k8s_ctx.py pg_hba.conf

再激活“吊销文件配置”功能,将在第二个容器中启动命令,这时候运行 sed 命令,sed 命令将会对k8s_ctx.py 文件进行操作,而不是真正的 pg_hba.conf 文件。

我们读取共享的配置文件 pg_hba.conf ,
Pasted image 20250415232325
能够获取在第二个容器中运行的完整 Python 源代码,从而生成新的攻击面。

阿里云提供了一项功能,用于在进行选定的升级之前,验证PostgreSQL实例是否可以升级到更新版本。
这是为了避免损坏数据库,
Pasted image 20250415232329
在检索到的代码中审核了此功能,并发现了一个命令行注入漏洞,该漏洞允许我们在负责此操作的容器中执行代码,
Pasted image 20250415232331
install_user 参数被格式化为命令行,无任何过滤,该命令后来以 root 权限执行!但是我们能控制
install_user 吗?如果能控制,那就能利用命令注入获取到第二个容器的权限。
是的,它是从我们的数据库中选择的,查询如下:在本数据库容器中执行

select rolname from pg_authid where oid=10;

此查询返回了 PostgreSQL 超级用户角色名称 alicloud_rds_admin,即数据库的管理员用户名,常规情况下, install_user 参数值为 alicloud_rds_admin

  1. 特权容器的利用,被攻击的容器是有特权的,能够逃逸到主机(K8s 节点)。
  2. 多租户集群发现,在对节点进行基础的侦察后,发现阿里云再次使用了多租户集群。
  3. 供应链攻击的潜在风险,发现服务使用的私有容器注册库与 AnalyticDB 使用的相同,这意味着攻击者可以使用 AnalyticDB 的凭据对 RDS 进行供应链攻击。

2. 教训总结

2.1. Linux 容器之间的隔离

数据库容器和本 Pod 中其他容器共享了 PID 命名空间,这允许我们的容器访问操作其他容器,进行横向移动。所以需要精确地确定容器之间应该如何协同工作,不能单纯的直接共享命名空间。

2.2. 过度宽容的权限

在上面两个数据库 Pod 容器中,节点的 kubelet 服务账号权限过高,由于 K8s 集群是多租户的,因此可以访问其他租户的资源,所以需要将 kubelet 权限范围限制为昀低限度,以更好地将节点与其他资源隔离开来。

2.3. 容器镜像拉取密钥管理不当及权限过大

云提供商倾向于使用自己的私有容器注册表在 K8s 环境中托管容器镜像,为了能够做到这一点,K8s 节点必须可以访问注册表凭据,以便它可以拉取必要的镜像。用于拉取镜像的凭据没有正确得到管理并允许推送权限,会导致镜像投毒攻击。所以要将注册表用户权限限定为仅拉取操作。