Wawa stores are a favorite among customers in Pennsylvania, New Jersey, Delaware, and beyond. When the company recently announced a new Android app to launch with their rewards program, I was interested in installing it and researching how it worked. Soon after registering and associating a gift card to my account, I discovered a serious vulnerability that would allow an attacker to arbitrarily associate gift cards to his account. Since the app does not require physical access to the card in order to be used at the register, the attacker could then use the remaining balances on the cards.

I signed up for the rewards program, purchased a gift card in the store, and installed the app to begin monitoring its API requests. As I navigated the app, I immediately hit a roadblock: I noticed that I wasn’t seeing any of the web requests in my proxy after performing actions that obviously required API communication. My next thought was that certificate pinning was likely being used. Though pinning is a great idea, it only temporarily impedes the ability to capture traffic, so I set out to reverse engineer the app in order to disable the pinning.

After decompiling the app and grepping the source for references to SSL, certificates, X509TrustManager, etc., I found the following class that seemed to enforce the pinning:

package com.wawa.android.app.server; import android.util.Log; import com.squareup.okhttp.OkHttpClient; import java.io.IOException; import java.math.BigInteger; import java.net.HttpURLConnection; import java.net.URL; import java.security.PublicKey; import java.security.cert.Certificate; import java.util.concurrent.TimeUnit; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; import retrofit.client.Request; import retrofit.client.UrlConnectionClient; public class AppOkClient extends UrlConnectionClient { static final int CONNECT_TIMEOUT_MILLIS = 30000; static final int READ_TIMEOUT_MILLIS = 30000; public static final String TAG = retrofit/client/UrlConnectionClient.getSimpleName(); static HostnameVerifier hostnameVerifier = new HostnameVerifier() { public boolean verify(String s, SSLSession sslsession) { String s1; s1 = (new BigInteger(1, sslsession.getPeerCertificates()[0].getPublicKey().getEncoded())).toString(16); Log.i(AppOkClient.TAG, (new StringBuilder()).append("server pubkey =").append(s1).toString()); if (!"https://mobapi.mdc53.com/".equalsIgnoreCase("https://mobapi.mdc53.com/")) { break MISSING_BLOCK_LABEL_89; } Log.i(AppOkClient.TAG, " local pubkey =30820122300d06092a864886f70d01010105000382010f003082010a0282010100b6a4715f92fee9bf21c188cd14878c49d6f571aea89a55bcef994aefbaa3805a456f08c7031572ac1c61e30c0b1074894c0fc8a8fca157f75d298ee94550a35c4c5bd376ca80ea6af38bb7cce6d03ea36c6b2e452dc9ea39c52fd4fd1c93452fba520972360e92b355723d8bb796c2dfbfe4820536fa4ebb95e7aeb68c1eca39262793e1e7b6ab1754f37cb61ddc0215d9fa38055e935c8b1ee1d4b74e91791c5a0090adcca725ccd0448858bb5d91852f3204ed624281fafb988152dfce3e3b369fec5b96acae4ae9200aef43c84594b8e2f1ac62402acce3eddb85879126e72d29e34b3e0dc347d89ae1a6fa91d8e26c6058b2e37191931f98eb2ed472f1a30203010001"); SSLPeerUnverifiedException sslpeerunverifiedexception; boolean flag; return s1.equalsIgnoreCase("30820122300d06092a864886f70d01010105000382010f003082010a0282010100b6a4715f92fee9bf21c188cd14878c49d6f571aea89a55bcef994aefbaa3805a456f08c7031572ac1c61e30c0b1074894c0fc8a8fca157f75d298ee94550a35c4c5bd376ca80ea6af38bb7cce6d03ea36c6b2e452dc9ea39c52fd4fd1c93452fba520972360e92b355723d8bb796c2dfbfe4820536fa4ebb95e7aeb68c1eca39262793e1e7b6ab1754f37cb61ddc0215d9fa38055e935c8b1ee1d4b74e91791c5a0090adcca725ccd0448858bb5d91852f3204ed624281fafb988152dfce3e3b369fec5b96acae4ae9200aef43c84594b8e2f1ac62402acce3eddb85879126e72d29e34b3e0dc347d89ae1a6fa91d8e26c6058b2e37191931f98eb2ed472f1a30203010001"); if (!"https://mobapi.mdc53.com/".equalsIgnoreCase("https://uatmobapi.mdc53.com/")) { break MISSING_BLOCK_LABEL_120; } Log.i(AppOkClient.TAG, " local pubkey =30820122300d06092a864886f70d01010105000382010f003082010a0282010100b6a4715f92fee9bf21c188cd14878c49d6f571aea89a55bcef994aefbaa3805a456f08c7031572ac1c61e30c0b1074894c0fc8a8fca157f75d298ee94550a35c4c5bd376ca80ea6af38bb7cce6d03ea36c6b2e452dc9ea39c52fd4fd1c93452fba520972360e92b355723d8bb796c2dfbfe4820536fa4ebb95e7aeb68c1eca39262793e1e7b6ab1754f37cb61ddc0215d9fa38055e935c8b1ee1d4b74e91791c5a0090adcca725ccd0448858bb5d91852f3204ed624281fafb988152dfce3e3b369fec5b96acae4ae9200aef43c84594b8e2f1ac62402acce3eddb85879126e72d29e34b3e0dc347d89ae1a6fa91d8e26c6058b2e37191931f98eb2ed472f1a30203010001"); if (!s1.equalsIgnoreCase("30820122300d06092a864886f70d01010105000382010f003082010a0282010100b6a4715f92fee9bf21c188cd14878c49d6f571aea89a55bcef994aefbaa3805a456f08c7031572ac1c61e30c0b1074894c0fc8a8fca157f75d298ee94550a35c4c5bd376ca80ea6af38bb7cce6d03ea36c6b2e452dc9ea39c52fd4fd1c93452fba520972360e92b355723d8bb796c2dfbfe4820536fa4ebb95e7aeb68c1eca39262793e1e7b6ab1754f37cb61ddc0215d9fa38055e935c8b1ee1d4b74e91791c5a0090adcca725ccd0448858bb5d91852f3204ed624281fafb988152dfce3e3b369fec5b96acae4ae9200aef43c84594b8e2f1ac62402acce3eddb85879126e72d29e34b3e0dc347d89ae1a6fa91d8e26c6058b2e37191931f98eb2ed472f1a30203010001")) { return false; } break MISSING_BLOCK_LABEL_160; if (!"https://mobapi.mdc53.com/".equalsIgnoreCase("https://devmobapi.wawa.com/")) { break MISSING_BLOCK_LABEL_160; } Log.i(AppOkClient.TAG, " local pubkey =30820122300d06092a864886f70d01010105000382010f003082010a0282010100bb5bf1006ea9b634f95b0828bf462194201b11a60098bc18325ed56d738930783586c7e9ecb41ae8087c24403df477a8983cc52d87e3fc4174bef06aa49d41530d0ec39b91d398a26e0cd755b7a915e3e0434dd3daff6db53d0deeea656a0f5e51a4fc73cb3a2b227f418d8a0f3116eeec29ee6a90d7c84122da750e9fa8949d0bdbffb6b466650b44c94e63f1d15516d135b2b6fad010ac61b7068d5c40e64c09b217bff5b981831dfd9b0ddcdd37ae2ff983870e23272b80a0dddb1bb22a4ceb2424ee93ad5539d27f5a7c06f79e0e8003b3280da98005ee400eee041341ad91212cbd9517c900fe07a0bff9c215bdbbb0b2a972a2ee4e8e6a5869ad67c6290203010001"); flag = s1.equalsIgnoreCase("30820122300d06092a864886f70d01010105000382010f003082010a0282010100bb5bf1006ea9b634f95b0828bf462194201b11a60098bc18325ed56d738930783586c7e9ecb41ae8087c24403df477a8983cc52d87e3fc4174bef06aa49d41530d0ec39b91d398a26e0cd755b7a915e3e0434dd3daff6db53d0deeea656a0f5e51a4fc73cb3a2b227f418d8a0f3116eeec29ee6a90d7c84122da750e9fa8949d0bdbffb6b466650b44c94e63f1d15516d135b2b6fad010ac61b7068d5c40e64c09b217bff5b981831dfd9b0ddcdd37ae2ff983870e23272b80a0dddb1bb22a4ceb2424ee93ad5539d27f5a7c06f79e0e8003b3280da98005ee400eee041341ad91212cbd9517c900fe07a0bff9c215bdbbb0b2a972a2ee4e8e6a5869ad67c6290203010001"); if (!flag) { return false; } break MISSING_BLOCK_LABEL_160; sslpeerunverifiedexception; sslpeerunverifiedexception.printStackTrace(); return true; } }; private final OkHttpClient client; public AppOkClient() { this(generateDefaultOkHttp()); } public AppOkClient(int i) { this(generateOkHttpWithTimeout(i)); } public AppOkClient(OkHttpClient okhttpclient) { client = okhttpclient; } private static OkHttpClient generateDefaultOkHttp() { OkHttpClient okhttpclient = new OkHttpClient(); okhttpclient.setConnectTimeout(30000L, TimeUnit.MILLISECONDS); okhttpclient.setReadTimeout(30000L, TimeUnit.MILLISECONDS); return okhttpclient; } private static OkHttpClient generateOkHttpWithTimeout(int i) { OkHttpClient okhttpclient = new OkHttpClient(); okhttpclient.setConnectTimeout(i, TimeUnit.MILLISECONDS); okhttpclient.setReadTimeout(i, TimeUnit.MILLISECONDS); return okhttpclient; } protected HttpURLConnection openConnection(Request request) throws IOException { client.setHostnameVerifier(hostnameVerifier); return client.open(new URL(request.getUrl())); } }

