CVE-2019-7304(dirty sock)
해킹/ kernel 2019. 3. 5. 15:10
background
cve-2019-7304는 local user에서 root로의 권한 상승을 위한 LPE 취약점이다. snappy라는 third party 패키지 매니져 데몬의 취약점을 이용한다. 대표적으로 우분투가 이 패키지를 기본으로 사용하며 Arch Linux CentOS, Debian, Fedora, Gentoo Linux, Solus, Manjaro Linux, Linux Mint, Open Embedded, Raspbian, OpenWrt, openSUSE등 다양한 distro에서 사용 가능한걸로 알려져 있다.
snap 데몬 documentation - 인증 관련
https://github.com/snapcore/snapd/wiki/REST-API 의 snapcore github wiki를 확인하면 /run/snapd.socket이라는 이름의 소켓 파일을 통해 https 리퀘스트를 받아 작동하는 것을 알 수 있다. 또 일반적인 INET 소켓이 아니라 UNIX socket을 사용한다고도 명시되어 있다.
아래쪽 Authentication 부분을 읽어보면 기능에 따라 root 권한에서만 실행되는 기능이 있고 open access인 기능이 있다. REST-API를 읽어보면 기능별로 인증이 필요한지 여부와 request, response 형식이 상세하게 나와있다.
linux manual - UNIX 타입 소켓
linux 매뉴얼을 살펴보면 unix 타입 소켓은 네트워크가 아닌 내부 프로세스끼리의 통신을 위한 소켓 타입이라고 한다. 네트워크를 사용하지 않는 프로세스들이 굳이 포트번호를 잡아먹지 않게 하기 위해 존재하는게 아닐까 추측해본다. 특이한점은 INET 소켓처럼 포트와 ip주소를 정해주는게 아니라 소켓 파일을 생성하고 그 소켓의 경로를 사용해서 통신한다. 이후 recv, send의 구현은 일반적인 소켓과 별 차이점이 없었다.
snapd가 생성한 UNIX 소켓
snapd 데몬은 위와같이 /run 디렉토리 위에 snapd.socket이라는 UNIX 소켓을 생성하고 대기한다.
dirty_sock 취약점은 이 소켓을 이용해서 uid를 파싱할때 발생하는 인증 취약점이다. 인증을 우회한 후에는 snapd의 자체 기능을 이용해서 새로운 sudo user를 만드는 방식으로 공격한다.
vulnerable code
Request의 인증을 검사하는 canAccess()함수
snapd/daemon/daemon.go 파일의 canAccess() 함수에서 사용자가 요청한 리퀘스트에 접근 권한이 존재하는지 여부를 검사한다. 코드를 읽어보면 알 수 있는 ucrednetGet() 함수의 반환값으로 uid를 판별하고 별다른 체크 없이 uid == 0일 경우 accessOK를 리턴하는 것을 볼 수 있다. 취약점은 저 ucrednetGet()함수와 r.RemoteAddr을 파싱하는 과정에서 발생한다.
ucrednetGet() 함수 내용
ucrednapd/daemon/ucrednet.go 파일에 존재한다. 함수 내부로 들어와보면 인자로 문자열 형태의 remoteAddr 변수를 받는것을 알 수 있다. 두번째 박스를 보면 이 remoteAddr이 ';'를 기준으로 토크나이즈 되어 파싱된다. 세번째 박스 부분에서는 "uid="라는 prefix 이후에 존재하는 숫자를 int형으로 변환해서 uid 변수로 넣은 후 리턴한다.
remoteAddr string을 반환하는 String() 함수
마지막으로 문제가 되는 부분이 이 String 함수인데 ucrednetGet() 함수로 들어갈 인자 remoteAddr을 반환하는 함수다. Sprintf 함수를 사용해서 문자열 형태로 만들어주는데 저부분에서 문제가 발생한다.
문제는 이게 하나의 문자열로 붙어서 인자로 전달되며 ucrednetGet()함수는 for문 안쪽에서 루프를 돌며 이부분을 토크나이징 한다는 점이다. 토크나이즈 되는 remoteAddr 문자열은 "uid=~~;pid=~~;socket=/run/snapd.socket;[사용자가 생성한 소켓 경로]" 의 형태로 Sprintf에 의해 만들어진다. 이 문자열을 돌면서 uid를 파싱하기 때문에 사용자가 소켓 이름을 "~~~.socket;uid=0" 이런식으로 생성할 경우 처음에는 실제 uid값을 가져오지만 마지막에 uid를 0으로 인식하게 된다. 이게 사실상 CVE-2019-7304 인증실패 취약점의 끝이다.
proof of concept
note: poc와 익스플로잇은 취약한 버전중 2.37 버전을 이용했다. https://github.com/snapcore/snapd 의 깃허브 주소에 코드와 빌드 정보가 자세하게 설명되어 있다.
test.py (python2) |
|
String()함수를 동적으로 디버깅 하기 위해 간단하게 snapd 데몬에 http request를 보내는 프로그램을 작성했다. 위 코드처럼 내가 새로운 UNIX socket을 바인드 하고 snapd의 소켓인 /run/snapd.socket에 connect 한 뒤 HTTP requset를 전송할 수 있다.
breakpoint - ucrednet.go:76
String() 함수에서 Sprintf()를 호출하는 부분 쯤에 브레이크를 걸고 poc 코드를 돌려 확인해보면 ucrednetAddr 구조인 wa 변수의 구조를 알 수 있다. 여기서 네번째에 인자로 들어가는 Addr은 다시 한겹의 구조체를 가지는데 이부분을 %s 포맷으로 Sprintf할 때 어떤 값이 나오는지 주소를 따라가보면 클라이언트 소켓 정보를 의미한다는 것을 알 수 있다.
test.py 결과
test.py 코드를 실행하면 위와 같이 test.py 코드로 리퀘스트를 보내면 /v2/logout 페이지는 root access가 필요하기 때문에 401 Unauthorized 에러가 돌아온다.
poc.py (python2) |
|
취약점을 트리거 하는 poc 코드는 위와 같다. test.py에서 socket의 이름만 뒤에 ;uid=0을 붙인 형태로 바꿔준다. 이 방법을 통해 uid=0 권한으로의 우회가 된다면 결과가 401 Unauthorized 외에 다른 형태로 나타날 것이다. poc 코드를 실행한 뒤 snapd를 디버깅해보면 아래와 같은 결과가 나타난다.
breakpoint - ucrednet.go:76
test.py에서 브레이크를 걸었던 Sprintf() 부분. 당연히 Sprintf의 클라이언트 소켓 부분은 ";uid=0" 문자열이 붙은 상태다.
breakpoint - github.com/snapcore/snapd/daemon.ucrednetGet
ucrednetGet 함수에서 인자로 넘어오는 remoteAddr 변수를 확인하면 좀더 명확하다. ucrednetGet() 함수가 실행되고 remoteAddr 변수의 마지막 부분에 uid=0 토큰이 추가된 것 처럼 보이는(실제로는 소켓 이름에 ";uid=0"이 포함된거지만) 것을 확인할 수 있다. 당연히 for문 안에서 저 문자열을 파싱하면 uid 변수가 한번은 1000, 한번은 0으로 입력되서 반환시에 전달되는 uid값은 0이 될 것이다. 캡쳐가 너무 많고 그부분까지 확인할 필요성을 느끼지 못해 포스팅에 적지는 않지만 uid에 들어가는 값을 확인하고 싶다면 ucrednetGet() 함수 주소를 구한 뒤 +516 위치에 브레이크를 걸고 $rsp+0x20 주소를 확인하면 된다.
poc.py 결과
결과는 test.py때의 401 인증실패가 아니라 400 Bad Request가 돌아왔다. 로그인이 안된 상태에서 로그아웃을 시도했기 때문이다. 하지만 이 결과를 보면 확실히 root로의 권한 우회가 성공했다는걸 알 수 있고 그러므로 root 권한으로 snap의 기능을 이용해 공격이 가능할 것이다.
exploit
익스플로잇 방법은 poc 버전에 따라 두종류로 나뉜다:
1. snapd가 제공하는 메일을 이용한 로그인 기능이 있는데 당연히 이를 위한 새로운 유저 생성 기능(POST /v2/create-user)도 존재한다. 이 기능을 이용하면 새로운 유저가 할당된다. 이때 sudoer 필드를 true로 설정해줘 sudo user를 생성할 수 있다.
2. 패키지를 설치하는 과정에서 install.sh처럼 실행될 bash script를 sideload 할 수 있다. 이 bash script에 dirty_sock이라는 새로운 sudo 유저로 생성하는 코드를 집어넣는다. 설치 과정은 root 권한에서 일어나므로 인증우회에 성공했다면 root 권한을 얻을 수 있다(애초에 sudo권한 없는 유저가 패키지 설치할 수 있는게 취약점임). 이 방법을 사용하면 이메일을 만들 필요 없이 공격이 가능하지만 snapd가 최신버전으로 업데이트 되며 취약점이 사라진다. 제로데이일때는 항상 유효한 방법일듯.
snap daemon documentation - create-user 기능
1.의 create_user 기능의 documentation 부분이다. root 권한으로만 호출이 가능한데 이 dirty_sock 취약점으로 우회가 가능하다. email과 sudoer인지 여부를 리퀘스트로 보내고 username과 ssh-key 페어를 응답으로 받는다.
snap daemon documentation - sideload request 기능
2.에서 설명한 패키지 설치 (POST /v2/snaps)기능. multipart 데이터로 snap content를 전송하면 action은 자동으로 install이 실행되고 name="devmode"일 경우에는 security confinement를 disable 해준다고도 되어있다. 마지막에는 filename이 들어간 필드 다음에 snap file data를 첨부하는듯.
익스플로잇 코드는 위 링크의 github에 잘 정리되어 있다. 굳이 포스팅의 길이를 늘리고 싶지 않기 때문에 전문 첨부는 패스.
patch
2.37, 2.37.1 버전 git diff 결과
다른 변경점도 상당히 많지만 요점은 저부분이다. 특이하게도 ucrednet이라는 새로운 타입을 만들어버렸다. ucrednetAddr 구조체도 pid uid socket을 다 지우고 ucrednet 구조체를 가리키게 바뀐게 보인다. 그리고 기존에 ucrednetAddr 구조체를 인자로 받넌 String() 함수가 새로생긴 ucrednet 구조체를 인자로 받게 변경되었다. 하지만 가장 중요한 변경점은 저 String() 함수에 더이상 필요없는 클라이언트 소켓 이름을 추가하지 않도록 바뀌었다는 점이다.
끝-
'해킹 > kernel' 카테고리의 다른 글
kernel 1-day 분석 환경설정하기 (0) | 2019.01.23 |
---|---|
Dirty Cow (CVE-2016-5195) 분석 - 2 (1) | 2019.01.22 |
Dirty Cow (CVE-2016-5195) 분석 - 1 (0) | 2019.01.22 |