3 minutes
Finding a 4 million ticket data leak
I recently bought some tickets for a concert and saw that the QR code on the tickets all had the same prefix and end with a random number. However this random number was only 6 digits long, meaning there were only 1 million different combinations. This got me curious and as I started to look further into it, I uncovered a massive data leak of 4 million tickets.
Discovery
The QR codes looked like this (I use FOO
so the actual company is not identifiable):
FOO16300-482797
FOO16300-234322
My first guess was that the first number (16300
) is the event id, as all my tickets were for the same event. The second number (482797
) seems like a random number between 000000 and 999999.
This random number only has 6 digits, meaning there were only 10^6 = 1 million different combinations. There were about 5.000 tickets sold for this event, so if you generate a random number there is a 1 in 200 chance it matches a valid ticket.
However, going to the bouncer 200 times in a row will probably get you kicked out, so this is not a feasible way to check. That made me think; how do the bouncers check whether a ticket is valid? After some Googling I found that there is a ticket scanner app that they use, and it was downloadable from the Play Store!
I downloaded the app and was greeted with a login screen. Luckily it was free to create an account, so I made one, logged in and created a fake event. The app allowed me to scan tickets for my own event. However when I tried to scan the real ticket that I bought for the concert, it did not only show information about the ticket, but also that it was valid!
Exploitation
I fired up Burp Suite and started intercepting the API requests made by the app. When I scanned a ticket, the app made a request to the following endpoint:
Request
GET /api/v1/Barcodes/FOO16300-482797
Response
{
"barcode": "FOO16300-482797",
"customer_email": "REDACTED",
"customer_name": "REDACTED",
"event_id": 16300,
"event_name": "REDACTED",
"event_start": "REDACTED",
"event_end": "REDACTED",
"scanned": true,
"scanned_at": "REDACTED",
"scannedByEmail": "REDACTED",
"scannedByName": "REDACTED",
"ticket_name": "Saturday ticket",
}
The response contains information about the ticket, but also customer information, like an email address and name. Later on, I even found an endpoint that returned all tickets for an event at once.
Request
POST /api/v1/Barcodes/Newest
{
"EventId": "<EVENT_ID>"
}
I also noted that the event id was incrementing with 1 for each new event, meaning I could easily loop through all events and get all tickets that way. I sampled a few events and I found that I have access to a total of around 4.000.000 tickets for around 17.000 events.
Mitigation
- Barcodes should be longer (or with chars), so they can’t be brute forced
- Do not allow the ticket scanner to scan tickets from events that are not yours
Timeline
Date | Description |
---|---|
28-07-2021 | Reported Vulnerability |
29-07-2021 | Vulnerability confirmed |
03-09-2021 | Received reward €1000 and lifetime guest list |