Stealing $100,000 at DEF CON with this one neat trick. (Payment Village Card Hacking Challenge Writeup)

JankhJankh
11 min readAug 20, 2023

At Defcon 31 I stumbled across the payment village. Knowing nothing about the space, I decided to give the workshop and CTF a try.
The workshop and CTF were fantastic to introduce me to the weird world of payment. The biggest takeaway from which was that backwards compatibility is as terrible for security in payments, as it is everywhere else in cyber.

The Payment Village had a challenge for after their CTF, called the Card Hacking Challenge. I was handed a credit card a bunch of problems to solve, culminating in stealing $100,000 from the card.

Note: My knowledge on this topic is limited to one sleep deprived day at
DEF CON, documentation I read during that day, and any information I obtained during this subsequent writeup. As such, things may be incorrect, or imprecise. If so please let me know.

The Card Hacking Challenge challenge:

We were given an APK for the payment app, a POS device with which we could submit payments, and a credit card with fake money on it:

Test Credit Cards For Stealing Money

The challenges themselves were as follows:

My Approach

As my phone didn’t have NFC capability, I chose to look at a decompiled version in JADX, to pull out any notable strings.
From this, it was pretty easy to find the location it submits to the Transaction Stream (Basically the place where vendors submit transactions to a bank). I’ll go into what these parameters mean a bit later on, but the gist of it is, we can see the endpoint it submits to, and which parameters are dynamic, and which are static:

Request to the transaction stream
The function for building parameters for the request

This is helpful, as it gives us a lot of context as to what data gets sent to the
transaction stream. But it would take a lot of time to figure out how to
construct the whole payload, so instead I configured the POS machine to route it’s web traffic through my laptop with BurpSuite, allowing me to tap my card to the payment system and intercept the request to the transaction stream, showing me a successful payment.
Note: This was not done on the DEF CON Wi-Fi for obvious reasons.

Successful Payment

As the goal is to steal $100,000, my next goal was to identify how to tamper
this request to change the value charged to the card. To get a better feeling
for these parameters, I sent the request to repeater to begin playing with the parameters. However, unfortunately for me, the subsequent repeat
transactions were declined.

Replayed transactions returned a transaction declined error

This makes sense, as replaying payment seems like a trivial way to steal money. But whatever checks were taking place would have to be considered
by the POS machine for regular transactions. So I intercepted another payment request to tamper.

The next big question was, what were each of these parameters?
It turns out that each of these parameters corresponds to EMV tags. I found a full list online which you can look through here:

https://neapay.com/online-tools/emv-tags-list.html

A short list of important ones can be found in the writeup for last years
Payment Village card hacking challenge:

9f36 (Application Transaction Counter), 9f26 (Transaction Cryptogram), 82
(AIP), 5a (PAN), 57 (Track2 Equivalent), 9f37 (UN), 5f2a (Transaction
Currency), 9a (Date), 9f02 (Amount).

With this list of tags to explain what data meant what, plus a little bit of
googling, I found out that for EMV transactions, you need to update the
“amount” and “9f02” parameters to change the payment amount.
However, doing so to steal $1000 returned an error stating that authorizing a payment over $10 required cardholder verification such as a PIN.

One small note: the 9f02 parameter needs to be 12 digits, so it needs starting 0’s to pad the value to a length of 12.

Modifying the amount above 1000 cents returned a different error, requiring secure cardholder verification

This makes sense, given the app didn’t prompt the user for a pin code. But,
as it turns out, whether or not a user used a pin, or a signature, or any such
information is all within the transaction data. In this case, within the 9f34
“Cardholder Verification Method (CVM) Results” field. After finding a site
online that decodes this value, it became clear that the first two characters
(1f) meant “No Cardholder Verification Method required”. So I changed this to 04 for “Encrypted PIN by Integrated Circuit Card”. This was successful, in that it returned a different error message. The new error message said Auth Failed, and provided an AC string:

New error message returned

From a bit more googling, this was due to the Application Cryptogram (AC)
being incorrect. The AC, is derived from a number of parameters in the
request as part of an anti tampering measure. From my googling, it appears
that the specific fields which are present in the cryptogram is application
dependent, but they generally include all sensitive values such as transaction amount, country code, Application Transaction Counter, and a random unpredictable number to avoid replays.
Interestingly, the response we received from the server included an AC, so I
tried copying the AC from the server into the 9f26 parameter, which the
specification said was the name of the field containing the Application
Cryptogram.

Doing so successfully circumvented the Pin check, and we took $1000 from
the user:

