본문 바로가기

카테고리 없음

[CVE-2019-6111] scp client missing received object name validation

아침부터 Facebook에 SCP 관련 취약점들이 포스팅되길래 부랴부랴 이슈 검색하고, 소스 다운로드 받아서 분석해봤습니다.

이전에는 몰랐는데 scp 바이너리를 실행할 때, -v 옵션을 사용하니 생각보다 많은 정보들이 출력되네요 ^^...


일단 다운로드 받은 openssh 소스에서 scp.c 파일을 열고, scp 바이너리를 실행해서 소스를 분석해보겠습니다.

OpenSSH Github : https://github.com/openssh/openssh-portable 


먼저, 디렉토리와 파일을 대상으로 scp 바이너리를 실행해서 로그를 확인했습니다.

아래 로그는 scp를 실행할 때 -v 옵션을 사용하면 출력되는 로그에서 취약점 분석에 필요한 로그만 발췌한 것입니다.


[ 파일 복사 ]

debug1: Sending environment.

debug1: Sending env LANG = ko_KR.UTF-8

debug1: Sending env LC_CTYPE = ko_KR.UTF-8

debug1: Sending command: scp -v -f ~/CMD

Sending file modes: C0644 17 CMD

Sink: C0644 17 CMD

CMD                                                              100%   17     1.3KB/s   00:00

debug1: client_input_channel_req: channel 0 rtype exit-status reply 0

debug1: client_input_channel_req: channel 0 rtype eow@openssh.com reply 0

debug1: channel 0: free: client-session, nchannels 1

debug1: fd 0 clearing O_NONBLOCK

debug1: fd 1 clearing O_NONBLOCK

Transferred: sent 2448, received 1992 bytes, in 0.0 seconds

Bytes per second: sent 72820.3, received 59255.7

debug1: Exit status 0 



[ 디렉토리 복사 ]

debug1: Sending environment.

debug1: Sending env LANG = en_US.UTF-8

debug1: Sending env LC_CTYPE = ko_KR.UTF-8

debug1: Sending command: scp -v -r -f ~/test

Sink: D0755 0 test

Entering directory: D0755 0 b

Sink: D0755 0 b

Sink: E

Sending file modes: C0644 0 a

Sink: C0644 0 a

a                                                                100%    0     0.0KB/s   00:00

Sink: E 



[ 서버에 없는 파일 복사 시도 ]

debug1: Sending environment.

debug1: Sending env LANG = ko_KR.UTF-8

debug1: Sending env LC_CTYPE = ko_KR.UTF-8

debug1: Sending command: scp -v -f ~/NonExists

debug1: client_input_channel_req: channel 0 rtype exit-status reply 0

debug1: client_input_channel_req: channel 0 rtype eow@openssh.com reply 0

debug1: channel 0: free: client-session, nchannels 1

Sink: \001scp: /home/beautyjang/NonExists: No such file or directory

scp: /home/beautyjang/NonExists: No such file or directory 


위 로그에서 "Sink:..." 로그가 중요합니다. 해당 로그는 sink 함수에서 출력하는데, sink 함수는 서버의 파일을 내부로 복사할 때 호출되는 함수입니다.

sink 함수를 분석하면 서버의 응답 데이터를 분석하고, 그 내용에 따라 디렉터리를 생성하거나 파일을 생성하는 코드를 확인할 수 있습니다.


분석 내용을 간략하게  정리하면 다음과 같습니다.


1) 세션을 종료할 때는 '\x02' 문자가 전송되고, 프로그램이 종료된다.

2) 서버에 없는 파일을 복사하면 '\x01' 문자가 전송되고, 에러를 카운팅한다.

3) 파일을 복사하면 'C'가 전송되고, 서버에서 전달받은 파일명으로 파일을 생성하고 데이터를 기록한다.

4) 디렉토리를 복사하면 'D'가 전송되고, 디렉토리 생성 후 디렉터리 변수를 생성한 디렉터리로 변경한 뒤 재귀호출한다.

5) 디렉토리에 대한 복사가 완료되면 'E'가 전송되고, 재귀호출 이전 상태로 돌아간다.


위 분석 내용에서 취약점과 관련이 있는 코드는 3번 항목에 있는 파일을 복사하는 케이스입니다.


SCP 클라이언트는 파일 복사 명령어(scp -f [파일명])를 서버에 전송하고, sink 함수를 호출합니다.


서버는 대상 파일이 존재하는지 확인하고, 아래와 같은 포멧의 데이터를 전송합니다.

 [C][퍼미션] [파일크기] [파일명]

  ex) C0755 74 test.txt


sink 함수는 [파일크기]만큼 데이터를 읽어서 대상 디렉터리에 [파일명]으로 파일을 생성하고(혹은 기존 파일을 덮어쓰거나), chmod 모드 함수를 사용해서 [퍼미션]에 있는 값으로 퍼미션을 설정합니다.


