Skip to content

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

  1. Use Exploring the App Package to extract the app. The main binary is ./Payload/MASTestApp.app/MASTestApp.
  2. Use radare2 (iOS) with the -i option to run this script.
 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
e asm.bytes=false
e scr.color=false
e scr.interactive=false
e asm.var=false
e bin.relocs.apply=true

?e Search for input type=password in the binary string table:
iz~input type="password"

?e

?e xrefs to the string containing the password field:
axt @ 0x10000ac00

?e

?e Search for calls to `loadHTMLString:baseURL:`
f~loadHTMLString

?e

?e xrefs to `loadHTMLString:baseURL:`:
axt @ 0x1000140a0

?e

?e Code snippet showing how the string is passed to `loadHTMLString:baseURL:`

pd 10 @ 0x100004258
1
2
#!/bin/bash
r2 -q -e bin.relocs.apply=true -i password_field_webview.r2 -A MASTestApp > output.txt

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: 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>

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.