// HackTricks · Network Services

Pentesting ISO 8583 Payment Sockets

Pentesting ISO 8583 Payment Sockets

Overview

Some Android POS terminals and payment apps do not send payment, refund, or void traffic over HTTP. Instead, they authenticate over a raw TLS socket and then exchange ISO 8583 frames directly with the processor. In these cases, normal Burp-style API testing misses the real payment path.[1]

This attack surface is especially relevant when reversing Android POS apps because the mobile APK usually exposes the socket hostname/port, debug toggles, and the pre-auth bootstrap flow.[1]

Quick protocol refresher

Per the ISO 8583 message structure, a message is built from:[2]

  1. Message type
  2. One or two 64-bit bitmaps indicating which fields are present[3]
  3. Data elements in bitmap order

In real deployments there is often also a transport-specific prefix/header before the ISO 8583 payload, for example:

  • 4 ASCII digits with the frame length
  • a literal marker such as ISO
  • proprietary auth/bootstrap frames before ISO 8583 is accepted

Useful fields during testing:

  • DE2: PAN/card number
  • DE4: amount
  • DE11: STAN
  • DE37: RRN
  • DE39: response code (00 usually means approved)
  • DE41: terminal ID
  • DE42: merchant ID
  • DE55: ICC/EMV data

Reconstructing the hidden payment channel

If payments do not appear in Burp or the app’s REST/API traffic, pivot into the APK and look for:[1]

  • raw Socket / SSLSocket usage
  • hardcoded processor hostnames and ports
  • flags such as debugMode=false
  • separate REST login + socket auth flows
  • code that prints raw request/response hex into Android logs

A practical workflow is:

  1. Decompile the APK and identify the payment socket endpoint.
  2. Enable latent debug logging if present, then rebuild/sign/reinstall the APK.
  3. Use adb logcat to capture raw frame hex instead of trying to MITM the TLS socket.
  4. Recreate any bootstrap flow first (for example: REST login → JWT → socket AUTH frame → AUTHOK).
  5. Replay or mutate ISO 8583 frames over your own client.

Example log capture patterns:[4]

adb logcat | grep -iE 'iso|8583|auth|socket|tls'
adb logcat "PaymentSocket:D *:S"

A common proprietary bootstrap observed in POS apps is:

[length:4 ASCII] + AUTH + [jwt_length:4 ASCII] + JWT
[length:4 ASCII] + raw ISO 8583 bytes

What to test

Authentication and state-machine checks

  • Connect and send a valid ISO 8583 frame before any auth/bootstrap frame.
  • Send malformed or truncated AUTH frames and check whether the server still transitions to an authenticated state.
  • Reuse a JWT from merchant A on a fresh socket and verify whether the socket session is actually bound to that merchant, terminal, and device.

If unauthenticated requests are processed, look for ghost transactions: processor-side state changes with no merchant history or audit entry.[1]

Replay and duplicate detection

Capture a valid financial request and replay it:

  • immediately
  • after reconnecting
  • after the deduplication/cache window expires
  • with the same DE11 and DE37

Weak processors only cache duplicates briefly and later re-accept the same frame as a new charge. Persistent duplicate detection should bind at least merchant, terminal, amount, card, STAN, RRN, and transaction lifecycle.[1]

Cross-merchant / object-ownership flaws

Treat DE37 (RRN) as an object reference and test it like an IDOR/BOLA primitive:

  • authenticate as merchant B
  • submit a void/refund referencing merchant A’s RRN
  • modify DE41 and DE42 independently from the authenticated session
  • try forged or non-existent RRNs such as 000000000000

If the backend trusts the RRN alone, one merchant may void or refund another merchant’s transactions.[1]

Bypassing terminal-only checks

Do not trust POS UI validations.

If the terminal locally blocks a void/refund because the wrong card was inserted, but the app already generated the raw ISO 8583 frame, recover that frame from logs and send it directly. Backend approval indicates the processor validates only the transaction reference/format, not the original card identity.[1]

Business-logic mutations

High-value mutations include:[1]

  • increase DE4 above the original amount
  • zero amount 000000000000
  • negative/signed encoding edge cases if the implementation supports them
  • currency changes mid-flow
  • MTI confusion such as turning a valid reversal/void into another reversal/advice type

Watch both the ISO 8583 response and the merchant/admin dashboard state.

Parser robustness

ISO 8583 parsers are easy to desynchronise when proprietary field encodings are mixed with LLVAR/LLLVAR and binary TLVs.

Useful fuzz cases:

  • length indicator larger than real field data
  • length indicator shorter than real field data
  • set a bitmap bit but omit the corresponding field bytes
  • include field bytes while clearing the bitmap bit
  • mutate TLVs inside DE55

Expected failures include parser crashes, field shifts, approvals on malformed requests, and inconsistent dashboard state.[1]

Minimal socket client skeleton

jwt = http_login(login_url, username, password, serial)
sock = open_tls_socket(host, port)
payload = b"AUTH" + f"{len(jwt):04d}".encode() + jwt.encode()
sock.sendall(f"{len(payload):04d}".encode() + payload)
assert sock.recv(1024).startswith(b"AUTHOK")
sock.sendall(frame)
print(sock.recv(4096))

References