아래 코드는 sink 함수에서 발췌한 것인데, 파일을 생성하기 전에 파일명에 Directory Traversal 패턴이 포함되어 있는지 확인합니다.

        if (*cp == '\0' || strchr(cp, '/') != NULL ||

            strcmp(cp, ".") == 0 || strcmp(cp, "..") == 0) {

            run_err("error: unexpected filename: %s", cp);

            exit(1);

        } 


파일명에 Directory Traversal 공격 패턴이 없으면 open 함수를 사용해서 파일을 생성하고, 서버의 데이터를 읽어서 파일에 기록합니다. 같은 이름의 파일이 있으면 서버에서 받은 데이터로 덮어써집니다. 코드를 보면 알겠지만 어디에도 서버에서 전달받은 [파일명]이 클라이언트가 요청한 파일이 맞는지 확인하는 코드가 없습니다.

         if (targisdir) { // 복사되는 위치가 디렉터리인 경우

            static char *namebuf;

            static size_t cursize;

            size_t need;


            need = strlen(targ) + strlen(cp) + 250;

            if (need > cursize) {

                free(namebuf);

                namebuf = xmalloc(need);

                cursize = need;

            }

            (void) snprintf(namebuf, need, "%s%s%s", targ,

                strcmp(targ, "/") ? "/" : "", cp); // cp = 서버에서 전달받은 파일명

            np = namebuf;

        } else

            np = targ;


        ... (생략)


        if ((ofd = open(np, O_WRONLY|O_CREAT, mode)) < 0) { // np = 복사 대상 디렉토리 + "/" + 서버에서 전달받은 파일명

bad:            run_err("%s: %s", np, strerror(errno));

            continue;

        }

        (void) atomicio(vwrite, remout, "", 1);

        if ((bp = allocbuf(&buffer, ofd, COPY_BUFLEN)) == NULL) {

            (void) close(ofd);

            continue;

        } 


        ... (생략)


        for (count = i = 0; i < size; i += bp->cnt) { // 파일 데이터를 읽어서 버퍼에 저장

            amt = bp->cnt;

            if (i + amt > size)

                amt = size - i;

            count += amt;

            do {

                j = atomicio6(read, remin, cp, amt,

                    scpio, &statbytes);

                if (j == 0) {

                    run_err("%s", j != EPIPE ?

                        strerror(errno) :

                        "dropped connection");

                    exit(1);

                }

                amt -= j;

                cp += j;

            } while (amt > 0);


            if (count == bp->cnt) {

                /* Keep reading so we stay sync'd up. */

                if (wrerr == NO) {

                    if (atomicio(vwrite, ofd, bp->buf, // 버퍼에 있는 데이터를 파일에 기록

                        count) != count) {

                        wrerr = YES;

                        wrerrno = errno;

                    }

                }

                count = 0;

                cp = bp->buf;

            }

        }


코드 분석 내용에 따르면 scp 바이너리를 실행할 때, 복사되는 위치가 디렉터리이면 서버에서 받은 파일명이 아무런 검증없이 경로명에 사용되어 대상 디렉토리에 있는 임의의 파일을 덮어쓰거나 퍼미션을 변경할 수 있는 것으로 확인됩니다.


예를들어 "scp test@192.168.100.2:~/test.txt ~/"와 같은 명령어를 실행하면 파일을 열 때 "~/[파일명]"이 사용되고, 서버가 test.txt가 아닌 "C0755 200 aaaa.txt"를 응답해도 정상적으로 처리되어 "~/aaaa.txt" 파일이 생성됩니다.


테스트는 OpenSSH 소스를 변경해서 진행했습니다.(paramiko로 만들려니 귀찮아서 ...)

아래 코드는 서버에서 전송한 데이터의 파일명("test.txt")을 "vuln.txt"으로 변경하는 코드입니다.

        if (verbose_mode)

            fmprintf(stderr, "Sink: %s", buf);


        if (strstr(buf, "test.txt") != NULL) {

            char *ptr = strstr(buf, "test.txt");

            sprintf(ptr, "%s", "vuln.txt");

        } 


위와 같이 sink 함수의 코드를 일부 변경하고, 빌드 후 "./scp [USER]@[SERVER-IP]:~/test.txt ./" 명령어를 실행하면 현재 디렉터리에 vuln.txt 파일이 생성되는 것을 확인할 수 있습니다.


분석 내용에 따르면 해당 취약점은 scp의 대상 위치가 디렉터리일 경우 발생하며, 클라이언트가 요청한 파일과 관련없이 서버가 대상 디렉터리에 임의의 파일을 생성하거나 기존 파일을 덮어쓸 수 있고, 퍼미션을 변경할 수 있는 취약점입니다. 아쉽게도(?) 상위 디렉터리로 이동하는건 불가능합니다.