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