目录
3. 运行 execve() 后 capabilities 的变化
因为后面需要学习Docker的逃逸,理解Linux Capabilities是很有必要的,这里的基础概念与基本应用都是龙哥总结好的,这篇内容我也是学习+总结这些基础知识和基本使用
首先介绍一下Linux Capabilities是什么
Linux Capabilities指的是Linux操作系统中的一种权限管理机制,它允许对进程的权限进行细粒度的控制,以便实现最小特权原则。这些capabilities可以被分配给进程,允许它们执行特定的系统操作,而无需具有完整的超级用户权限。Capabilities可以帮助减少系统中权限过大的进程数量,从而增强系统的安全性。
Linux 是一种安全的操作系统,它把所有的系统权限都赋予了一个单一的 root 用户,只给普通用户保留有限的权限。
两种办法:
1.是通过 sudo
提升权限,如果用户很多,配置管理和权限控制会很麻烦;
2.是通过 SUID(Set User ID on execution)来实现,它可以让普通用户允许一个 owner
为 root 的可执行文件时具有 root 的权限。
SUID
的概念比较晦涩难懂,举个例子就明白了,以常用的 passwd
命令为例,修改用户密码是需要 root 权限的,但普通用户却可以通过这个命令来修改密码,这就是因为 /bin/passwd
被设置了 SUID
标识,所以普通用户执行 passwd 命令时,进程的 owner 就是 passwd 的所有者,也就是 root 用户。
SUID
虽然可以解决问题,但却带来了安全隐患。
当运行设置了 SUID
的命令时,通常只是需要很小一部分的特权,但是 SUID
给了它 root 具有的全部权限。这些可执行文件是黑客的主要目标,如果他们发现了其中的漏洞,就很容易利用它来进行安全攻击。比如说find命令给与suid权限就可以反弹shell操作
为了对 root 权限进行更细粒度的控制,实现按需授权,Linux 引入了另一种机制叫
capabilities
。
Capabilities
机制是在 Linux 内核 2.2
之后引入的,原理很简单,就是将之前与超级用户 root(UID=0)关联的特权细分为不同的功能组,Capabilites 作为线程(Linux 并不真正区分进程和线程)的属性存在,每个功能组都可以独立启用和禁用。
其本质上就是将内核调用分门别类,具有相似功能的内核调用被分到同一组中。
这样一来,权限检查的过程就变成了:在执行特权操作时,如果线程的有效身份不是 root,就去检查其是否具有该特权操作所对应的 capabilities,并以此为依据,决定是否可以执行特权操作。
Capabilities 可以在进程执行时赋予,也可以直接从父进程继承。
所以理论上如果给 nginx 可执行文件赋予了 CAP_NET_BIND_SERVICE
capabilities,那么它就能以普通用户运行并监听在 80 端口上。
下面是一些常见的capability:
capability 名称 | 描述 |
---|---|
CAP_AUDIT_CONTROL | 启用和禁用内核审计;改变审计过滤规则;检索审计状态和过滤规则 |
CAP_AUDIT_READ | 允许通过 multicast netlink 套接字读取审计日志 |
CAP_AUDIT_WRITE | 将记录写入内核审计日志 |
CAP_BLOCK_SUSPEND | 使用可以阻止系统挂起的特性 |
CAP_CHOWN | 修改文件所有者的权限 |
CAP_DAC_OVERRIDE | 忽略文件的 DAC 访问限制 |
CAP_DAC_READ_SEARCH | 忽略文件读及目录搜索的 DAC 访问限制 |
CAP_FOWNER | 忽略文件属主 ID 必须和进程用户 ID 相匹配的限制 |
CAP_FSETID | 允许设置文件的 setuid 位 |
CAP_IPC_LOCK | 允许锁定共享内存片段 |
CAP_IPC_OWNER | 忽略 IPC 所有权检查 |
CAP_KILL | 允许对不属于自己的进程发送信号 |
CAP_LEASE | 允许修改文件锁的 FL_LEASE 标志 |
CAP_LINUX_IMMUTABLE | 允许修改文件的 IMMUTABLE 和 APPEND 属性标志 |
CAP_MAC_ADMIN | 允许 MAC 配置或状态更改 |
CAP_MAC_OVERRIDE | 忽略文件的 DAC 访问限制 |
CAP_MKNOD | 允许使用 mknod() 系统调用 |
CAP_NET_ADMIN | 允许执行网络管理任务 |
CAP_NET_BIND_SERVICE | 允许绑定到小于 1024 的端口 |
CAP_NET_BROADCAST | 允许网络广播和多播访问 |
CAP_NET_RAW | 允许使用原始套接字 |
CAP_SETGID | 允许改变进程的 GID |
CAP_SETFCAP | 允许为文件设置任意的 capabilities |
CAP_SETPCAP | 参考 capabilities man page |
CAP_SETUID | 允许改变进程的 UID |
CAP_SYS_ADMIN | 允许执行系统管理任务,如加载或卸载文件系统、设置磁盘配额等 |
CAP_SYS_BOOT | 允许重新启动系统 |
CAP_SYS_CHROOT | 允许使用 chroot() 系统调用 |
CAP_SYS_MODULE | 允许插入和删除内核模块 |
CAP_SYS_NICE | 允许提升优先级及设置其他进程的优先级 |
CAP_SYS_PACCT | 允许执行进程的 BSD 式审计 |
CAP_SYS_PTRACE | 允许跟踪任何进程 |
CAP_SYS_RAWIO | 允许直接访问 /devport、/dev/mem、/dev/kmem 及原始块设备 |
CAP_SYS_RESOURCE | 忽略资源限制 |
CAP_SYS_TIME | 允许改变系统时钟 |
CAP_SYS_TTY_CONFIG | 允许配置 TTY 设备 |
CAP_SYSLOG | 允许使用 syslog() 系统调用 |
CAP_WAKE_ALARM | 允许触发一些能唤醒系统的东西(比如 CLOCK_BOOTTIME_ALARM 计时器) |
Linux capabilities 分为进程 capabilities 和文件 capabilities。
每一个线程,具有 5 个 capabilities 集合,每一个集合使用 64
位掩码来表示,显示为 16
进制格式。
这 5 个 capabilities 集合分别是:
Permitted
Effective
Inheritable
Bounding
Ambient
每个集合中都包含零个或多个 capabilities。这5个集合的具体含义如下:
定义了线程能够使用的 capabilities 的上限。
它并不使能线程的 capabilities,而是作为一个规定。也就是说,线程可以通过系统调用 capset()
来从 Effective
或 Inheritable
集合中添加或删除 capability,前提是添加或删除的 capability 必须包含在 Permitted
集合中(其中 Bounding 集合也会有影响,具体参考下文)。 如果某个线程想向 Inheritable
集合中添加或删除 capability,首先它的 Effective
集合中得包含 CAP_SETPCAP
这个 capabiliy。
简单的理解:这个集合就是表示你最多可以有哪些权限
内核检查线程是否可以进行特权操作时,检查的对象便是 Effective
集合。
如之前所说,Permitted
集合定义了上限,线程可以删除 Effective 集合中的某 capability,随后在需要时,再从 Permitted 集合中恢复该 capability,以此达到临时禁用 capability 的功能。
简单理解:这个集合就是表示在上线的范围内,也就是在Permitted范围内,哪些权限是有效的
当执行exec()
系统调用时,能够被新的可执行文件继承的 capabilities,被包含在 Inheritable
集合中。
这里需要说明一下,包含在该集合中的 capabilities 并不会自动继承给新的可执行文件,即不会添加到新线程的 Effective
集合中,它只会影响新线程的 Permitted
集合。
简单理解:这个就是表示子进程可以继承父进程的集合
Bounding
集合是 Inheritable
集合的超集,如果某个 capability 不在 Bounding
集合中,即使它在 Permitted
集合中,该线程也不能将该 capability 添加到它的 Inheritable
集合中。
Bounding 集合的 capabilities 在执行 fork()
系统调用时会传递给子进程的 Bounding 集合,并且在执行 execve
系统调用后保持不变。
这个集合有以下几点是需要注意的:
当线程运行时,不能向 Bounding 集合中添加 capabilities。
一旦某个 capability 被从 Bounding 集合中删除,便不能再添加回来。
将某个 capability 从 Bounding 集合中删除后,如果之前 Inherited
集合包含该 capability,将继续保留。但如果后续从 Inheritable
集合中删除了该 capability,便不能再添加回来。
简单的理解:就是所有可以执行权限的集合
Linux 4.3
内核新增了一个 capabilities 集合叫 Ambient
,用来弥补 Inheritable
的不足。
Ambient
具有如下特性:
Permitted
和 Inheritable
未设置的 capabilities,Ambient
也不能设置。
当 Permitted
和 Inheritable
关闭某权限(比如 CAP_SYS_BOOT
)后,Ambient
也随之关闭对应权限。这样就确保了降低权限后子进程也会降低权限。
非特权用户如果在 Permitted
集合中有一个 capability,那么可以添加到 Ambient
集合中,这样它的子进程便可以在 Ambient
、Permitted
和 Effective
集合中获取这个 capability。
Ambient
的好处显而易见,举个例子,如果你将 CAP_NET_ADMIN
添加到当前进程的 Ambient
集合中,它便可以通过 fork()
和 execve()
调用 shell 脚本来执行网络管理任务,因为 CAP_NET_ADMIN
会自动继承下去。
我们可以分别查看一下root用户和普通的用户,他们的cap权限都是多少
[root@centos111 user1]# cat /proc/$$/status |grep Cap
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000001fffffffff
CapAmb: 0000000000000000
这里的普通用户是没有权限的
root@utuntu000:~# cat /proc/$$/status |grep Cap
CapInh: 0000000000000000
CapPrm: 0000003fffffffff
CapEff: 0000003fffffffff
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000
这里的root用户是有权限的
文件的 capabilities 被保存在文件的扩展属性中。
如果想修改这些属性,需要具有 CAP_SETFCAP
的 capability。
文件与线程的 capabilities 共同决定了通过 execve()
运行该文件后的线程的 capabilities。
文件的 capabilities 功能,需要文件系统的支持,如果文件系统使用了 nouuid
选项进行挂载,那么文件的 capabilities 将会被忽略。
类似于线程的 capabilities,文件的 capabilities 包含了 3 个集合:
Permitted
Inheritable
Effective
这3个集合的具体含义如下:
这个集合中包含的 capabilities,在文件被执行时,会与线程的 Bounding 集合计算交集,然后添加到线程的 Permitted
集合中。
这个集合与线程的 Inheritable
集合的交集,会被添加到执行完 execve()
后的线程的 Permitted
集合中。
这不是一个集合,仅仅是一个标志位。
如果设置开启,那么在执行完 execve()
后,线程 Permitted
集合中的 capabilities 会自动添加到它的 Effective
集合中。对于一些旧的可执行文件,由于其不会调用 capabilities 相关函数设置自身的 Effective
集合,所以可以将可执行文件的 Effective bit 开启,从而可以将 Permitted
集合中的 capabilities 自动添加到 Effective
集合中。
详情请参考 Linux capabilities 的 man page。
上面介绍了线程和文件的 capabilities,你们可能会觉得有些抽象难懂。
下面通过具体的计算公式,来说明执行 execve()
后 capabilities 是如何被确定的。
我们用 P
代表执行 execve()
前线程的 capabilities,P'
代表执行 execve()
后线程的 capabilities,F
代表可执行文件的 capabilities。那么:
P’(ambient) = (file is privileged) ? 0 : P(ambient)
P’(permitted) = (P(inheritable) & F(inheritable)) | (F(permitted) & P(bounding))) | P’(ambient)
P’(effective) = F(effective) ? P’(permitted) : P’(ambient)
P’(inheritable) = P(inheritable) [i.e., unchanged]
P’(bounding) = P(bounding) [i.e., unchanged]
我们一条一条来解释:
如果用户是 root 用户,那么执行 execve()
后线程的 Ambient
集合是空集;如果是普通用户,那么执行 execve()
后线程的 Ambient
集合将会继承执行 execve()
前线程的 Ambient
集合。
执行 execve()
前线程的 Inheritable
集合与可执行文件的 Inheritable
集合取交集,会被添加到执行 execve()
后线程的 Permitted
集合;可执行文件的 capability bounding 集合与可执行文件的 Permitted
集合取交集,也会被添加到执行 execve()
后线程的 Permitted
集合;同时执行 execve()
后线程的 Ambient
集合中的 capabilities 会被自动添加到该线程的 Permitted
集合中。
如果可执行文件开启了 Effective 标志位,那么在执行完 execve()
后,线程 Permitted
集合中的 capabilities 会自动添加到它的 Effective
集合中。
执行 execve()
前线程的 Inheritable
集合会继承给执行 execve()
后线程的 Inheritable
集合。
这里有几点需要着重强调:
上面的公式是针对系统调用 execve()
的,如果是 fork()
,那么子线程的 capabilities 信息完全复制父进程的 capabilities 信息。
可执行文件的 Inheritable
集合与线程的 Inheritable
集合并没有什么关系,可执行文件 Inheritable
集合中的 capabilities 不会被添加到执行 execve()
后线程的 Inheritable
集合中。如果想让新线程的 Inheritable
集合包含某个 capability,只能通过 capset()
将该 capability 添加到当前线程的 Inheritable
集合中(因为 P’(inheritable) = P(inheritable))。
如果想让当前线程 Inheritable
集合中的 capabilities 传递给新的可执行文件,该文件的 Inheritable
集合中也必须包含这些 capabilities(因为 P’(permitted) = (P(inheritable) & F(inheritable))|…)。
将当前线程的 capabilities 传递给新的可执行文件时,仅仅只是传递给新线程的 Permitted
集合。如果想让其生效,新线程必须通过 capset()
将 capabilities 添加到 Effective
集合中。或者开启新的可执行文件的 Effective 标志位(因为 P’(effective) = F(effective) ? P’(permitted) : P’(ambient))。
在没有 Ambient
集合之前,如果某个脚本不能调用 capset()
,但想让脚本中的线程都能获得该脚本的 Permitted
集合中的 capabilities,只能将 Permitted
集合中的 capabilities 添加到 Inheritable
集合中(P’(permitted) = P(inheritable) & F(inheritable)|…),同时开启 Effective 标志位(P’(effective) = F(effective) ? P’(permitted) : P’(ambient))。有 有 Ambient
集合之后,事情就变得简单多了,后续的文章会详细解释。
如果某个 UID 非零(普通用户)的线程执行了 execve()
,那么 Permitted
和 Effective
集合中的 capabilities 都会被清空。
从 root 用户切换到普通用户,那么 Permitted
和 Effective
集合中的 capabilities 都会被清空,除非设置了 SECBIT_KEEP_CAPS 或者更宽泛的 SECBIT_NO_SETUID_FIXUP。
关于上述计算公式的逻辑流程图如下所示(不包括 Ambient
集合):
下面我们用一个例子来演示上述公式的计算逻辑,以 ping
文件为例。
如果我们将 CAP_NET_RAW
capability添加到 ping 文件的 Permitted
集合中(F(Permitted)),它就会添加到执行后的线程的 Permitted
集合中(P’(Permitted))。
由于 ping 文件具有 capabilities 感知能力,即能够调用 capset()
和 capget()
,它在运行时会调用 capset()
将 CAP_NET_RAW
capability 添加到线程的 Effective
集合中。
即,文件系统有Permitted权限,根据公式 P’(effective) = F(effective) ? P’(permitted) : P’(ambient) ,因为文件系统没有e权限
换句话说,如果可执行文件不具有 capabilities 感知能力,我们就必须要开启 Effective 标志位(F(Effective)),这样就会将该 capability 自动添加到线程的 Effective
集合中。具有capabilities 感知能力的可执行文件更安全,因为它会限制线程使用该 capability 的时间。
我们也可以将 capabilities 添加到文件的 Inheritable
集合中,文件的 Inheritable
集合会与当前线程的 Inheritable
集合取交集,然后添加到新线程的 Permitted
集合中。这样就可以控制可执行文件的运行环境。
看起来很有道理,但有一个问题:如果可执行文件的有效用户是普通用户,且没有 Inheritable
集合,即 F(inheritable) = 0
,那么 P(inheritable)
将会被忽略(P(inheritable) & F(inheritable))。
由于绝大多数可执行文件都是这种情况,因此 Inheritable
集合的可用性受到了限制。我们无法让脚本中的线程自动继承该脚本文件中的 capabilities,除非让脚本具有 capabilities 感知能力。
要想改变这种状况,可以使用 Ambient
集合。
Ambient
集合会自动从父线程中继承,同时会自动添加到当前线程的 Permitted
集合中。举个例子,在一个 Bash 环境中(例如某个正在执行的脚本),该环境所在的线程的 Ambient
集合中包含 CAP_NET_RAW
capability,那么在该环境中执行 ping 文件可以正常工作,即使该文件是普通文件(没有任何 capabilities,也没有设置 SUID)。
最后拿 docker 举例,如果你使用普通用户来启动官方的 nginx 容器,会出现以下错误:
bind() to 0.0.0.0:80 failed (13: Permission denied)
因为 nginx 进程的 Effective
集合中不包含 CAP_NET_BIND_SERVICE
capability,且不具有 capabilities 感知能力(普通用户),所以启动失败。
要想启动成功,至少需要将该 capability 添加到 nginx 文件的 Inheritable
集合中,同时开启 Effective 标志位,并且在 Kubernetes Pod 的部署清单中的 securityContext –> capabilities 字段下面添加 NET_BIND_SERVICE
(这个 capability 会被添加到 nginx 进程的 Bounding
集合中),最后还要将 capability 添加到 nginx 文件的 Permitted
集合中。如此一来就大功告成了,参考公式:P’(permitted) = …|(F(permitted) & P(bounding)))|…,P’(effective) = F(effective) ? P’(permitted) : P’(ambient)。
如果容器开启了 securityContext/allowPrivilegeEscalation
,上述设置仍然可以生效。如果 nginx 文件具有 capabilities 感知能力,那么只需要将 CAP_NET_BIND_SERVICE
capability 添加到它的 Inheritable
集合中就可以正常工作了。
当然了,除了上述使用文件扩展属性的方法外,还可以使用 Ambient
集合来让非 root 容器进程正常工作,但 Kubernetes 目前还不支持这个属性,具体参考 Kubernetes 项目的 issue。
Linux 系统中主要提供了两种工具来管理 capabilities:libcap
和 libcap-ng
。
libcap
提供了 getcap
和 setcap
两个命令来分别查看和设置文件的 capabilities,同时还提供了 capsh
来查看当前 shell 进程的 capabilities。
libcap-ng
更易于使用,使用同一个命令 filecap
来查看和设置 capabilities。
安装很简单,以 CentOS 为例,可以通过以下命令安装:
yum install -y libcap
如果想查看当前 shell 进程的 capabilities,可以用 capsh
命令。
下面是 CentOS 系统中的 root 用户执行 capsh
的输出:
[root@centos111 ~]# capsh --print
Current: = cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,35,36+ep
Bounding set =cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,35,36
Securebits: 00/0x0/1'b0
secure-noroot: no (unlocked)
secure-no-suid-fixup: no (unlocked)
secure-keep-caps: no (unlocked)
uid=0(root)
gid=0(root)
groups=0(root)
解释一下:
Current : 表示当前 shell 进程的 Effective capabilities 和 Permitted capabilities。可以包含多个分组,每一个分组的表示形式为 capability[,capability…]+(e|i|p)
,其中 e
表示 effective,i
表示 inheritable,p
表示 permitted。不同的分组之间通过空格隔开
例如:Current: = cap_sys_chroot+ep cap_net_bind_service+eip
。
再举一个例子:cap_net_bind_service+e cap_net_bind_service+ip
和 cap_net_bind_service+eip
等价。
Bounding set : 这里仅仅表示 Bounding 集合中的 capabilities,不包括其他集合,所以分组的末尾不用加上 +...
。
Securebits : 我也没搞清楚这是个什么鬼。
这个命令输出的信息比较有限,完整的信息可以查看 /proc 文件系统,比如当前 shell 进程就可以查看 /proc/$$/status
。
其中一个重要的状态就是 NoNewPrivs
,可以通过以下命令查看:
grep NoNewPrivs /proc/$$/status
NoNewPrivs: 0
根据 prctl(2) 中的描述,自从 Linux 4.10 开始,/proc/[pid]/status
中的 NoNewPrivs
值表示了线程的 no_new_privs
属性。至于 no_new_privs
究竟是干嘛的,下面来解释一下。
一般情况下,execve()
系统调用能够赋予新启动的进程其父进程没有的权限,最常见的例子就是通过 setuid
和 setgid
来设置程序进程的 uid 和 gid 以及文件的访问权限。
这就给不怀好意者钻了不少空子,可以直接通过 fork 来提升进程的权限,从而达到不可告人的目的。
为了解决这个问题,Linux 内核从 3.5 版本开始,引入了 no_new_privs
属性(实际上就是一个 bit,可以开启和关闭),提供给进程一种能够在 execve()
调用整个阶段都能持续有效且安全的方法。
开启了 no_new_privs
之后,execve 函数可以确保所有操作都必须调用 execve()
判断并赋予权限后才能被执行。这就确保了线程及子线程都无法获得额外的权限,因为无法执行 setuid 和 setgid,也不能设置文件的权限。
一旦当前线程的 no_new_privs
被置位后,不论通过 fork,clone 或 execve 生成的子线程都无法将该位清零。
Docker 中可以通过参数 --security-opt
来开启 no_new_privs
属性,
例如:docker run --security-opt=no_new_privs busybox
。
下面通过一个例子来体会一下 no_new_privs
属性的作用。
首先撸一段 C 代码,显示当前进程的有效用户 id:
cat testnnp.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc, char *argv[])
{
printf("Effective uid: %d\n", geteuid());
return 0;
}
编译:
make testnnp
cc testnnp.c -o testnnp
将可执行文件打入 docker 镜像中:
cat Dockerfile
FROM fedora:latest
ADD testnnp /root/dockerfile/testnnp
RUN chmod +s /root/dockerfile/testnnp
ENTRYPOINT /root/dockerfile/testnnp/testnnp
在包含Dockerfile的目录中,打开终端并执行以下命令来构建Docker镜像:
[root@centos111 dockerfile]# docker build -t testnnp .
[+] Building 10.9s (8/8) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 134B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/fedora:latest 0.9s
=> [internal] load build context 0.0s
=> => transferring context: 14.50kB 0.0s
=> [1/3] FROM docker.io/library/fedora:latest@sha256:06df381d697d14940c886fda8e94a4fdc838df74e93f65111ed3ea04f 9.3s
=> => resolve docker.io/library/fedora:latest@sha256:06df381d697d14940c886fda8e94a4fdc838df74e93f65111ed3ea04f 0.0s
=> => sha256:06df381d697d14940c886fda8e94a4fdc838df74e93f65111ed3ea04f7a7d6e0 975B / 975B 0.0s
=> => sha256:dfb5e6183f515192b37df9356622b676461a41b724d9f92953433dca3e85deb1 529B / 529B 0.0s
=> => sha256:8404925a71fd9f56243a4c54fe44f1192e29a19d23e7c858842ddc8b43b4ca9e 2.00kB / 2.00kB 0.0s
=> => sha256:8237fce9fd6b5acc12e7010d84e6245c6b00937de832c1987c4769866af1573a 64.87MB / 64.87MB 6.2s
=> => extracting sha256:8237fce9fd6b5acc12e7010d84e6245c6b00937de832c1987c4769866af1573a 3.0s
=> [2/3] ADD testnnp /root/testnnp 0.1s
=> [3/3] RUN chmod +s /root/testnnp 0.4s
=> exporting to image 0.1s
=> => exporting layers 0.0s
=> => writing image sha256:29345ec2c7ab9231eb64b4e9bf4ee0959a7155e3bfe7fbfa7bae559eb6dff52f 0.0s
=> => naming to docker.io/library/testnnp 0.0s
等待Docker镜像构建完成后,您可以使用以下命令来查看新构建的镜像:
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
testnnp latest 29345ec2c7ab 8 minutes ago 177MB
最后,您可以使用以下命令来运行新构建的镜像:
docker run -it myimage
下面来做两个实验:
先在没有开启 no-new-privileges
的情况下启动容器:
[root@centos111 dockerfile]# docker run --security-opt=no-new-privileges=false testnnp
Effective uid: 0
从输出结果来看,只要给可执行文件设置了 SUID 标识,即使我们使用普通用户(UID=1000)来运行容器,进程的有效用户也会变成 root。
接着在开启 no-new-privileges
的前提下启动容器,以防止执行设置了 SUID 标识的可执行文件进行 UID 转换:
docker run -it --rm --user=1000 --security-opt=no-new-privileges testnnp
Effective uid: 1000
可以看到,开启了 no_new_privs
属性之后,即使可执行文件设置了 SUID 标识,线程的有效用户 ID 也不会变成 root。
这样即使镜像中的代码有安全风险,仍然可以通过防止其提升权限来避免受到攻击。
Kubernetes 也可以开启 no_new_privs
,不过逻辑稍微复杂一点。
当 Pod 的 SecurityContext
定义下的 allowPrivilegeEscalation
字段值为 false 时(默认就是 false),如果不满足以下任何一个条件,就会开启 no_new_privs
属性:
设置了 privileged=true
增加了 CAP_SYS_ADMIN
capabilities,即 capAdd=CAP_SYS_ADMIN
以 root 用户运行,即 UID=0
例如,当设置了 privileged=true
和 allowPrivilegeEscalation=false
时,就不会开启 no_new_privs
属性。同理,设置了 capAdd=CAP_SYS_ADMIN
和 allowPrivilegeEscalation=false
也不会开启 no_new_privs
属性。
可以通过 getcap
来查看文件的 capabilities,例如:
getcap /bin/ping /usr/sbin/arping
/bin/ping = cap_net_admin,cap_net_raw+p
/usr/sbin/arping = cap_net_raw+p
也可以使用 -r
参数来递归查询:
getcap -r /usr 2>/dev/null
/usr/bin/ping = cap_net_admin,cap_net_raw+p
/usr/bin/newgidmap = cap_setgid+ep
/usr/bin/newuidmap = cap_setuid+ep
/usr/bin/gnome-keyring-daemon = cap_ipc_lock+ep
/usr/sbin/mtr = cap_net_raw+ep
/usr/sbin/arping = cap_net_raw+p
/usr/sbin/clockdiff = cap_net_raw+p
/usr/sbin/suexec = cap_setgid,cap_setuid+ep
如果想查看某个进程的 capabilities,可以直接使用 getpcaps
,后面跟上进程的 PID:
getpcaps 1099
Capabilities for `1099': = cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,35,36+ep
如果想查看一组相互关联的线程的 capabilities(比如 nginx),可以这么来看:
getpcaps $(pgrep nginx)
Capabilities for `1183': = cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,35,36+ep
Capabilities for `1184': =
这里你会看到只有主线程才有 capabilities,子线程和其他 workers 都没有 capabilities,这是因为只有 master 才需要特殊权限,例如监听网络端口,其他线程只需要响应请求就好了。
设置文件的 capabilities 可以使用 setcap
,语法如下:
setcap CAP+set filename
例如,将 CAP_CHOWN
和 CAP_DAC_OVERRIDE
capabilities 添加到 permitted
和 effective
集合:
setcap CAP_CHOWN,CAP_DAC_OVERRIDE+ep 1.txt
如果想移除某个文件的 capabilities,可以使用 -r
参数:
setcap -r filename
安装也很简单,以 CentOS 为例:
yum install libcap-ng-utils
libcap-ng 使用 filecap
命令来管理文件的 capabilities。有几个需要注意的地方:
filecap 添加删除或查看 capabilities 时,capabilities 的名字不需要带 CAP_
前缀(例如,使用 NET_ADMIN
代替 CAP_NET_ADMIN
);
filecap 不支持相对路径,只支持绝对路径;
filecap 不允许指定 capabilities 作用的集合,capabilities 只会被添加到 permitted
和 effective
集合。
查看文件的 capabilities:
filecap /full/path/to/file
递归查看某个目录下所有文件的 capabilities:
filecap /full/path/to/dir
例如:
filecap /usr/bin/
file capabilities
/usr/bin/newgidmap setgid
/usr/bin/newuidmap setuid
/usr/bin/gnome-keyring-daemon ipc_lock
注意 : filecap 只会显示“capabilities 被添加到
permitted
和effective
集合中”的文件。所以这里没有显示 ping 和 arping。
递归查看整个系统所有文件的 capabilities:
$ filecap /
# or
$ filecap -a
设置文件的 capabilities 语法如下:
$ filecap /full/path/to/file cap_name
例如:
$ filecap /usr/bin/tac dac_override
移除某个文件的 capabilities:
$ filecap /full/path/to/file none
到此capabilities的基本知识和用法就介绍完毕了,实际应用将会在下一篇中会进行介绍