
이것은 CVE-2024-6387을 위해 제가 작성한 POC입니다.
Qualys Security Advisory
regreSSHion: OpenSSH 서버에서의 RCE, glibc 기반 Linux 시스템에서 (CVE-2024-6387)
요약 SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3 (Debian 3.0r6, 2005)
필요한 것은 도약에 대한 믿음뿐
-- The Interrupters, "Leap of Faith"
예비 참고: OpenSSH는 세계에서 가장 안전한 소프트웨어 중 하나입니다. 이 취약점은 거의 완벽한 구현에서 발생한 한 번의 실수입니다. 그 심층 방어 설계와 코드는 모범이자 영감이며, OpenSSH 개발자들의 모범적인 작업에 감사드립니다.
우리는 OpenSSH 서버(sshd)에서 취약점(시그널 핸들러 레이스 조건)을 발견했습니다. 클라이언트가 LoginGraceTime 초(기본 120초, 구버전 OpenSSH에서는 600초) 내에 인증하지 않으면, sshd의 SIGALRM 핸들러가 비동기적으로 호출됩니다. 그러나 이 시그널 핸들러는 async-signal-safe 하지 않은 다양한 함수(예: syslog())를 호출합니다. 이 레이스 조건은 sshd의 기본 구성에 영향을 미칩니다.
조사 결과, 이 취약점은 실제로 CVE-2006-5051 ("OpenSSH 4.4 이전 버전의 시그널 핸들러 레이스 조건으로 원격 공격자가 서비스 거부(충돌)를 유발하고 잠재적으로 임의 코드를 실행할 수 있음")의 회귀(regression)라는 것을 알게 되었습니다. 이는 2006년 Mark Dowd에 의해 보고되었습니다.
이 회귀는 2020년 10월(OpenSSH 8.5p1) 커밋 752250c ("OpenSSH를 위한 개정된 로그 인프라")에 의해 도입되었으며, 이 커밋이 실수로 sigdie()에서 "#ifdef DO_LOG_SAFE_IN_SIGHAND"를 제거했습니다. sigdie()는 sshd의 SIGALRM 핸들러에 의해 직접 호출되는 함수입니다. 다시 말해:
OpenSSH < 4.4p1은 CVE-2006-5051에 대한 백포트 패치가 적용되지 않았거나, CVE-2006-5051에 대한 잘못된 수정이었던 CVE-2008-4109에 대한 패치가 적용되지 않은 경우 이 시그널 핸들러 레이스 조건에 취약합니다.
4.4p1 <= OpenSSH < 8.5p1은 이 시그널 핸들러 레이스 조건에 취약하지 않습니다. (CVE-2006-5051 패치가 sigdie()에 추가한 "#ifdef DO_LOG_SAFE_IN_SIGHAND"가 이 안전하지 않은 함수를 안전한 _exit(1) 호출로 변환했기 때문입니다.)
8.5p1 <= OpenSSH < 9.8p1은 이 시그널 핸들러 레이스 조건에 다시 취약합니다. ("#ifdef DO_LOG_SAFE_IN_SIGHAND"가 sigdie()에서 실수로 제거되었기 때문입니다.)
이 취약점은 glibc 기반 Linux 시스템에서 원격으로 익스플로잇 가능하며, syslog() 자체가 async-signal-unsafe 함수(예: malloc() 및 free())를 호출합니다. 이는 루트 권한으로 인증되지 않은 원격 코드 실행으로 이어집니다. sshd의 권한 있는 코드에 영향을 미치며, 이 코드는 샌드박스 처리되지 않고 전체 권한으로 실행됩니다. 우리는 다른 libc나 운영체제는 조사하지 않았습니다. 하지만 OpenBSD는 분명히 취약하지 않습니다. 그 이유는 OpenBSD의 SIGALRM 핸들러가 syslog_r()을 호출하는데, 이는 2001년 OpenBSD가 고안한 async-signal-safer 버전의 syslog()이기 때문입니다.
이 취약점을 원격으로 익스플로잇하기 위해 (우리가 아는 한, CVE-2006-5051은 이전에 성공적으로 익스플로잇된 적이 없습니다), 우리는 2001년 Michal Zalewski가 발표한 선구적인 논문 "Delivering Signals for Fun and Profit"에서 영감을 얻었습니다.
https://lcamtuf.coredump.cx/signals.txt
그럼에도 불구하고, 우리는 즉시 세 가지 주요 문제에 직면했습니다:
이론적 관점에서, SIGALRM에 의해 적절한 시점에 중단되었을 때 sshd를 불일치 상태로 만드는 유용한 코드 경로를 찾아야 하며, 그런 다음 이 불일치 상태를 SIGALRM 핸들러 내에서 익스플로잇해야 합니다.
실제적 관점에서, 우리는 sshd에서 이 유용한 코드 경로에 도달할 방법을 찾고, 적절한 시점에 중단시킬 가능성을 최대화해야 합니다.
타이밍 관점에서, 우리는 이 유용한 코드 경로를 적절한 시점에 중단시킬 가능성을 더욱 높일 방법을 원격으로 찾아야 합니다.
최신 운영체제 보호 기능(특히 ASLR 및 NX)에 즉시 대처하지 않고 이 세 가지 문제에 집중하기 위해, 우리는 먼저 i386의 구버전 OpenSSH를 익스플로잇하고, 이를 바탕으로 최신 버전을 익스플로잇하기로 결정했습니다:
첫째, "debian-30r6-dvd-i386-binary-1_NONUS.iso"의 "SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3": 이는 권한 분리(privilege separation)가 기본적으로 활성화되어 있고 그 시대의 모든 중요한 취약점(특히 CVE-2003-0693 및 CVE-2002-0640)에 대해 패치된 최초의 데비안 버전입니다.
이 버전을 원격으로 익스플로잇하기 위해, free() 호출을 SIGALRM으로 중단시키고(sshd의 공개 키 파싱 코드 내에서), 힙을 불일치 상태로 만든 다음, SIGALRM 핸들러 내에서 다른 free() 호출 중 이 불일치 상태를 악용합니다.
실험에서 이 레이스 조건을 이기기 위해 평균 약 10,000번의 시도가 필요했습니다. 즉, 600초(LoginGraceTime)당 10개의 연결(MaxStartups)이 허용되는 경우, 원격 루트 쉘을 얻는 데 평균 약 1주일이 걸립니다.
둘째, "ubuntu-6.06.1-server-i386.iso"의 "SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3": 이는 CVE-2006-5051 ("OpenSSH 4.4 이전 버전의 시그널 핸들러 레이스 조건")에 여전히 취약한 마지막 우분투 버전입니다.
이 버전을 원격으로 익스플로잇하기 위해, pam_start() 호출을 SIGALRM으로 중단시키고, PAM 구조 중 하나를 불일치 상태로 만든 다음, SIGALRM 핸들러 내에서 pam_end() 호출 중 이 불일치 상태를 악용합니다.
실험에서 이 레이스 조건을 이기기 위해 평균 약 10,000번의 시도가 필요했습니다. 즉, 120초(LoginGraceTime)당 10개의 연결(MaxStartups)이 허용되는 경우, 원격 루트 쉘을 얻는 데 평균 약 1-2일이 걸립니다.
마지막으로, "debian-12.5.0-i386-DVD-1.iso"의 "SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2": 이는 현재 데비안 안정 버전이며, CVE-2006-5051의 회귀에 취약합니다.
이 버전을 원격으로 익스플로잇하기 위해, malloc() 호출을 SIGALRM으로 중단시키고(sshd의 공개 키 파싱 코드 내에서), 힙을 불일치 상태로 만든 다음, SIGALRM 핸들러 내에서 다른 malloc() 호출 중 이 불일치 상태를 악용합니다(보다 정확히는 syslog() 내부).
실험에서 이 레이스 조건을 이기기 위해 평균 약 10,000번의 시도가 필요하므로, 120초(LoginGraceTime)당 100개의 연결(MaxStartups)이 허용되는 경우 약 3-4시간이 걸립니다. 궁극적으로 원격 루트 쉘을 얻는 데 평균 약 6-8시간이 걸리는데, 그 이유는 ASLR로 인해 glibc의 주소를 절반 정도만 올바르게 추측할 수 있기 때문입니다.
이 연구는 아직 진행 중입니다:
우리는 가상 머신만 대상으로 했으며, 베어메탈 서버는 대상으로 하지 않았습니다. 네트워크 링크는 대부분 안정적이었습니다(약 10ms 패킷 지터).
우리는 익스플로잇의 다양한 측면이 크게 개선될 수 있다고 확신합니다.
amd64 익스플로잇 작업을 시작했습니다. 이는 더 강력한 ASLR 때문에 훨씬 어렵습니다.
amd64 작업을 시작한 지 며칠 후, OpenSSH의 공개 Bugzilla에서 sshd의 SIGALRM 핸들러 교착 상태에 관한 다음 버그 보고서를 발견했습니다:
https://bugzilla.mindrot.org/show_bug.cgi?id=3690
따라서 우리는 즉시 OpenSSH 개발자에게 이 교착 상태가 익스플로잇 가능한 취약점으로 인해 발생한다는 사실을 알리기로 결정했으며, amd64 작업을 보류하고 이 권고를 작성하기 시작했습니다.
하지만 그건 나답지 않아, 나는 벗어나고 있어
-- The Interrupters, "Haven't Seen the Last of Me"
이 OpenSSH 버전의 SIGALRM 핸들러는 packet_close()를 호출하고, 이는 buffer_free()를 호출하며, 다시 xfree()와 free()를 호출합니다. free()는 async-signal-safe하지 않습니다:
따라서 우리는 이 데비안의 glibc(2.2.5)의 malloc 코드를 읽기 시작했습니다. 첫 번째 free() 호출이 SIGALRM에 의해 중단될 수 있고, SIGALRM 핸들러 내의 두 번째 free() 호출(위의 341-344줄) 중에 악용될 수 있는지 알아보기 위해서였습니다. 이 glibc의 malloc은 2000년 Solar Designer가 개척한 unlink() 기술에 대해 강화되지 않았기 때문에, 우리는 chunk_free()(free()에 의해 내부적으로 호출됨)에서 흥미로운 코드 경로를 빠르게 발견했습니다: