Skip to content

MASTG-DEMO-0100: Object Deserialization Using Serializable with semgrep

Download MASTG-DEMO-0100 APK Open MASTG-DEMO-0100 Folder Build MASTG-DEMO-0100 APK

Sample

The sample code demonstrates an insecure deserialization flaw in an Android app. The app reads a Base64 encoded serialized object from the payload_b64 Intent extra, deserializes it with ObjectInputStream.readObject(), and uses the result to overwrite the current user state. In this demo, an attacker can exploit the flaw by launching the activity with a crafted serialized AdminUser object, for example with:

adb shell am start -n org.owasp.mastestapp/.MainActivity --es payload_b64 'rO0ABXNyAChvcmcub3dhc3AubWFzdGVzdGFwcC5NYXN0Z1Rlc3QkQWRtaW5Vc2VyAAAAAAAAAMgCAAFaAAdpc0FkbWlueHIAJ29yZy5vd2FzcC5tYXN0ZXN0YXBwLk1hc3RnVGVzdCRCYXNlVXNlcgAAAAAAAABkAgABTAAIdXNlcm5hbWV0ABJMamF2YS9sYW5nL1N0cmluZzt4cHQAD0V4cGxvaXRlZCBBZG1pbgE='

The payload was generated using PayloadGenerator.java. It instantiates a MastgTest.AdminUser object (with username = "Exploited Admin" and isAdmin = true), serializes it via ObjectOutputStream into a byte array, and encodes the result in base64. The final adb command string (with the encoded payload) is printed to stdout and can be used directly to launch the attack. You can regenerate the payload using the following commands, which prints the complete payload as shown above.

mkdir -p build/payloadgen
javac -d build/payloadgen PayloadGenerator.java
java -cp build/payloadgen org.owasp.mastestapp.PayloadGenerator

This payload causes the app to accept attacker controlled serialized data and replace UserManager.currentUser with an admin object, resulting in privilege escalation without authentication. Behavior: When the app is launched normally and the Start button is pressed, it calls mastgTest() and displays the default user state, showing a standard user and (Not an Admin). However, the MainActivity also calls MastgTest(this).processIntent(intent) in onCreate(). This means that if the activity is started with a crafted payload_b64 extra, the app processes and deserializes that attacker controlled object before the test result is shown. As a result, pressing Start after sending the adb command causes the app to display PRIVILEGED ADMIN! because the current user state has been overwritten with a malicious AdminUser object.

 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
85
86
package org.owasp.mastestapp

import android.content.Context
import android.content.Intent
import android.util.Log
import java.io.ByteArrayInputStream
import java.io.ObjectInputStream
import java.io.Serializable
import java.util.Base64


class MastgTest(@Suppress("unused") private val context: Context) {

    open class BaseUser(val username: String) : Serializable {
        companion object {
            // Different UID to distinguish from the Admin subclass
            @JvmStatic
            val serialVersionUID = 100L
        }
    }


    class AdminUser(username: String) : BaseUser(username) {

        var isAdmin: Boolean = false

        companion object {
            @JvmStatic
            val serialVersionUID = 200L
        }
    }


    object UserManager {

        var currentUser: BaseUser = BaseUser("Standard User")
    }


    fun mastgTest(): String {
        // SUMMARY: This sample demonstrates insecure object deserialization from an untrusted source (Intent extra).
        val user = UserManager.currentUser
        val status = if (user is AdminUser && user.isAdmin) {
            "PRIVILEGED ADMIN!"
        } else {
            "(Not an Admin)"
        }

        val resultString = "Current User: ${user.username}\n" +
                "Status: $status\n\n" +
                "Vulnerability: Unwanted Object Deserialization is active.\n" +
                "The app will deserialize any 'BaseUser' subclass from the 'payload_b64' extra, " +
                "overwriting the current user state.\n\n" +
                "ADB command to trigger:\nadb shell am start -n org.owasp.mastestapp/.MainActivity --es payload_b64 'rO0ABXNyAChvcmcub3dhc3AubWFzdGVzdGFwcC5NYXN0Z1Rlc3QkQWRtaW5Vc2VyAAAAAAAAAMgCAAFaAAdpc0FkbWlueHIAJ29yZy5vd2FzcC5tYXN0ZXN0YXBwLk1hc3RnVGVzdCRCYXNlVXNlcgAAAAAAAABkAgABTAAIdXNlcm5hbWV0ABJMamF2YS9sYW5nL1N0cmluZzt4cHQAD0V4cGxvaXRlZCBBZG1pbgE='"

        Log.d("MASTG-TEST", resultString)
        return resultString
    }