Successful payment for $1000 confirming we have circumvented the PIN check

However, I wanted a fully automated solution, so I did a bit more googling on how replay attacks are prevented, and it turns out the 9f36 parameter, holds the Application Transaction Counter, which must increment with subsequent requests. By manually incrementing this value, we can then retrieve the AC from the response and use it to sign our next request:

Retrieving the AC from the error message

With this, we can successfully forge as many requests as we want to retrieve money, without requiring the POS system.

Using the retrieved AC to create a successful message

One final, small problem was the account did not have $100,000 in it, and
would return a “not enough money!” message

Not enough money error when charging $100,000

As it turned out, this was due to us already taking some money from the card, and it having exactly $100,000 to start. I attempted some tricks with refunds before one of the organizers sent a hint saying that getting the card to zero was sufficient. So to make things easy for myself I wrote the python script below, which attempted to take $1000 at a time every second, and if it received a “Not enough money!” error, it would half the amount requested and try again.

import requests
import time
def sendreq(inc, number):
req = "http://www.paymentvillageprocessing.com/auth_host.php?
amount="+str(number)+"&trans_type=EMV&9f15=9223&9f34=040802&9f36="+
str(inc)+"&9f26=C9176950D42AF636&9F6B=0&9f33=0808&82=0040&5a=101643
5539211092&9f27=AAC&9f37=AE09D662&5f2a=0840&9a=230818&9f02="+("0"*
(12-
len(number)))+str(number)+"&9f6c=0200&9c=01&9f77=1&9c=20&type=purch
ase"
body = requests.get(req)
a = body.text.split("AC: ")
b = a[1].split(" ")
c = b[0]
req2 = "http://www.paymentvillageprocessing.com/auth_host.php?
amount="+str(number)+"&trans_type=EMV&9f15=9223&9f34=040802&9f36="+
str(inc)+"&9f26="+c+"&9F6B=0&9f33=0808&82=0040&5a=1016435539211092&
9f27=AAC&9f37=AE09D662&5f2a=0840&9a=230818&9f02="+("0"*(12-
len(number)))+str(number)+"&9f6c=0200&9c=01&9f77=1&9c=20&type=purch
ase"
print(req2)
body2 = requests.get(req2)
print(body2.text)
if("Not enough money" in body2.text):
return False
else:
return True


moneycount = 100000
for i in range(20,99):
test = sendreq("01"+str(i),str(moneycount))
if(test==False):
moneycount=int(moneycount/2)
time.sleep(1)
Successful stealing of one cent:
Subsequent failure of stealing one cent:

Flags were submitted in the form of short vuln descriptions, so I submitted the following issue summary to the Payments Village team.

Title
Application Cryptogram Reflection and Pin Bypass

Issue Summary
Upon submitting an invalid payment request, the server returns the
calculated Application Cryptogram, as shown below:

HTTP/1.1 200 OK
Date: Sun, 13 Aug 2023 02:46:47 GMT
Server: Apache/2.4.54 (Ubuntu)
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Vary: Accept-Encoding
Content-Length: 95
Connection: close
Content-Type: text/html; charset=UTF-8
Card 1092 <br> Auth Failed!<br>Data is recorded for monitoring
purposes<br>AC: 7291767EE35E8B18

This allows a malicious vendor to modify any parameters within a
payment request and retrieve a valid Application Cryptogram to use
for a subsequent payment with any arbitrary parameters. In the
demonstrated attack, this was done to increase the charge to the
users card.
Additionally, a pin was in place to prevent expenses over 100
cents. This was circumvented via modifying the Cardholder
Verification Method (CVM) Parameter (9F34) to 040802. Which
effectively marked the transaction as approved with a pin. The full
info on the Parameter used is shown below:
04 Encrytped PIN by ICC
08 If transaction in application currency and < Y
02 Sucessful

Evidence
Below is my code for automated retrieval of all money in the
account. Sorry for the atrocious formatting. It functionally
retrieves the AC from a failed request and embeds it in a valid
request, then if the amount retrieved was greater than the amount
in the bank account, it halves it and tries again. I ran this until
there were 0 cents left in the account.

<The submission also attached the python script found above, but I have removed it for brevity.>

I ran this until I observed a successful request for 1 cent, followed by an error for 1 cent. Confirming to me the account had no money left.

Prizes

I woke up the next morning to a nice message from one of the organizers telling me I’d scored some loot.

The loot in question, was a patch and a second challenge coin. I scored one earlier for completing the CTF, the latter was for the Card Hacking.

My first, and second ever challenge coins.

