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]
- Message type
- One or two 64-bit bitmaps indicating which fields are present[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 (
00usually 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/SSLSocketusage - 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:
- Decompile the APK and identify the payment socket endpoint.
- Enable latent debug logging if present, then rebuild/sign/reinstall the APK.
- Use
adb logcatto capture raw frame hex instead of trying to MITM the TLS socket. - Recreate any bootstrap flow first (for example: REST login → JWT → socket
AUTHframe →AUTHOK). - 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
AUTHframes 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))