    fun processIntent(intent: Intent) {
        if (intent.hasExtra("payload_b64")) {
            val b64Payload = intent.getStringExtra("payload_b64")
            Log.d("VULN_APP", "Received a base64 payload. Deserializing user object...")

            try {
                val serializedPayload = Base64.getDecoder().decode(b64Payload)
                // FAIL: [MASTG-TEST-0337] The app deserializes objects from an untrusted source without any type filtering or validation.
                val ois = ObjectInputStream(ByteArrayInputStream(serializedPayload))

                val untrustedObject = ois.readObject()
                ois.close()

                if (untrustedObject is BaseUser) {
                    UserManager.currentUser = untrustedObject
                    Log.i("VULN_APP", "User state overwritten with deserialized object!")
                } else {
                    Log.w("VULN_APP", "Deserialized object was not a user. State unchanged.")
                }

            } catch (e: Exception) {
                Log.e("VULN_APP", "Failed to deserialize payload", e)
            }
        }
    }
}
  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
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
package org.owasp.mastestapp;

import android.content.Context;
import android.content.Intent;
import android.util.Log;
import androidx.autofill.HintConstants;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.Base64;
import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;

/* compiled from: MastgTest.kt */
@Metadata(d1 = {"\u0000&\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0010\u000e\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0004\b\u0007\u0018\u00002\u00020\u0001:\u0003\f\r\u000eB\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0004\b\u0004\u0010\u0005J\u0006\u0010\u0006\u001a\u00020\u0007J\u000e\u0010\b\u001a\u00020\t2\u0006\u0010\n\u001a\u00020\u000bR\u000e\u0010\u0002\u001a\u00020\u0003X\u0082\u0004¢\u0006\u0002\n\u0000¨\u0006\u000f"}, d2 = {"Lorg/owasp/mastestapp/MastgTest;", "", "context", "Landroid/content/Context;", "<init>", "(Landroid/content/Context;)V", "mastgTest", "", "processIntent", "", "intent", "Landroid/content/Intent;", "BaseUser", "AdminUser", "UserManager", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48)
/* loaded from: classes3.dex */
public final class MastgTest {
    public static final int $stable = 8;
    private final Context context;

    public MastgTest(Context context) {
        Intrinsics.checkNotNullParameter(context, "context");
        this.context = context;
    }

    /* compiled from: MastgTest.kt */
    @Metadata(d1 = {"\u0000\u0012\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u000e\n\u0002\b\u0006\b\u0017\u0018\u0000 \b2\u00020\u0001:\u0001\bB\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0004\b\u0004\u0010\u0005R\u0011\u0010\u0002\u001a\u00020\u0003¢\u0006\b\n\u0000\u001a\u0004\b\u0006\u0010\u0007¨\u0006\t"}, d2 = {"Lorg/owasp/mastestapp/MastgTest$BaseUser;", "Ljava/io/Serializable;", HintConstants.AUTOFILL_HINT_USERNAME, "", "<init>", "(Ljava/lang/String;)V", "getUsername", "()Ljava/lang/String;", "Companion", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48)
    public static class BaseUser implements Serializable {
        public static final int $stable = 0;
        private static final long serialVersionUID = 100;
        private final String username;

        public BaseUser(String username) {
            Intrinsics.checkNotNullParameter(username, "username");
            this.username = username;
        }

        public final String getUsername() {
            return this.username;
        }
    }

    /* compiled from: MastgTest.kt */
    @Metadata(d1 = {"\u0000\u001a\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u000e\n\u0002\b\u0003\n\u0002\u0010\u000b\n\u0002\b\u0005\b\u0007\u0018\u0000 \u000b2\u00020\u0001:\u0001\u000bB\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0004\b\u0004\u0010\u0005R\u001a\u0010\u0006\u001a\u00020\u0007X\u0086\u000e¢\u0006\u000e\n\u0000\u001a\u0004\b\u0006\u0010\b\"\u0004\b\t\u0010\n¨\u0006\f"}, d2 = {"Lorg/owasp/mastestapp/MastgTest$AdminUser;", "Lorg/owasp/mastestapp/MastgTest$BaseUser;", HintConstants.AUTOFILL_HINT_USERNAME, "", "<init>", "(Ljava/lang/String;)V", "isAdmin", "", "()Z", "setAdmin", "(Z)V", "Companion", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48)
    public static final class AdminUser extends BaseUser {
        private static final long serialVersionUID = 200;
        private boolean isAdmin;
        public static final int $stable = 8;

        /* JADX WARN: 'super' call moved to the top of the method (can break code semantics) */
        public AdminUser(String username) {
            super(username);
            Intrinsics.checkNotNullParameter(username, "username");
        }

        /* renamed from: isAdmin, reason: from getter */
        public final boolean getIsAdmin() {
            return this.isAdmin;
        }

        public final void setAdmin(boolean z) {
            this.isAdmin = z;
        }
    }

    /* compiled from: MastgTest.kt */
    @Metadata(d1 = {"\u0000\u0014\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0003\n\u0002\u0018\u0002\n\u0002\b\u0005\bÇ\u0002\u0018\u00002\u00020\u0001B\t\b\u0003¢\u0006\u0004\b\u0002\u0010\u0003R\u001a\u0010\u0004\u001a\u00020\u0005X\u0086\u000e¢\u0006\u000e\n\u0000\u001a\u0004\b\u0006\u0010\u0007\"\u0004\b\b\u0010\t¨\u0006\n"}, d2 = {"Lorg/owasp/mastestapp/MastgTest$UserManager;", "", "<init>", "()V", "currentUser", "Lorg/owasp/mastestapp/MastgTest$BaseUser;", "getCurrentUser", "()Lorg/owasp/mastestapp/MastgTest$BaseUser;", "setCurrentUser", "(Lorg/owasp/mastestapp/MastgTest$BaseUser;)V", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48)
    public static final class UserManager {
        public static final UserManager INSTANCE = new UserManager();
        private static BaseUser currentUser = new BaseUser("Standard User");
        public static final int $stable = 8;

        private UserManager() {
        }

        public final BaseUser getCurrentUser() {
            return currentUser;
        }

        public final void setCurrentUser(BaseUser baseUser) {
            Intrinsics.checkNotNullParameter(baseUser, "<set-?>");
            currentUser = baseUser;
        }
    }

    public final String mastgTest() {
        String status;
        BaseUser user = UserManager.INSTANCE.getCurrentUser();
        if ((user instanceof AdminUser) && ((AdminUser) user).getIsAdmin()) {
            status = "PRIVILEGED ADMIN!";
        } else {
            status = "(Not an Admin)";
        }
        String resultString = "Current User: " + user.getUsername() + "\nStatus: " + status + "\n\nVulnerability: Unwanted Object Deserialization is active.\nThe app will deserialize any 'BaseUser' subclass from the 'payload_b64' extra, overwriting the current user state.";
        Log.d("MASTG-TEST", resultString);
        return resultString;
    }

    public final void processIntent(Intent intent) throws ClassNotFoundException, IOException {
        Intrinsics.checkNotNullParameter(intent, "intent");
        if (intent.hasExtra("payload_b64")) {
            String b64Payload = intent.getStringExtra("payload_b64");
            Log.d("VULN_APP", "Received a base64 payload. Deserializing user object...");
            try {
                byte[] serializedPayload = Base64.getDecoder().decode(b64Payload);
                ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(serializedPayload));
                Object untrustedObject = ois.readObject();
                ois.close();
                if (untrustedObject instanceof BaseUser) {
                    UserManager.INSTANCE.setCurrentUser((BaseUser) untrustedObject);
                    Log.i("VULN_APP", "User state overwritten with deserialized object!");
                } else {
                    Log.w("VULN_APP", "Deserialized object was not a user. State unchanged.");
                }
            } catch (Exception e) {
                Log.e("VULN_APP", "Failed to deserialize payload", e);
            }
        }
    }
}

Steps

Let's run our semgrep rule against the sample code.

../../../../rules/mastg-android-object-deserialization.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
rules:
  - id: mastg-android-object-deserialization
    severity: WARNING
    languages:
      - java
    metadata:
      summary: This rule looks for use of Object Deserialization
    message: "[MASVS-CODE-4] The application makes use of Object deserialization in the code using the ObjectInputStream.readObject() function call."
    patterns:
      - pattern: |-
          $VAR = new java.io.ObjectInputStream(...);
          ...
          $OBJ = $VAR.readObject();
run.sh
1
2
#!/bin/bash
NO_COLOR=true semgrep -c ../../../../rules/mastg-android-object-deserialization.yml ./MastgTest_reversed.java --text -o output.txt

Observation

The output contains the locations of all ObjectInputStream.readObject() usages in the code.

output.txt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
┌────────────────┐
 1 Code Finding 
└────────────────┘

    MastgTest_reversed.java
    ❯❱rules.mastg-android-object-deserialization
          [MASVS-CODE-4] The application makes use of Object deserialization in the code using the ObjectInputStream.readObject() function call.

          107 ObjectInputStream ois = new ObjectInputStream(new
               ByteArrayInputStream(serializedPayload));        
          108 Object untrustedObject = ois.readObject();

Evaluation

The test fails because the app deserializes untrusted data from an external Intent extra using ObjectInputStream.readObject() without any class filtering before deserialization. processIntent() takes the Base64 value from payload_b64, decodes it, deserializes it, and if the result is a BaseUser, stores it as the current user.

public final void processIntent(Intent intent) ... {
    ...
    if (intent.hasExtra("payload_b64")) {
        String b64Payload = intent.getStringExtra("payload_b64");
        ...
        byte[] serializedPayload = Base64.getDecoder().decode(b64Payload);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(serializedPayload));
        Object untrustedObject = ois.readObject();
        ois.close();
        if (untrustedObject instanceof BaseUser) {
            UserManager.INSTANCE.setCurrentUser((BaseUser) untrustedObject);
            ...
        }
    }
}

This means an attacker can send a crafted serialized BaseUser subclass, such as AdminUser with isAdmin = true, and overwrite UserManager.currentUser. The result is privilege escalation without authentication. In a broader scenario, unsafe deserialization can also lead to code execution if a suitable gadget chain is present.