demo
ios
MASTG-TEST-0378
MASTG-DEMO-0144: Password Field Rendered in WebView DOM Without Native Overlay
Download MASTG-DEMO-0144 IPA
Open MASTG-DEMO-0144 Folder
Build MASTG-DEMO-0144 IPA
Sample
This sample loads an HTML login form containing <input type="password"> directly into a WKWebView via loadHTMLString:baseURL:. The password field lives in the DOM, so any JavaScript running on the page can read the user's typed value at any time via document.getElementById('password').value.
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
79
80
81
82
83
84
85
86
87 import UIKit
import WebKit
// SUMMARY : This sample demonstrates a WKWebView that loads HTML containing a password field .
// The password input is rendered in the DOM , which means any JavaScript running on the page
// can read the typed value via element . value at any time , including XSS payloads .
struct MastgTest {
@inline ( never ) @_optimize ( none )
public static func mastgTest ( completion : @escaping ( String ) -> Void ) {
DispatchQueue . main . async {
showWebView ( completion : completion )
}
}
private static func showWebView ( completion : @escaping ( String ) -> Void ) {
let config = WKWebViewConfiguration ()
let webView = WKWebView ( frame : . zero , configuration : config )
// FAIL : [ MASTG - TEST - 0378 ] The app loads HTML containing < input type = "password" >
// into a WKWebView . The typed value is stored in element . value and is readable
// by any JavaScript on the page , including XSS payloads :
// document . querySelector ( 'input[type=password]' ) . value
let html = """
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { font-family: -apple-system, sans-serif; text-align: center; padding-top: 60px; }
input { width: 80%; padding: 10px; margin: 8px 0; font-size: 16px; }
button { padding: 12px 24px; font-size: 16px; margin-top: 8px; }
</style>
</head>
<body>
<h2>Login</h2>
<input type="text" id="username" placeholder="Username" />
<input type="password" id="password" placeholder="Password" />
<br>
<button onclick="submitForm()">Sign In</button>
<script>
function submitForm() {
const user = document.getElementById('username').value;
// FAIL: [MASTG-TEST-0378] password value is accessible to page JavaScript
const pass = document.getElementById('password').value;
window.webkit.messageHandlers.bridge.postMessage({
action: 'login', username: user, password: pass
});
}
</script>
</body>
</html>
"""
webView . loadHTMLString ( html , baseURL : nil )
let vc = UIViewController ()
vc . view = webView
guard let presenter = topViewController () else {
completion ( "Failed to present: no view controller." )
return
}
presenter . present ( vc , animated : true ) {
completion ( "WebView with password field loaded." )
}
}
private static func topViewController ( base : UIViewController ? = nil ) -> UIViewController ? {
let root = base ?? UIApplication . shared . connectedScenes
. compactMap { $ 0 as ? UIWindowScene }
. flatMap { $ 0. windows }
. first { $ 0. isKeyWindow } ? . rootViewController
if let nav = root as ? UINavigationController {
return topViewController ( base : nav . visibleViewController )
}
if let tab = root as ? UITabBarController {
return topViewController ( base : tab . selectedViewController )
}
if let presented = root ? . presentedViewController {
return topViewController ( base : presented )
}
return root
}
}
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.
Observation
The output shows the input type="password" string in the binary's string table and its location in the function that constructs the HTML passed to loadHTMLString:baseURL:.
output.txt 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 Search for input type = password in the binary string table :
6 0x0000ac00 0x10000ac00 1051 1052 5. __TEXT . __cstring ascii < ! DOCTYPE html > \n < html > \n < head > \n < meta name = "viewport" content = "width=device-width, initial-scale=1.0" > \n < style > \n body { font - family : - apple - system , sans - serif ; text - align : center ; padding - top : 60 px ; } \n input { width : 80 % ; padding : 10 px ; margin : 8 px 0 ; font - size : 16 px ; } \n button { padding : 12 px 24 px ; font - size : 16 px ; margin - top : 8 px ; } \n </ style > \n </ head > \n < body > \n < h2 > Login </ h2 > \n < input type = "text" id = "username" placeholder = "Username" /> \n < input type = "password" id = "password" placeholder = "Password" /> \n < br > \n < button onclick = "submitForm()" > Sign In </ button > \n < script > \n function submitForm () { \n const user = document . getElementById ( 'username' ) . value ; \n // FAIL : [ MASTG - TEST - 0378 ] password value is accessible to page JavaScript \n const pass = document . getElementById ( 'password' ) . value ; \n window . webkit . messageHandlers . bridge . postMessage ({ \n action : 'login' , username : user , password : pass \n }); \n } \n </ script > \n </ body > \n </ html >
xrefs to the string containing the password field :
sym . MASTestApp . MastgTest . showWebView . _6E8AB2C58CE173A727EF27CB85DF8CD8 . completion_ ... FZ_ 0x100004258 [ STRN : r -- ] add x8 , x8 , str . __DOCTYPE_html__n_html__n_head__n_____meta_name_viewport__content_widthdevice_width__initial_scale1 .0 ___n_____style__n________body__font_family : __apple_system__sans_serif__text_align : _center__padding_top : _60px___n________input__width : _80___padding : _10px__margin : _8px_0__font_size : _16px___n________button__padding : _12px_24px__font_size : _16px__margin_top : _8px___n______style__n__head__n_body__n_____h2_Login__h2__n_____input_type_text__id_username__placeholder_Username_____n_____input_type_password__id_password__placeholder_Password_____n_____br__n_____button_onclick_submitForm____Sign_In__button__n_____script__n________function_submitForm____n____________const_user__document . getElementById_username_ . value__n_______________FAIL : __MASTG_TEST_0x03__password_value_is_accessible_to_page_JavaScript_n____________const_pass__document . getElementById_password_ . value__n____________window . webkit . messageHandlers . bridge . postMessage__n________________action : _login__username : _user__password : _pass_n_______________n_________n______script__n__body__n__html_
0x10000b3ad 24 str . loadHTMLString : baseURL :
0x1000140a0 8 reloc . fixup . loadHTMLString : baseURL :
sym . MASTestApp . MastgTest . showWebView . _6E8AB2C58CE173A727EF27CB85DF8CD8 . completion_ ... FZ_ 0x100004274 [ DATA : r -- ] ldr x1 , [ x8 , 0xa0 ]
│ 0x100004258 add x8 , x8 , 0xc00 ; 0x10000ac00 ; "<!DOCTYPE html> \n <html> \n <head> \n <meta name=" viewport " content=" width = device - width , initial - scale = 1.0 "> \n <style> \n body { font-family: -apple-system, sans-serif; text-align: center; padding-top: 60px; } \n input { width: 80%; padding: 10px; margin: 8px 0; font-size: 16px; } \n button { padding: 12px 24px; font-size: 16px; margin-top: 8px; } \n </style> \n </head> \n <body> \n <h2>Login</h2> \n <input type=" text " id=" username " placeholder=" Username " /> \n <input type=" password " id=" password " placeholder=" Password " /> \n <br> \n <button onclick=" submitForm () ">Sign In</button> \n <script> \n function submitForm() { \n const user = document.getElementById('username').value; \n // FAIL: [MASTG-TEST-0378] password value is accessible to page JavaScript \n const pass = document.getElementById('password').value; \n window.webkit.messageHandlers.bridge.postMessage({ \n action: 'login', username: user, password: pass \n }); \n } \n </script> \n </body> \n </html>"
│ 0x10000425c sub x8 , x8 , 0x20
│ 0x100004260 add x0 , x23 , 0x3f5
│ 0x100004264 orr x1 , x8 , 0x8000000000000000
│ 0x100004268 bl sym . imp . Foundationbool_ ... ridgeToObjectiveCSo8NSStringCyF_ ; Foundationbool ( ... ridgeToObjectiveCSo8NSStringCyF )
│ 0x10000426c mov x23 , x0
│ 0x100004270 adrp x8 , sym . __METACLASS_DATA__TtC10MASTestAppP33_9471609302C95FC8EC1D59DD4CF2A2DB19ResourceBundleClass ; 0x100014000
│ 0x100004274 ldr x1 , [ x8 , 0xa0 ] ; [ 0x10000b3ad : 8 ] = 0x4c4d544864616f6c ; "loadHTMLString:baseURL:" ; char * selector
│ 0x100004278 mov x0 , x21 ; void * instance
│ 0x10000427c mov x2 , x23
Evaluation
The test case fails because the app renders <input type="password"> inside the WKWebView DOM without intercepting focus or overlaying a native secure input view.
The <input type="password"> field is part of the HTML constant stored at 0x10000ac00, and the cross-reference shows it is loaded inside sym.MASTestApp.MastgTest.showWebView._6E8AB2C58CE173A727EF27CB85DF8CD8.completion_...FZ_ at 0x100004258. That string is bridged to an NSString (via Foundation...bridgeToObjectiveC... at 0x100004268) and dispatched through the loadHTMLString:baseURL: selector, which is loaded into x1 at 0x100004274, confirming that the password field is rendered directly in the WKWebView DOM.
Inspecting the loading function reveals no WKUserScript registration that intercepts focus on the field, and no native UITextField overlay is presented. The submitForm function in the page JavaScript (visible inside the HTML constant at 0x10000ac00) reads document.getElementById('password').value and sends it through the bridge, meaning the plaintext password is present in the DOM and accessible to any script running on the page before and during submission.