Since the verify method seemed to return a boolean as to whether the public keys (server’s and locally hard-coded) matched, I altered this method to simply return true in all cases, effectively disabling the pinning. Below is part of the patched smali representation of this method:

# virtual methods .method public verify(Ljava/lang/String;Ljavax/net/ssl/SSLSession;)Z .locals 10 .param p1, "arg0" # Ljava/lang/String; .param p2, "arg1" # Ljavax/net/ssl/SSLSession; .prologue const/4 v6, 0x0 const/4 v5, 0x1 const v0, 1 return v0 .line 33 :try_start_0 invoke-interface {p2}, Ljavax/net/ssl/SSLSession;->getPeerCertificates()[Ljava/security/cert/Certificate; move-result-object v2

Lines 13 and 14 create and return the true value. After recompiling and installing the patched app, I verified I was then able to proxy the requests made within the app.

Now that the API requests were being captured, I began the process of associating the card to my account. In order to link a card, customers are first asked to provide the card number and the PIN, found by scratching off an area on the back of the card.

After entering these values, the app validated the card by checking the balance. Below is the request and response:

POST https://mobapi.mdc53.com/customer/giftcard/balance HTTP/1.1 Authorization: Bearer ***REMOVED*** Content-Type: application/json; charset=UTF-8 Content-Length: 76 User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.4.2; SCH-I545 Build/KOT49H) Host: mobapi.mdc53.com Connection: Keep-Alive Accept-Encoding: gzip {"customerID":"221966","giftCardNumber":"***REMOVED***7162","pin":"***REMOVED***"}

