
루트 권한 상승을 위한 신뢰할 수 있는 익스플로잇 + 라이트업. (Ubuntu 22.04에서 테스트됨)
CVE-2022-37706

안녕하세요 여러분, 이번에는 제가 Linux의 주요 창 관리자 중 하나인 Enlightenment (https://www.enlightenment.org/) 에서 발견한 최신 0-day에 대해 이야기하려고 합니다.
이 0-day는 모든 사용자를 매우 쉽고 즉시 root 권한으로 승격시킵니다.
익스플로잇은 Ubuntu 22.04에서 테스트되었지만, 어떤 배포판에서도 잘 작동할 것입니다.
우선 Enlightenment는 창 관리자, 컴포지터 및 최소 데스크톱입니다.
Linux(주요 플랫폼), BSD 및 기타 호환 UNIX 시스템을 위한 것이죠.
저는 이 창 관리자를 조금 실험해 보기 위해 설치했습니다. 흥미로웠습니다.
많은 도구가 포함되어 있고 솔직히 꽤 깔끔해 보이기 때문입니다.
apt install enlightenment으로 패키지를 설치한 후, 시스템에 설치된 파일과
디렉터리를 살펴봤습니다. 많은 모듈과 많은 헬퍼 바이너리가 있었지만,
가장 흥미로운 것은 다음과 같습니다:
➜ enlightenment cd /usr/lib/x86_64-linux-gnu/enlightenment/
➜ enlightenment find . -perm -4000
./utils/enlightenment_ckpasswd
./utils/enlightenment_system
./utils/enlightenment_sys
SUID 바이너리 몇 개를 설치합니다. 그중 하나를 사용해 root로 권한 상승을 할 수 있을지 생각해 보았습니다.
바이너리들은 모두 안전해 보이고 잘 작성되어 있었습니다.
우리가 이야기할 바이너리는 enlightenment_sys입니다.
다른 모든 대상과 마찬가지로 사전 평가를 수행한 후 적용할 전략을 선택합니다.
아직 안 보셨다면 제 블로그를 참조하세요 (https://pwn-maher.blogspot.com/2020/10/vulnerability-assessment.html)
저는 하향식(Top-Down) 접근 방식으로 코드를 감사했습니다.
그리고 이 창 관리자는 오픈 소스이므로 이 모든 바이너리와 모듈에 대한
소스 코드를 사용할 수 있습니다.
그래서 가장 먼저 한 일은 apt source enlightenment로 모든 소스 코드를 받는 것이었고,
약간의 파고들기로 대상 바이너리 코드에 도달할 수 있었습니다.
하지만 바이너리를 디버깅하기 위해 분석을 하고 주소를 확보하기 위해 Ghidra에 로드했습니다.
중단점 등을 설정할 수 있었죠.
첫 시도에서 심볼은 발견되지 않았지만, 그런 건 필요 없었습니다. 그 이유는
상대적으로 작은 바이너리였기 때문입니다.
놀랍게도 소스 코드를 직접 보는 것보다 Ghidra의 디컴파일된 의사 코드를 보는 것이
훨씬 즐거웠습니다 (매크로도 피할 수 있고, 특정 코드 블록을 컴파일하는 데
사용되는 OS에 대한 검사도 피할 수 있습니다).
그럼 분석을 시작해 봅시다.
1- 바이너리를 다뤄보기.
대상에 대한 몇 가지 정보를 확인하기 위해 file 명령어를 실행해 봅시다:

바이너리를 실행해도 아무 출력이 없습니다:

--help 인자를 주면 다음과 같은 출력이 나옵니다:

죄송하지만, 이걸로 root를 얻을 겁니다.
이제 그냥 strace를 사용해서 execve나 openat 같은 의심스러운 syscall을
사용하는지 확인해 봅시다:
strace ./enlightenment_sys 2>&1 | grep open

변조할 권한이 없는 위치에 있는 알려진 라이브러리만 엽니다.
strace ./enlightenment_sys 2>&1 | grep exec

2- 바이너리를 리버스 엔지니어링한 다음 익스플로잇해 봅시다.
새 Ghidra 프로젝트를 만들고 이 특정 바이너리를 로드했습니다.
심볼이 발견되지 않았기 때문에 entry를 이용해 main 함수를 찾을 수 있습니다.
entry 함수의 첫 번째 인자는 main 그 자체입니다.
이후 참조를 위해 이름을 main으로 바꿨습니다.
조금 아래로 스크롤하면 이미 system() 함수가 사용되고 있는 것을 볼 수 있습니다.
포너(pwner)로서 저는 이 특정 함수를 실행하기 위해 며칠씩 챌린지에 매달리곤 합니다 x)
메모리 손상 버그나 힙 문제를 찾기 위해 바이너리를 리버스했지만,
실제로는 이상한 명령 주입(Command Injection)이었습니다.
바이너리는 system을 실행하기 전에 모든 보안 예방 조치를 취하지만, 안타깝게도
우리는 항상 거기에 입력을 주입할 수 있습니다.

좋습니다. 이제 바이너리의 시작부터 system 함수까지 추적하면서
거기에 우리 입력을 주입할 방법을 찾아봅시다.
먼저 바이너리는 첫 번째 인자가 --help 또는 -h인지 확인하고 앞서 본
메시지를 표시합니다.

둘째, 권한을 root로 상승시킵니다.

다음으로, 의도하지 않은 다른 바이너리를 호출하지 않도록
거의 모든 환경 변수를 해제합니다 (보안 예방 조치).

따라서 우리가 입력한 첫 번째 인자가 "mount"이면 이 분기로 들어가고,
주어진 일부 플래그를 검사하며, 그 플래그들은 스택에 설정됩니다.
다음으로 mount 다음의 인자가 UUID=인지 확인합니다. 우리는 여기로 들어가고 싶지 않으므로
"/dev/../tmp/;/tmp/exploit"을 입력했습니다.

이렇게 하면 410행의 검사, 즉 strncmp 검사를 통과합니다.
/dev/로 시작하지 않으면 바이너리가 종료되기 때문입니다.
다음으로 우리가 제공한 파일에 대해 stat64를 호출합니다. ";"라는 폴더를 만들 수 있으며,
그것이 명령 주입을 유발합니다.
지금까지 익스플로잇은 이미 /dev/../tmp/;/tmp/exploit 파일을 생성했지만,
이것이 호출될 익스플로잇은 아닙니다.


이제 system()에 점점 가까워지고 있습니다.
이제 p (포인터)는 SUID 바이너리에 전달된 마지막 인자인
/tmp///net으로 업데이트됩니다.
왜 /tmp/net을 전달할 수 있는데 /tmp///net을 제공하는 걸까요?
우리는 다음 검사를 우회할 것입니다:
if (((next_next == (char *)0x0) || (next_next[1] == '\0')) || ((long)next_next - (long)p != 6))
/tmp/net이 존재해야 하고 /tmp///의 길이가 6이어야 했습니다.
이제 마지막 stat64가 "/dev/net"의 존재를 확인합니다.
__snprintf_chk(cmd,0x1000,1,0x1000,"/dev%s",next_next);
그리고 그것을 찾을 수 있으므로 마지막 검사를 통과합니다.
이제 일부 파일의 사용 가능 여부를 확인하지만, 이 시점에서는 중요하지 않습니다.
모든 준비가 끝났고 임의 명령 실행(arbitrary command execution)을
트리거할 단계에 거의 도달했기 때문입니다.
이제 eina_strbuf_new()는 system에 전달될 명령을 초기화할 뿐입니다.
문제는 우리가 다음과 같이 입력했다는 것입니다:
/bin/mount -o noexec,nosuid,utf8,nodev,iocharset=utf8,utf8=0,utf8=1,uid=$(id -u), "/dev/../tmp/;/tmp/exploit" /tmp///net
하지만 바이너리는 eina_strbuf_append_printf()를 여러 번 호출하여 다음과 같이 만듭니다:
/bin/mount -o noexec,nosuid,utf8,nodev,iocharset=utf8,utf8=0,utf8=1,uid=$(id -u), /dev/../tmp/;/tmp/exploit /tmp///net
큰따옴표가 제거되었고, 우리는 /tmp/exploit를
root로 호출할 수 있습니다.

바이너리는 의도하지 않은 동작을 완화하기 위해 최선을 다했지만, 늘 그렇듯
무엇이든 뚫릴 수 있습니다. 이렇게 논리적 버그를 이용해 익스플로잇할 줄은
예상하지 못했습니다.
저는 다음 CVE가 LPE root로 이어지는 메모리 손상이었으면 합니다.
Twitter 공개: https://twitter.com/maherazz2/status/1569665311707734023