
Root 익스플로잇 for CVE-2021-4034 (PwnKit) — pkexec의 out-of-bounds write를 악용하여 Linux 시스템에서 root 권한으로 권한 상승을 수행합니다.
PwnKit 취약점에 대한 루트 익스플로잇입니다. 원본 보고서는 여기에서 확인하세요.
이 익스플로잇은 대상 시스템 소유자의 명시적 허가를 받은 상태에서만 사용하세요.
libc 외에는 의존성이 필요 없습니다. 그냥 make를 실행하세요.
옵션 없이 실행하면 익스플로잇이 실행됩니다:
[linux@linux ~]$ ./exploit
-----------------------------------------------------------------------------
__\ / __ __ _ __ _ __ | \ / _ ___
/ V |_ --- _)/ \ _)/| ---|_|/ \__)|_| | V |_) _/|_|
\__ |__ /__\_//__ | |\_/__) | | | \/__| |
-----------------------------------------------------------------------------
sh-5.1# whoami
root
sh-5.1#
pkexec의 경로와 "from" 문자셋을 사용자 지정할 수 있습니다:
[linux@linux ~]$ ./exploit -h
...
./exploit [-c] [-h] [-f from_charset] [-p /path/to/pkexec]
-----------------------------------------------------------------------------
-c 정리만 수행 - 익스플로잇하지 않음
-p <path> pkexec 경로 (기본값: "/usr/bin/pkexec")
-f <from_charset> 사용자 지정 "from" 문자셋 (기본값: "UTF-8")
-h 이 메시지 표시
GIO_USE_VFS는 도대체 뭐가 문제일까?!소셜 미디어에서 일부 익스플로잇이 GIO_USE_VFS=가 정의되지 않으면 실패하는 이유를 묻는 사람들을 몇 명 봤습니다. 왜 이전 버전에서는 작동하는 걸까요?
polkit의 커밋 daf3d5c2d15466a267221fcb099c59c870098e03이 범인입니다.
다음은 diff의 관련 부분입니다:
--- a/src/programs/pkexec.c
+++ b/src/programs/pkexec.c
@@ -503,6 +503,9 @@ main (int argc, char *argv[])
opt_user = NULL;
local_agent_handle = NULL;
+ /* GIO의 원격 파일 접근을 비활성화합니다. */
+ setenv ("GIO_USE_VFS", "local", 1);
+
/* 올바른 호출인지 확인 */
if (geteuid () != 0)
{
이 커밋 이전 버전은 GIO_USE_VFS 변수를 정의할 필요 없이 익스플로잇이 가능합니다. 이후 버전은 이 변수가 정의되지 않으면 익스플로잇이 불가능합니다.
이 커밋의 목적은 사실 헛다리입니다. 중요한 것은 변수의 의미가 아니라 변수의 존재가 프로그램 환경에 어떤 영향을 미치는가입니다. 진실을 알려면 libc를 살펴봐야 합니다.
libc에서 프로세스의 환경은 이 전역 변수가 가리키는 char * 배열로 표현됩니다:
char **environ;
environ은 힙에 존재하며 때때로 재배치됩니다. 이미 어디로 흘러가는지 눈치채셨을 수도 있습니다. setenv.c의 이 코드 스니펫을 확인해 보세요:
#if !_LIBC
# define __environ environ
# ifndef HAVE_ENVIRON_DECL
extern char **environ;
# endif
#endif
int
__add_to_environ (const char *name, const char *value, const char *combined,
int replace)
{
char **ep;
// ... 생략
ep = __environ;
size = 0;
if (ep != NULL)
{
for (; *ep != NULL; ++ep)
if (!strncmp (*ep, name, namelen) && (*ep)[namelen] == '=')
break;
else
++size;
}
if (ep == NULL || __builtin_expect (*ep == NULL, 1))
{
char **new_environ;
/* 이 공간은 우리가 할당했습니다; 확장할 수 있습니다. */
new_environ = (char **) realloc (last_environ,
(size + 2) * sizeof (char *));
// ... 생략
last_environ = __environ = new_environ;
}
__add_to_environ()은 setenv(3)와 putenv(3) 모두에서 호출되어 약속한 대로 환경 변수를 설정합니다. 해당 환경 변수가 정의되어 있지 않으면, 새 항목(새 환경 key=value 쌍에 대한 포인터)을 수용하기 위해 environ을 재할당해야 합니다. 정의되어 있으면 environ 배열의 크기는 변경되지 않으므로 재할당할 이유가 없습니다. 간결함을 위해 해당 부분은 생략했습니다 - 직접 확인해 보시길 권장합니다.
이제 익스플로잇으로 돌아가 보겠습니다. 여기까지 오셨다면 아마 이 익스플로잇의 방법론을 이미 알고 계실 것입니다 (모르신다면 원본 보고서를 확인해 주세요).
우리는 빈 프로그램 인자(argv)를 pkexec에 전달하여 환경 변수를 몰래 주입하려고 합니다. argc가 진짜로 비어 있으면(프로그램 이름조차 없음), 인접한 환경 변수들이 인자와 충돌합니다. 우리는 이 동작을 악용하여 pkexec가 대상 실행 파일의 정규 경로를 환경에 쓰도록 강제합니다. 그러나 코드의 이 부분에 도달하기 전에 다음이 실행됩니다:
setenv ("GIO_USE_VFS", "local", 1);
이 변수가 환경에 존재하지 않으면 environ이 재할당되어 argv와 충돌하지 않게 됩니다. 결과적으로 경계를 벗어난 쓰기가 프로그램의 환경에 영향을 미치지 않아 익스플로잇이 실패하게 됩니다.