POC — Scoopz NBJS bridge abuse

Particle Media · qb9.c() allowlist prefix-bypass → native bridge from attacker-controlled WebView URL

Host this page at an attacker host that passes the allowlist

Any host matching <allowed-host>.<attacker-domain> passes qb9.c(), e.g. https://thescoopz.com.evil.com. When the victim opens the mini-game / web URL in the Scoopz WebView, NBJS is bridged (isBridgeAllowedForCurrentUrl() = true) and non-ad native actions are permitted.

Trigger

[ready] awaiting trigger — bridge: ?
Protocol notes (from smali)
Bridge object:  addJavascriptInterface(bridge, "NBJS")   // window.NBJS
Entry point:    NBJS.callNative(JSON)   @JavascriptInterface
JSON fields:    { action: string, callback: string }
                callback is JS expression evaluated as:  javascript:<callback>({"success":1,...})

Gates:
  isBridgeAllowedForCurrentUrl()  -> qb9.c(getOriginalUrl())  || legacyMiniGameMode
  isActionPermittedForCurrentPage(action) -> true for ALL actions EXCEPT the 4 ad
        actions (preload/show x interstitial/reward), which need legacy + allowlist.

  => The ONLY effective gate for getLoginInfo / nbRequest / openBrowser / share /
     follow / openProfile / selectLocation ... is the qb9.c() URL allowlist.

qb9.b(host, allowed) bypass:
   host == allowed                       (exact)
   host.endsWith("." + allowed)          (subdomain)
   host.startsWith(allowed + ".")        <-- BUG: attacker host passes

getLoginInfo returns:
   userId, userName, nickName, avatarUrl, isGuest,
   userToken (= session cookie sc_t_3), deviceId, os, version, abInfo,
   zipcode, locationName, lat, lon

nbRequest({path, method, query})  -> authenticated API call with session cookie.
openBrowser(url) / openWeb(url)   -> navigate WebView to arbitrary URL.