I’ve never got a challenge coin before, so scoring two at payments village meant a lot.

Thoughts on the other challenges during the challenge:

I decided to call it after this challenge as it was getting late and there were parties to be found. But I wrote down my other ideas in the moment, to give my logic for the next challenges without the bias of hindsight.

Frida:

Someone else in the CTF room recommended using Frida and just hooking
the app to modify these parameters, as the AC would need to be calculated
somewhere in the app. However, I was pretty happy avoiding having to setup active debugging, and instead spend my time reading the standards and what each of the parameters meant.
It does pose the question of whether AC reflection would actually be considered an issue for the transaction stream. But that is a question for another day.

Challenge 2

Refund requests were successful regardless of the value of the 9f02
(Authorised amount of the transaction) parameter.

Successful refund, by changing the type parameter to refund.

However, a refund needed to be aligned with a purchase, as such, repeating a refund returned the following response:

Replaying a refund request failed.
Refund amount with 9f02 field set to 10,000,000$ was successful?
Refund amount with 9f02 field set to -10,000,000$ was successful?

The final of those refunds might steal all the money on the card, but I couldn’t find a great way to test this.
There is also a specification called 9f03 for “Secondary amount associated with the transaction representing a cashback amount”. I added this parameter and tampered with it for both purchases and refunds, but found no notable change in functionality.

Challenge 3:

I tried using card generators, in case this was just a test to see if user’s knew how to forge valid PAN, expiry dates, and CVVs, but my generated cards were not accepted by the system.

I also looked through the code for test cards, in case there was sufficient card data within unit tests in the APK itself, but this was unsuccessful as well.

Thoughts after the challenge:

After reading a writeup from another user, I realised the issue with my refund requests above might have been that the transaction type (trans_type) needed to be refund, not EMV for a refund. The fact I “refund complete” responses when I submitted a valid ATC, and “Refund can’t be issued at the moment” when I didn’t does make me question whether this attack could have succeeded with the wrong trans type. Either way, I hope putting my candid thoughts in the writeup helps provide some insight.

To complete challenge 3, to steal 15 other cards from the SoftPOS system, another user simply asked other user’s to tap their cards to their POS. machine. While this makes perfect sense, it lacked an elegance I was hoping for. It did, however, make me realise that if I had used the DEF CON open Wi-Fi, rather than the private Wi-Fi network I had chosen for opsec reasons, I would have been able to eavesdrop in on the http traffic for other users, which would have made retrieving 15 Credit cards reasonably trivial, I presume.

Closing thoughts

Payment is fascinating, and I’m very keen to hit some bug bounties with my new found context, and if the challenge is on next year I will definitely do it again. (Although potentially from home, this year’s Defcon was not very Covid friendly).

Thanks to the payment village (@L_AGalloway, @a66ot, @alecadena) this was an incredibly insightful experience.

Special shoutout to 2Keebs who I collaborated with and kept me sane while we read walls and walls of whitepapers.

Links I used during the challenges other folk might find useful:

Useful stuff on understanding creditcard data inc Magstripe data:
http://blog.opensecurityresearch.com/2012/02/deconstructing-credit-cards-data.html

CVV Calculator:
https://neapay.com/online-tools/calculate-cvv-cvc-icvv-cvv2-cvc2-dcvv.html

Very interesting stuff on NFC:
https://i.blackhat.com/asia-20/Thursday/asia-20-Galloway-First-Contact-New-Vulnerabilities-In-Contactless-Payments-wp.pdf

Good ideas for stealing money:
https://owasp.org/www-pdf-archive//OWASPLondon_20190718_Hack_IN-Cash_Out-tyunusov.pdf

Best reference for EMV tag info:
https://neapay.com/online-tools/emv-tags-list.html#9F6C
![[Pasted image 20230815072758.png]]

List of processing codes:
https://neapay.com/post/iso8583-processing-codes-transaction-processing_98.html

Cryptogram calculator tool:
https://neapay.com/online-tools/calculate-cryptogram.html

Last year’s CTF writeup:
https://www.paymentvillage.org/blog/how-to-solve-the-def-con-paymentvillage-org-credit-card-challenge

Very useful for understanding card attacks.
Leigh-Anne Galloway — It Only Takes A Minute to Clone a Credit Card, Thanks to a 50-Year-Old Problem
https://drive.google.com/file/d/17YIKh2aWf9n-Hr8cD8_K9vUGtiM-zuFU/view

Online EMV Decoder:
http://www.hartleyenterprises.com/decodecvmr.html

--

--