demo
ios
MASTG-TEST-0379
MASTG-DEMO-0145: DOM Inspection Using evaluateJavaScript Without Content World Isolation
Download MASTG-DEMO-0145 IPA
Open MASTG-DEMO-0145 Folder
Build MASTG-DEMO-0145 IPA
Sample
This sample demonstrates a WKWebView that reads sensitive content from the DOM (a recipient account number) using evaluateJavaScript(_:completionHandler:) without specifying a content world. The script runs in the .page world, where the prototype chain is shared with page JavaScript. A malicious page can override document.querySelector before the call runs:
// Attacker controlled page script
document . querySelector = function ( selector ) {
return { textContent : "ATTACKER_CONTROLLED" };
};
After this override, the evaluateJavaScript call returns the attacker value instead of the real DOM content. The native code receives and acts on poisoned data.
MastgTest.swift 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78 import UIKit
import WebKit
// SUMMARY : This sample demonstrates DOM inspection using evaluateJavaScript without
// a content world parameter . The scripts run in the page world ( . page ), where the
// prototype chain is shared with page JavaScript . A malicious page can override
// document . querySelector or other built - ins before these calls run , causing the app
// to receive attacker - controlled values instead of real DOM content .
class MastgTest : NSObject , WKNavigationDelegate {
private var webView : WKWebView ?
private static var currentTest : MastgTest ?
private var completion : (( String ) -> Void ) ?
@inline ( never ) @_optimize ( none )
public static func mastgTest ( completion : @escaping ( String ) -> Void ) {
DispatchQueue . main . async {
let test = MastgTest ()
currentTest = test
test . showWebView ( completion : completion )
}
}
private func showWebView ( completion : @escaping ( String ) -> Void ) {
self . completion = completion
let config = WKWebViewConfiguration ()
let webView = WKWebView ( frame : . zero , configuration : config )
self . webView = webView
let html = """
<html>
<body>
<div id="recipient_account_number">ACC_9876543210</div>
</body>
</html>
"""
webView . navigationDelegate = self
webView . loadHTMLString ( html , baseURL : nil )
}
public func webView ( _ webView : WKWebView , didFinish navigation : WKNavigation ! ) {
// Maliciously injected code by an attacker . It overwrites the recipient data
webView . evaluateJavaScript ( """
document.querySelector = function(selector) {
return { textContent: "ATTACKER_CONTROLLED" };
};
""" )
// FAIL : [ MASTG - TEST - 0379 ] evaluateJavaScript runs in the page world .
// A malicious page can override document . querySelector before this executes :
// document . querySelector = () => ({ textContent : "ATTACKER_CONTROLLED" })
webView . evaluateJavaScript ( "document.querySelector('#recipient_account_number').textContent" ,
completionHandler : { [ weak self ] value , _ in
let account = value as ? String ?? "unknown"
self ? . completion ? ( "Recipient Account Number: \(account)" )
})
/*
// PASS : Using WKContentWorld . defaultClient ( or a custom world ) provides isolation .
// The script runs in a separate world where document . querySelector is the original
// built - in , even if the page world has overridden it .
webView . evaluateJavaScript ( "document.querySelector('#recipient_account_number').textContent" ,
in : nil ,
in : . defaultClient ) { [ weak self ] result in
switch result {
case . success ( let value ):
let account = value as ? String ?? "unknown"
self ? . completion ? ( "Recipient Account Number: \(account)" )
case . failure :
self ? . completion ? ( "Error: Failed to retrieve account number" )
}
}
*/
}
}
Steps
Use Exploring the App Package to extract the app. The main binary is ./Payload/MASTestApp.app/MASTestApp.
Use radare2 (iOS) with the -i option to run this script.
evaluate_js_no_world.r2 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 e asm . bytes = false
e scr . color = false
e scr . interactive = false
e asm . var = false
? e List all uses of the 'evaluateJavaScript:completionHandler:' selector :
f ~ evaluateJavaScript
? e
? e xrefs to 'evaluateJavaScript:completionHandler:' :
axt @ reloc . fixup . evaluateJavaScript : completionHa
? e
? e Code snippet at first call site ( attacker - injected prototype override ):
pd 15 @ 0x1000047dc
? e
? e Code snippet at second call site ( reads recipient account number ):
pd 43 @ 0x100004820
run.sh #!/bin/bash
r2 - q - e bin . relocs . apply = true - i evaluate_js_no_world . r2 - A MASTestApp > output . txt
Observation
The script finds two xrefs to evaluateJavaScript:completionHandler:, both inside MastgTest.webView(_:didFinish:). The disassembly at each call site is shown below.
output.txt 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72 List all uses of the 'evaluateJavaScript:completionHandler:' selector :
0x10000ad00 38 str . evaluateJavaScript : completionHandler :
0x1000144a8 8 reloc . fixup . evaluateJavaScript : completionHa
xrefs to 'evaluateJavaScript:completionHandler:' :
sym . MASTestApp . MastgTest . webView . allocator . didFinish ... ySo05WKWebF0C_So12WKNavigationCSgtFTf4ndn_n 0x100004804 [ DATA : r -- ] ldr x1 , reloc . fixup . evaluateJavaScript : completionHa
sym . MASTestApp . MastgTest . webView . allocator . didFinish ... ySo05WKWebF0C_So12WKNavigationCSgtFTf4ndn_n 0x1000048b0 [ DATA : r -- ] ldr x1 , reloc . fixup . evaluateJavaScript : completionHa
Code snippet at first call site ( attacker - injected prototype override ):
| 0x1000047dc adrp x8 , 0x10000a000
| 0x1000047e0 add x8 , x8 , 0x3f0 ; 0x10000a3f0 ; " document.querySelector = function(selector) { \n return { textContent: " ATTACKER_CONTROLLED " }; \n };"
| 0x1000047e4 sub x8 , x8 , 0x20
| 0x1000047e8 mov x9 , 0x3f ; '?'
| 0x1000047ec movk x9 , 0xd000 , lsl 48
| 0x1000047f0 add x0 , x9 , 0x69
| 0x1000047f4 orr x1 , x8 , 0x8000000000000000
| 0x1000047f8 bl sym . imp . Foundationbool_ ... ridgeToObjectiveCSo8NSStringCyF_ ; Foundationbool ( ... ridgeToObjectiveCSo8NSStringCyF )
| 0x1000047fc mov x21 , x0
| 0x100004800 adrp x23 , sym . __PROTOCOL_WKScriptMessageHandler ; 0x100014000
| 0x100004804 ldr x1 , [ x23 , 0x4a8 ] ; [ 0x10000ad00 : 8 ] = 0x657461756c617665 ; "evaluateJavaScript:completionHandler:" ; char * selector
| 0x100004808 mov x0 , x19 ; void * instance
| 0x10000480c mov x2 , x21
| 0x100004810 mov x3 , 0
| 0x100004814 bl sym . imp . objc_msgSend ; void * objc_msgSend ( void * instance , char * selector )
Code snippet at second call site ( reads recipient account number ):
| 0x100004820 adrp x8 , 0x10000a000
| 0x100004824 add x8 , x8 , 0x4a0 ; 0x10000a4a0 ; "document.querySelector('#recipient_account_number').textContent"
| 0x100004828 sub x8 , x8 , 0x20
| 0x10000482c orr x1 , x8 , 0x8000000000000000
| 0x100004830 mov x0 , 0x3f ; '?'
| 0x100004834 movk x0 , 0xd000 , lsl 48
| 0x100004838 bl sym . imp . Foundationbool_ ... ridgeToObjectiveCSo8NSStringCyF_ ; Foundationbool ( ... ridgeToObjectiveCSo8NSStringCyF )
| 0x10000483c mov x21 , x0
| 0x100004840 adrp x0 , segment . __DATA_CONST ; 0x100010000
| 0x100004844 add x0 , x0 , 0x758 ; 0x100010758
| ; aav .0 x100010758
| 0x100004848 mov w1 , 0x18
| 0x10000484c mov w2 , 7
| 0x100004850 bl sym . imp . swift_allocObject
| 0x100004854 mov x22 , x0
| 0x100004858 add x0 , x0 , 0x10
| 0x10000485c mov x1 , x20
| 0x100004860 bl sym . imp . swift_unknownObjectWeakInit
| 0x100004864 adrp x8 , sym . MASTestApp . MastgTest . mastg . allocator_ ... FZ_ ; 0x100004000
| 0x100004868 add x8 , x8 , 0x910
| 0x10000486c stp x8 , x22 , [ arg_20h ]
| 0x100004870 adrp x8 , segment . __DATA_CONST ; 0x100010000
| 0x100004874 ldr x8 , [ x8 , 0x50 ] ; [ 0x100010050 : 8 ] = 0
| ; reloc . _NSConcreteStackBlock
| 0x100004878 str x8 , [ sp ]
| 0x10000487c adrp x8 , sym . imp . SwiftUI . Font . system . design . C9 . Design_ ... FZ_ ; 0x100009000
| 0x100004880 ldr d0 , [ x8 , 0x810 ] ; [ 0x100009810 : 8 ] = 0x42000000 ; section .3 . __TEXT . __const
| [ 03 ] - r - x section size 420 named 3. __TEXT . __const
| 0x100004884 str d0 , [ arg_8h ]
| 0x100004888 adrp x8 , sym . MASTestApp . MastgTest . mastg . allocator_ ... FZ_ ; 0x100004000
| 0x10000488c add x8 , x8 , 0x5c0
| 0x100004890 adrp x9 , segment . __DATA_CONST ; 0x100010000
| 0x100004894 add x9 , x9 , 0x770
| 0x100004898 stp x8 , x9 , [ arg_10h ]
| 0x10000489c mov x0 , sp
| 0x1000048a0 bl sym . imp . _Block_copy
| 0x1000048a4 mov x20 , x0
| 0x1000048a8 ldr x0 , [ arg0 ] ; void * arg0
| 0x1000048ac bl sym . imp . swift_release ; void swift_release ( void * arg0 )
| 0x1000048b0 ldr x1 , [ x23 , 0x4a8 ] ; [ 0x10000ad00 : 8 ] = 0x657461756c617665 ; "evaluateJavaScript:completionHandler:" ; char * selector
| 0x1000048b4 mov x0 , x19 ; void * instance
| 0x1000048b8 mov x2 , x21
| 0x1000048bc mov x3 , x20
| 0x1000048c0 bl sym . imp . objc_msgSend ; void * objc_msgSend ( void * instance , char * selector )
| 0x1000048c4 mov x0 , x20
| 0x1000048c8 bl sym . imp . _Block_release
Evaluation
The test case fails because the app calls evaluateJavaScript:completionHandler: to read security-sensitive DOM content in the page world.
The output shows two call sites. The first (0x100004804) is the attacker-injected prototype override that is part of this demo's setup and not the app's production logic. At 0x1000047e0, add x8, x8, 0x3f0 resolves to the string "document.querySelector = function(selector) { return { textContent: \"ATTACKER_CONTROLLED\" }; };" in the read-only data section. This string is bridged to an NSString (stored in x21), then passed as x2 to objc_msgSend at 0x100004814 with x3 = 0 (no completion handler), confirming this is the page-level override.
The second call site (0x1000048b0) is the security-relevant one. At 0x100004824, add x8, x8, 0x4a0 resolves to "document.querySelector('#recipient_account_number').textContent". After Swift string boxing (sub, orr) and bridging to NSString via Foundationbool_...ridgeToObjectiveCSo8NSStringCyF_, the result is stored in x21 at 0x10000483c. At 0x100004840 to 0x1000048a0, a completion block is built and heap-copied via _Block_copy; the result lands in x20. Then at 0x1000048b0, ldr x1, [x23, 0x4a8] loads the selector evaluateJavaScript:completionHandler: into x1, and:
x0 ← x19 (the WKWebView instance)
x2 ← x21 ("document.querySelector('#recipient_account_number').textContent")
x3 ← x20 (the completion block)
The objc_msgSend at 0x1000048c0 therefore calls [webView evaluateJavaScript:@"document.querySelector('#recipient_account_number').textContent" completionHandler:block]. Because the selector is evaluateJavaScript:completionHandler: rather than the content-world variant evaluateJavaScript:inFrame:inContentWorld:completionHandler:, the script runs in the .page world. Since the first call site already ran in the same world and overrode document.querySelector, this call receives the attacker-controlled value instead of the real DOM content.