{ "giftCardID": 5383465, "balance": 5, "asOfDateTime": "2015-03-25T12:52:03+0000" }

Once the card is validated, another request is made to actually link the card to the user.

POST https://mobapi.mdc53.com/customer/giftcard/link HTTP/1.1 Authorization: Bearer ***REMOVED*** Content-Type: application/json; charset=UTF-8 Content-Length: 73 User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.4.2; SCH-I545 Build/KOT49H) Host: mobapi.mdc53.com Connection: Keep-Alive Accept-Encoding: gzip {"alias":"","customerID":"221966","giftCardID":"5383465","sortOrder":"0"}

Note that only a giftCardID is required to link a card to a user’s account. Further, the giftCardID resembles an internal primary key, which is likely sequential. Using this request, an attacker could iterate through giftCardIDs and arbitrarily assign them to his account, allowing him to spend the balances in-store without requiring physical access to the cards. This also would allow “inactive” cards to be added to an attacker’s account, which conceivably would allow him to use the card after it had later been activated at the register.

Realizing the potential impact of this vulnerability, I reached out to Wawa’s information security team. I tried connecting via LinkedIn, but ended up reaching someone by guessing the CTO’s email address. Their team was very responsive and appeared to take the vulnerability seriously.

Disclosure Timeline

2015-03-25: Initial email to CTO

2015-03-26: Receive response, report details of vulnerability

2015-03-27: Acknowledgment of issue

2015-03-27: Vulnerability mitigated (informed on 2015-04-01)

2015-03-31: Follow up for update

2015-04-01: Call to discuss limited patch details

While Wawa’s team reacted quickly, they could have been more thorough in follow-up regarding the status of the issue. Per Wawa’s team, the vulnerability, or design flaw, was corrected server-side. I communicated my concerns regarding this, as the “fix” did not seem to address the lack of authentication when adding cards. Though their team could not share details of the patch, I believe all cards are now flagged as “unlinkable” by default until a balance check request is performed. Presumably, the card is then made “linkable,” and then, under ideal conditions, is immediately associated to the user’s account. This dramatically mitigates the risk of the vulnerability being exploited for malicious intent but still leaves a small window in which an attacker could “snipe” the card, depending on the amount of time between the balance check and the link request.

Despite this outstanding concern, Wawa affirmed that the issue was resolved from their perspective and thanked me for working with them in reporting the vulnerability.

Share this: