앵하니의 더 나은 보안
Deeplink(딥링크) 활용 취약점과 그 시연 본문
Deeplink는 갑자기 왜?
금취분평 2023 기준에 새로 '모바일 DeepLink 도용 취약점'이라는 체크리스트가 나왔는데, 여태 진단 항목에 없었기에 딥링크에 대한 취약점 진단을 수행해본 적이 없다. 개념도 사실 제대로 안잡혀있고.
그래서 이번 기회에 개념부터 활용, 더 나아가 심화 영역까지 전반적으로다가 훑어보고자 한다.
아니 훑어보는거 말고 완전 씹고 뜯고 맛보고 즐겨서 너덜너덜 할 때까지 조져보자
렡ㅊ쓰 기맅
Deeplink?
특정 주소 혹은 값을 입력하면 앱이 실행되거나 앱 내 특정 화면으로 이동시키는 링크
라고 하지만 알기 쉽게 설명하자면, 쿠팡 광고 링크 같은거다.
인스타나 유튜브 같은데서 ‘어쩌구 저쩌구 한 썰’ 같은 링크를 타고 들어가면 꼭 쿠팡 파트너스 광고가 있는데, 이 광고를 클릭하면 쿠팡 앱 특정화면으로 들어가지거나, 앱이 설치 되지 않은 경우 플레이스토어 쿠팡 설치 화면으로 이동하게 된다. 이때 사용되는게 바로 이 딥링크다.
”인기상품 확인하고 계속 읽기” 버튼이 딥링크가 적용된 링크
물론 이 외에 사용자 편의를 위한 딥링크도 있다.
마케팅을 위한 혐오시런 딥링크는 많은 예시중 하나일 뿐.
Deeplink의 생김새
딥링크는 유형마다 생긴게 다른데, 요기요 앱 같은 경우
yogiyoapp://res?res_id=1122572&cat=0&ref=web&media_source=af_app_invites&shortlink=kv796e5h&source_caller=sdk
따위로 생겼다. 위 딥링크를 기본으로 현재는 많은 딥링크 형태가 파생됐다.
Deeplink 유형
앱에서 딥링크를 사용가능토록 구현하는 방법은 크게 3가지가 있다.
- URI Scheme
- App Link
- Universal Link
이 세개의 유형에 대해 먼저 알아보고, 앱에서 어떤식으로 딥링크를 지원하는지 파악한 뒤 딥링크가 활용된 취약점을 찾아보도록 하자
URI Scheme(Android)
안드로이드에선 androidmanifest.xml을 통해 스키마를 정의하고, 정의된 스키마를 이용해 딥링크를 사용할 수 있다.
URI Scheme 형태로 딥링크를 구현하기 위해선 androidmanifest.xml 내에 아래 규칙에 따라 intent-filter가 작성되어야 한다.
사용자가 scheme://host/path?parameter 형식의 딥링크를 클릭하게 되면, 시스템은 암시적 인텐트 호출 룰에 따라 적합한 액티비티를 찾아 호출하게 된다.
글로만 보면 확 와닿지 않으니, androidmanifest.xml을 직접 들여다보면서 어떻게 딥링크를 사용할 수 있는지, 사용되는지 알아보자
URI Scheme(Android) 무지성 테스트
[엠바고]
URI Scheme(android)의 한계
안드로이드에서 URI Scheme의 방식은 서로 다른 앱끼리 중복될 수 있다는 한계를 가지고 있단다.
사실 진단자 입장에서는 URI Scheme의 한계 따위 알 필요 없다. 그냥 어떤 형식의 스키마를 딥링크로 사용할 수 있는지만 파악하고 취약점 찾으면 되니까
그리고 중복된 앱이 있다고해서 그게 한계인지도 잘 모르겠다.
중복된다해도 어차피 사용자한테 어떤 앱 사용할지 묻고 사용자는 선택해서 쓸 수 있으니.
그래도 일단 흔히들 말하는 한계라고 하니까 대충 알아만 놓자
+ 보니까 URI Scheme로 구현한 딥링크는 앱 설치가 돼있지 않은 경우, 플레이 스토어의 어플리케이션 설치페이지로 넘어가지 않는다. 그래서 사용자의 사용을 유도하기 위해서는 App link 형태로 딥링크를 구현하는 것이 바람직하겠다.
URI Scheme(iOS)
iOS에서의 URI Scheme은 어플리케이션 바이너리 파일이 위치한 경로의 info.plist를 참고하면 된다.
info.plist에서 딥링크로 접근할 수 있는 스키마를 명시하고 함수를 통해 받은 딥링크를 처리한다.
근데 이 딥링크 처리 함수가 안드로이드처럼 깔끔하게 액티비티에서 일괄적으로 처리하지 않고, 어플리케이션마다 다르게 구현될 수 있다고 한다.😤
아래는 URI Scheme으로 딥링크를 처리할 수 있는 함수 목록과 그 예시
1. func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool
func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
// 딥링크 처리하는 코드 작성
if url.scheme == "myapp" {
if let host = url.host {
if host == "page1" {
// 딥링크의 호스트가 "page1"일 때의 처리
} else if host == "page2" {
// 딥링크의 호스트가 "page2"일 때의 처리
}
}
}
return true
}
2. UIApplication.shared.open(_:options:completionHandler:)
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 앱이 시작될 때 처리할 코드
return true
}
func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
// 딥링크를 처리하는 코드
if url.scheme == "myapp" {
if let host = url.host {
if host == "page1" {
// 딥링크의 호스트가 "page1"일 때의 처리
// 예를 들어, 페이지 1 화면으로 이동
} else if host == "page2" {
// 딥링크의 호스트가 "page2"일 때의 처리
// 예를 들어, 페이지 2 화면으로 이동
}
}
}
return true
}
}
3. canOpenURL(_:)/openURL(_:)
import UIKit
// URL 스킴을 처리하려는 스킴명
let schemeToCheck = "myapp"
if UIApplication.shared.canOpenURL(URL(string: "\(schemeToCheck)://")!) {
// 해당 URL 스킴을 처리할 수 있는 경우
print("앱에서 \(schemeToCheck) 스킴을 처리할 수 있습니다.")
} else {
// 해당 URL 스킴을 처리할 수 없는 경우
print("앱에서 \(schemeToCheck) 스킴을 처리할 수 없습니다.")
}
4. SceneDelegate가 있는경우(iOS 13버전 이상)
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) { }
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
if let url = URLContexts.first?.url {
// 딥링크 처리 코드
if url.scheme == "myapp" {
if let host = url.host {
if host == "page1" {
// 딥링크의 호스트가 "page1"일 때의 처리
} else if host == "page2" {
// 딥링크의 호스트가 "page2"일 때의 처리
}
}
}
}
}
App Link(android)
App Link 형식도 마찬가지로 딥링크를 구현하려면 androidmanifest.xml 파일에 intent-filter로 스키마를 명시해야한다. URI Scheme와 다른게 없어서 동일한 방식으로 딥링크를 생성하면 된다.
URI Scheme 방식과 다른게 있다면 스키마로 http, https만 사용가능하며 도메인 주소를 이용한다는 것.
그래서 도메인이 중복될 수 없게 관리하여, 서로 다른 두 앱에서 동일한 딥링크를 가질 수 없게 한다고 한다.
그리고 딥링크를 통해 접근해야하는 어플리케이션이 설치돼있지 않으면 설치를 위해 마켓의 설치 화면으로 이동시켜준다.
Universal Link(iOS)
android의 App Link와 비슷한 개념이다. Universal Link 또한 유효 어플리케이션이 설치돼있지 않은 경우 마켓의 어플리케이션 설치 화면을 호출한다.
Universal Link 역시 info.plist 파일에서 com.apple.developer.associated-domains 키를 통해 확인할 수 있고,
유니버셜 링크로 들어오는 요청은 UIApplicationDelega의 application:continueUserActivity:restorationHandler: 메서드에서 전달받은 NSUserActivity 객체을 사용해 처리된다.
(BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void(^)(NSArray * __nullable restorableObjects))restorationHandler {
if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) { // NSUserActivityTypeBrowsingWeb 타입이다.
// userActivity.webpageURL 로 대상 URL을 확인할 수 있다. 값이 없는 경우는 없다.
...
return YES; // 처리하려면 YES
}
return NO; // 아니라면 NO
}
App Link 및 Universal Link의 한계
URI Scheme의 한계를 보완코자 App Link와 Universal Link가 생겼다곤 하지만, 보완하기위해 나온 두 기술들은 또 다른 한계점이 존재한다.
- 우선 App Link는 Android API 6.0 버전 이상의 단말기에서만 사용 가능
- 어플리케이션이 설치돼있지 않으면 마켓으로 이동하는데, 이 과정에서 딥링크 유실이 발생해 원래 하려했던 행위를 수행 불가
- 구글에서 만든 앱(크롬, 메일, …)에서 App Link를 클릭할 경우 잘 동작하지만, 그 외의 앱에서는 동작않을 수 있음
Universal Link 또한 애플에서 만든 앱(사파리, 메일, …)에서 Universal Link를 클릭할 경우 잘 동작하지만, 그 외의 앱에서는 동작않을 수 있음
마찬가지로 진단자는 한계점까지 알필요는 없지만 그냥 이런 특징이 있구나 하고 참고만 하자
Deeplink를 활용한 취약점
진단자 입장에서 딥링크의 전수조사를 위해 앱이 딥링크를 어떻게 지원하는지 딥링크 구현 원리부터 알아봤다. 이 딥링크는 다양한 형태로 취약점이 발현되는데, 웹 성격의 리다이렉션이나 XSS/CSRF로 이용되는가하면, 모바일 성격의 화면호출로 인한 인증우회, 앱 데이터 파일 탈취 등의 행위가 가능하다.
이것 말고도 사실 어떻게 구현하느냐에 따라 무궁무진하게 악용할 수 있다.
그럼 이제 그 원리를 토대로 딥링크가 어떤 취약점으로 이용되는지, 어떻게 악용할 수 있는지 자세히 알아보도록 하자
Deeplink 활용 취약점 유형
Webview Hijacking
앱 내부 웹뷰를 호출하는 딥링크의 경우 파라미터로 리다이렉션 시킬 주소를 명시시키는 경우가 있는데, 이때 이 리다이렉션 주소를 변조시킨 딥링크를 배포하여 악성 URL로의 접근을 유도할 수 있다.
일반적인 리다이렉션 취약점과 유사하다고 생각하면 된다.
그리고 이 리다이렉션 취약점이 발생하는 동시에 웹뷰에서 javascript interface가 사용가능하면 Webview Hijacking 공격이 수행 가능하다.
Webview hijacking이 가능하면, victim의 모바일 정보(GPS 정보, 사용자 정보, 앱의 일부 기능 수행)나 중요 정보 탈취가 발생할 수 있다.
Webview에서 javascript interface를 사용할 수 있는지, 있다면 어떤 함수를 사용할 수 있는지에 대한 정보는 apk 파일의 smail코드 디컴파일 후 아래 함수 검색을 통해 확인 가능하다.
[Webview객체].setJavaScriptEnabled([Boolean])
[Webview객체].addJavascriptInterface([JavascriptInterface 객체],[지칭])
인증 프로세스 우회(Bypass local authentication)
‘화면 강제실행에 의한 인증단계 우회'와 완전 비슷한 취약점이다. 취약점 발생 매개체가 am start인지, 딥링크인지에 따라서 ‘화면 강제실행에 의한 인증단계 우회’ 취약점이냐, '모바일 DeepLink 도용 취약점’ 취약점이냐로 나뉜다.
단순 딥링크를 통해 인증우회에 성공했다면, 반드시 화면강제호출 취약점도 존재한다고 봐도 무방하다.
이는 화면강제 호출 조치 방안을 먼저 알면 이해가 쉬운데, 화면강제 호출의 문제는 강제 호출 되면 안되는 화면이 강제호출이 돼서 문제가 발생한다보기 때문에 androidmanifest.xml파일에서 해당 액티비티 선언의 exported 속성을 false로 변경할 것을 권고한다.(exported 속성은 앱 외부에서의 접근 가능 여부를 뜻한다.)
그럼 일반 사용자 권한으로는 강제호출을 할 수 없게된다.(물론 루트 권한으로는 여전히 강제호출이 가능)
하지만 딥링크로 사용되는 activity는 외부에서 호출해야되기 때문에 exported가 무조건 true로 설정된다.
그래서 딥링크로 사용할 화면(activity)은 exported가 true로 설정된다.
따라서, 만약 단순 딥링크 호출로 인증 프로세스 우회가 발생한다면, 화면강제호출 취약점 역시 존재한다고 할 수 있겠다.
내부 파일 접근 및 탈취(Insecure parameter handling)
딥링크를 통해 접근된 액티비티에서 데이터 내부 파일을 건든다면, 딥링크로 내부파일(/data/data/앱패키지/ 하위) 접근 또한 가능하다.
- 다운로드 딥링크 https://website.com/file.pdf 존재
- 해당 딥링크 클릭 시 특정 앱 데이터 경로 내 파일 file.pdf 다운로드
- 다운로드 되는 file.pdf파일의 단말기 내 절대경로는 /data/data/com.vulnerable-app/temp-files/file.pdf
이때, 딥링크를 조작하여 file.pdf 뿐만아니라 다른 앱 데이터에도 접근이 가능
ex) https://website.com/x/..%2F..%2Fdatabases/secret.db
Deeplink 활용 취약점 시연
Webview Hijacking
javascript interface까지 활용하는 데모가 없어서 단순 웹 뷰 리다이렉션 취약점으로 대체한다.
이 상황에서 javascript interface까지 사용가능하면 그게 그대로 Webview Hijacking 공격이 된다.
데모 앱은
Release v1.0 · hax0rgb/InsecureShop 여기서 다운로드해서 테스트해볼 수 있다.
그럼 이제 배웠던 내용을 토대로 InsecureShop 앱에서 실습한번 해보자
1. InsecureShop.apk의 Androidmanifest.xml 파일을 확인해 딥링크로 사용가능한 스키마 확보
2. com.insecureshop.WebViewActivity에서 인텐트 처리 프로세스 확인
3. 딥링크 파싱 과정 소스코드 분석
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_webview);
setSupportActionBar((Toolbar) _$_findCachedViewById(R.id.toolbar));
setTitle(getString(R.string.webview));
WebView webview = (WebView) findViewById(R.id.webview);
Intrinsics.checkExpressionValueIsNotNull(webview, "webview");
WebSettings settings = webview.getSettings();
Intrinsics.checkExpressionValueIsNotNull(settings, "webview.settings");
settings.setJavaScriptEnabled(true);
WebSettings settings2 = webview.getSettings();
Intrinsics.checkExpressionValueIsNotNull(settings2, "webview.settings");
settings2.setLoadWithOverviewMode(true);
WebSettings settings3 = webview.getSettings();
Intrinsics.checkExpressionValueIsNotNull(settings3, "webview.settings");
settings3.setUseWideViewPort(true);
WebSettings settings4 = webview.getSettings();
Intrinsics.checkExpressionValueIsNotNull(settings4, "webview.settings");
settings4.setAllowUniversalAccessFromFileURLs(true);
WebSettings settings5 = webview.getSettings();
Intrinsics.checkExpressionValueIsNotNull(settings5, "webview.settings");
settings5.setUserAgentString(this.USER_AGENT);
webview.setWebViewClient(new CustomWebViewClient());
Intent intent = getIntent();
Intrinsics.checkExpressionValueIsNotNull(intent, "intent");
Uri uri = intent.getData();
//딥링크를 통해 전달된 intent 정보 변수에 할당
if (uri != null) {
String data = null;
if (!StringsKt.equals$default(uri.getPath(), "/web", false, 2, null)) {
//딥링크의 path로 /web이 포함되지 않은 경우
if (StringsKt.equals$default(uri.getPath(), "/webview", false, 2, null)) {
//딥링크의 path로 /webview가 포함된 경우
Intent intent2 = getIntent();
Intrinsics.checkExpressionValueIsNotNull(intent2, "intent");
Uri data2 = intent2.getData();
if (data2 == null) {
Intrinsics.throwNpe();
}
String queryParameter = data2.getQueryParameter("url");
if (queryParameter == null) {
Intrinsics.throwNpe();
}
Intrinsics.checkExpressionValueIsNotNull(queryParameter, "intent.data!!.getQueryParameter(\"url\")!!");
if (StringsKt.endsWith$default(queryParameter, "insecureshopapp.com", false, 2, (Object) null)) {
//url 파라미터가 'insecureshopapp.com'으로 끝나는지 비교
Intent intent3 = getIntent();
Intrinsics.checkExpressionValueIsNotNull(intent3, "intent");
Uri data3 = intent3.getData();
data = data3 != null ? data3.getQueryParameter("url") : null;
//전달된 딥링크 내 url 파라미터가 null값이 아닐 경우 data에 할당
}
}
} else {
//딥링크의 path로 /web이 포함된 않은 경우
Intent intent4 = getIntent();
Intrinsics.checkExpressionValueIsNotNull(intent4, "intent");
Uri data4 = intent4.getData();
data = data4 != null ? data4.getQueryParameter("url") : null;
//전달된 딥링크 내 url 파라미터가 null값이 아닐 경우 data에 할당
}
if (data == null) {
finish();
}
webview.loadUrl(data);
//웹뷰 호출하면서 data의 url로 이동
Prefs.INSTANCE.getInstance(this).setData(data);
}
}
따라서 딥링크의 구조는
insecureshop://com.insecureshop/web?url=리다이렉션URI 또는
insecureshop://com.insecureshop/webview?url=[가변적]insecureshopapp.com
4. 분석한 소스코드를 토대로 exploit code 작성
https://naver.com이 악성 URI라 가정하에
insecureshop://com.insecureshop/web?url=https://naver.com 작성
또는 insecureshopapp.com으로 끝나는 도메인을 사서 webview path의 url 파라미터 값으로 전달
ex) phishing.insecureshopapp.com 도메인 구매후
insecureshop://com.insecureshop/webview?url=phishing.insecureshopapp.com 작성
5. 작성한 exploit code(insecureshop://com.insecureshop/web?url=https://naver.com) 테스트
인증프로세스 우회(Bypass local authentication)
글이 길어져서 그런지 동영상 업로드 시 포스트가 깨져서 관련 동영상 링크로 대신함
https://hackerone.com/reports/637194
내부 파일 접근 및 탈취(Insecure parameter handling)
Insecureshopapp 내 [Theft of Arbitrary files from LocalStorage] 취약점 참고
https://aeng-is-young.tistory.com/manage/newpost/62?type=post&returnURL=https%3A%2F%2Faeng-is-young.tistory.com%2Fentry%2FInsecureShopApp-%EB%B3%B8%EA%B2%A9%ED%95%B4%EC%B2%B4%EC%87%BC의 "LocalStorage에서 임의 파일 취득"를 참고해도 됨
Deeplink 취약점 보완 또는 안전한 설계 방법?
그렇다면 Deeplink 취약점이 발견됐거나 설계할 때 개발자에게 어떤식으로 가이드를 줘야 안전하게 개발될 수 있을까
답은 딥링크를 통해 호출되는 액티비티에서 인텐트 데이터의 유효성 검증을 무조건 하는 것
딥링크로 호출된 액티비티는 인텐트 데이터를 파싱할 때 해당 데이터를 함부로 신뢰하지 않아야 한다.
인텐트 데이터를 화이트리스트 방식으로 관리하거나, 그게 어렵다면 서버 사이드에서 타이트하게 관리해야한다.
취약점 유형에 따라 중요도나 취약성이 천차만별이기 때문에 케이스 바이 케이스로 가이드 줘야한다.
기본 원리는 액티비티에서 인텐트 데이터에 대한 검증을 수행하냐 안하냐에 따라 취약점이 발생하는거라 결국 적절한 데이터 검증 거친다면 해결할 수 있다.
번외
firebase dynamic link와 defrred deeplink의 원리는 기본적으로 URI Scheme과 App Link, Universial Link를 따라가기 때문에 따로 다루진 않았지만, 어떤방식으로 딥링크를 사용하는지 정도는 한번 알아보자
Firebase Dynamic Link
얘는 25년 8월을 기점으로 더 이상 지원하지 않는단다. 그러니 깊게 알 필요는 없고 대략적으로 훑어보기만 하자
다이나믹 링크는 대체로 https://myapp.page.link 처럼 생겼다. 생긴것만 알아두면 다른건 다 비슷하다.
얘도 결국 androidmanifest.xml에 인텐트 필터로 지정해줘야 하는건 똑같다.
ios는 info.plist 파일의 FirebaseDynamicLinksCustomDomains 필드에 써 넣어 사용된다.
Deferred Deeplink
deferred deeplink는 App link/Universal Link의 상위호환 느낌이다.
App link/Universial 링크는 단말기 내에 유효한 어플리케이션이 없을 경우 마켓까지 끌고 가서 앱 설치를 유도하지만 딱 그까지 밖에 못한다. 그럼 딥링크를 통해 원래 하고자 했던 행위가 유실되는데, Deferred Deeplink는 그런 단점을 보완시켜 유효한 어플리케이션이 없을 경우 단말기를 마켓으로 끌고가고, 설치가 완료되면 원래 이동하려 했던 화면으로 이동시킨다. 그래서 명령이 지연됐다고해서 deferred(지연된) deeplink라 지칭한다.
deferred 딥링크는 생각보다 복잡해서 애초에 앱 빌드할때 deferred 딥링크 용 sdk를 같이 말아넣어줘야하는 듯 하다.
참조
https://medium.com/prnd/%EB%94%A5%EB%A7%81%ED%81%AC%EC%9D%98-%EB%AA%A8%EB%93%A0%EA%B2%83-feat-app-link-universal-link-deferred-deeplink-61d6cf63a0a5
https://developer.android.com/training/app-links/deep-linking?hl=ko
https://help.dfinery.io/hc/ko/articles/360039757433-%EB%94%A5%EB%A7%81%ED%81%AC-Deeplink-URI%EC%8A%A4%ED%82%B4-%EC%9C%A0%EB%8B%88%EB%B2%84%EC%85%9C-%EB%A7%81%ED%81%AC-%EC%95%B1%EB%A7%81%ED%81%AC-%EA%B5%AC%EB%B6%84%EA%B3%BC-%EC%9D%B4%ED%95%B4
https://www.airbridge.io/blog-ko/deeplink-101-for-marketers-and-developers
http://ufo.stealien.com/2020-06-19/Deeplink
https://jdh5202.tistory.com/954
https://parkjonghyun.tistory.com/20
https://jaeryo2357.tistory.com/84
https://jaeryo2357.tistory.com/88
https://codechacha.com/ko/android-adb-start-activity/
https://engineering.linecorp.com/ko/blog/how-to-use-deeplink-in-trackit
https://blog.ab180.co/posts/deeplink-appmarketing
https://jdh5202.tistory.com/952
https://velog.io/@silver35/Android-Open-redirect-vulnerability-Using-Deeplink
https://hackerone.com/reports/637194
https://0xn3va.gitbook.io/cheat-sheets/android-application/intent-vulnerabilities/deep-linking-vulnerabilities
https://0xn3va.gitbook.io/cheat-sheets/android-application/intent-vulnerabilities
https://hackerone.com/reports/855618
https://ios-development.tistory.com/207
https://points.tistory.com/49
https://ohgyun.com/708
https://medium.com/wantedjobs/appsflyer-deferred-deeplink-%EC%A0%81%EC%9A%A9%EA%B8%B0-9b686a6004
https://black-jin0427.tistory.com/272