Public transportation payment systems have undergone significant changes over the years. Mobile payment solutions have become increasingly popular, allowing passengers to pay for their fare using smartphones or other mobile devices. This trend is likely to continue in the years to come. But how secure are mobile payment solutions for public transportation?
I set out to answer this question in my latest security project, which I first presented at DEF CON USA 31. In this post, I will first provide background information about the mobility-as-a-service (MaaS) industry and the companies that operate within it. Next, I will explain the research process that led me to discover vulnerabilities in the MaaS process and in a popular MaaS app that not only allowed me to access free train tickets, but also retrieve the personal information of users, including their credit cards and details about their ongoing rides. Finally, I will highlight the vendor response and identify how SafeBreach is sharing this information with the broader security community to help transportation organizations protect themselves.
Before we dive in, it’s important to understand how mobile ticketing works in the world of transportation. MaaS enables users to plan, book, and pay for different types of mobility services from one mobile application. A MaaS operator is a company that develops the applications that provide those features.
Riders use a MaaS application to purchase tickets that they scan at the ticketing gate in order to gain entry. Once the ride is complete, riders scan their tickets once more at the exit gates. The fare is then calculated at the exit gate based on the distance traveled. At the end of the month, all the fares are tallied and billed to the user.
The Research Process
Analyzing the Mobile Ticket Format
The ticket itself is a QR code that the gates scan and validate. The mobile solution provides the payment interface between the train services and the user in the form of that QR code. Below the QR code is a long string of numbers: this is the ticket number. I observed that the QR code was not encrypted, so I wondered if it might be possible to change this value in order to trick the gate into accepting a fake ticket.
First, I sought to understand the seemingly random ticket number. I collected a few train tickets, and noticed a repeating pattern in the form of the ending: the number “201” appeared on each ticket. This suggested that there might be a format that I could figure out through further analysis.
After a quick Google search, I found an API that gave me a JSON file with lots of information. In that file, one of the details mentioned was the MaaS operator. It turns out that the repeated number 201 at the end of each ticket that I collected represented the ID for the Moovit-Pango operator, which I used throughout the rest of this experiment. I did find other operators mentioned in the JSON, including one called IDF.
I understood that breaking down the whole form of the ticket was going to be too difficult, so I decided to conduct an experiment on the train station’s physical gates. What would happen if I ordered a ticket and modified the ending operator ID to be a different operator? In this case, I changed the Moovit-Pango operator to the IDF operator in order to ride for free; however, this did not seem to work. I decided to shift my focus from the ticket to the gate itself.
Experimenting with the Gates: Cancel and Use
Upon further examination, I realized that riders scan the same ticket at the entry and exit gate. I then tested to see if the gates would validate a ticket that was purchased, then immediately canceled. The gate accepted the canceled ticket, indicating that there was no real time communication between the gates and the MaaS operator. The canceled ticket also worked when passing through the exit. But ultimately, this did not result in a free fare; a week later, the MaaS operator realized that a ride had been taken and applied the appropriate fee to the account.
What likely happens is that when the ticket is scanned at each gate, a record is logged. These two logs together represent a complete ride. Even though the ticket was canceled, the MaaS operator was still able to calculate the fare from these logs.
Creating a Hole in the MaaS Logs
To get around this, I attempted to create a hole in the MaaS operator’s log. The process is similar to the previous process, but this time when I reached the destination with the canceled ticket, I ordered yet another ticket, which I immediately canceled. This second canceled ticket opened the gate, and there was no full ride that the MaaS operator could track. This approach actually worked, and no fare was charged. This seemed like a good lead to continue digging into, so I decided to look at a specific MaaS operator to see if there were additional security issues.
Testing the Moovit App
Moovit is a worldwide MaaS operator that was acquired by Intel for $900 million in May 2020. Moovit has more than 1.4 billion users around the world and operates in more than 100 countries and 5000 cities. The application is primarily used for navigation and to find routes, but also to pay for rides.
For the Moovit app, the account registration process begins with downloading the app and joining the payment service. As part of the payment service registration, users are prompted to provide their phone number. To validate the provided phone number belongs to the user and that phone is owned by the user, Moovit servers generate a random one-time password (OTP) and send it to the provided phone number. If the user responds with the correct OTP, Moovit will register a new account. This account is affiliated with the provided phone number and holds other details, like the user’s credit card information and other personally identifiable information (PII).
Moovit also offers a feature that enables users to switch their account between devices. The process of switching devices is quite seamless, as it follows the same steps as registering a new account. Let’s say a user takes their SIM card and places it in another device. The new device will have the same phone number of the old device, which already had an account registered, but that account is not connected to the new device. If the new device tries to join, Moovit servers will see that there is an existing account affiliated. Authorizing the generated OTP will cause the account that was previously connected to the old device to disconnect and transfer to the newly registered device.
Vulnerability #1: ABCs of API security
To start, I wondered if I would be able to find some basic insecurities in the Moovit app’s API. I decided to look at API requests between the server and the mobile application. All the traffic between the server and application was encrypted using a secure sockets layer (SSL) certificate, making it impossible to intercept. I managed to bypass the SSL certificate pinning using a Frida script. This allowed me to serve my own certificate and ultimately intercept all API requests between the server and the device. My goal was to understand the API communication between the device and the server. So, I focused on the API request for joining the payment service and the ticketing system.
When the application is first opened, a CreateUser API request is sent to the server. The name of the API may sound straightforward, but I quickly realized it was a misnomer. When the API request is sent, the server responds with a long identifier called UserKey. Through my research, I found that this identifier is actually assigned to the device and not to the user.
The servers hold additional details on the user like their credit card, but they also hold the UserKey. The server identifies the account using this key. Not all the UserKeys have affiliated accounts, but all the accounts have affiliated UserKeys.
Let’s assume there is a device with UserKey number one, linked to phone number one. Our goal was to hijack this account using the Account Switching feature. First, the attacker made a Create API request to obtain a UserKey legitimately. In this example, the attacker received UserKey number two.
After obtaining the UserKey, the attacker attempted to join the payment service by pretending to be the device with UserKey number one. In order to complete the registration, the attacker needed to provide the OTP generated by Moovit. Since the attacker didn’t have access to phone number one, they do not know the correct OTP.
As it turns out, the OTP is four digits long. I tested to see if a brute force attack would work, and to my surprise it did. I could go over all the OTP options from zero to 9099. In the worst case, the brute force took me five minutes. See the demonstration below.
After the brute force attack, the attacker could completely impersonate the account and use its credit card to order tickets. Furthermore, the attacker gained access to the account’s personal information. The server would change the affiliations and link the attacker UserKey to the account represented by the victim’s phone number.
This attack targets a specific phone number, which can be useful. But it also has limitations: it cannot be widely spread, takes time due to the brute force aspect, and causes the account to be disconnected from the device, making it noticeable. Its effectiveness is also limited due to the requirement of having the phone number linked to the targeted account.
Next, I decided to transform the problem of getting a phone number linked to an account to understand the user key format, with the goal of enumerating existing accounts.
Vulnerability # 2: UserKey Format
As I mentioned earlier, when the user opens the application for the first time, a CreateUser API request is sent to Moovit servers. The server’s response contains a UserKey. When I requested a UserKey, I noted that it looked like a type of universally unique identifier (UUID). If I made the API request multiple times, I could gather a lot of UserKey samples to analyze; so I created around 25,000 UserKeys. The keys seemed pretty similar.
At a glance, it seemed that there was a value in the UserKey that repeated in each of the keys. I thought it had the potential to be a known value, so I did another Google search and found an Oracle database function that generated a unique identifier with a similar value. In fact, this identifier was not really randomized well.
The documentation on this function showed that this identifier consists of host identifier, process or thread identifier, and a non-repeating value. I deemed it likely that the host identifier was the constant value that appeared in each of the keys. I confirmed this on the UserKeys that I created and looked for repeating values.
While the UserKeys were not randomized well, there were two parts that seemed pretty random. And it would be impossible to brute force these values. But, I noticed that the last four bytes of the UserKey repeated in some UserKeys.
Additionally, when this repetition happened, there were also four middle bytes that remained the same. These two numbers were the only ones that seem well randomized. The UserKeys became almost identical, suggesting a relationship between the last four bytes and the middle four bytes.
Brute forcing these two numbers would be very difficult due to the large number of possible combinations, so that wasn’t a possible approach here. I tried sorting all the keys and got a large number of keys with matching values. These UserKeys were almost identical, except those outlined in red below, which turned out to be sequential.
Some of these UserKeys followed a sequence, but there were a few missing. For example, between a zero and a seven, there are six missing keys. After looking into it, I realized that those missing keys were already taken by other users. If I looked for those gaps, I could fill them with user keys from other devices.
I knew all these missing user keys actually belonged to other devices. But what could I do with the device’s user key? Luckily, I found the perfect API to solve this puzzle. To retrieve the user’s information, I used the UserProfile API by passing the UserKey as an argument. When I made the request, the server responded with the details related to that UserKey. If there was an account associated with the UserKey, the response would include the account’s phone number. And if I had the phone number of an account, I could hijack that account with the brute force attack, right?
In the demonstration below, I show the process of obtaining UserKeys or newly registered devices and phone numbers. I also attempted to test whether downloading and registering the payment service would be detected by the script.
With the script now running, I could catch a newly registered account and get their phone numbers. I left the script to run for a few hours, and it was able to catch a large number of accounts, including some used globally.
For each account, I could link a phone number that belonged to the user. From that point, I could practically obtain all the newly registered users of Moovit. I would have a lot of phone numbers of accounts that could be attacked through brute force. This made the brute force attack more powerful, since I no longer needed a known phone number. Instead, I could go through all the newly registered accounts and attack them.
However, this method still disconnects the account from the original device, which the user would notice. I had this list of UserKeys and their phone numbers, and I wanted to be able to impersonate a user without disconnecting them from their device. To do this, I would have to understand how the API request looked from the user’s account perspective.
Vulnerability #3 – Enhancing capabilities
At this point, I knew that having just the UserKey wasn’t sufficient in order to purchase train tickets, and I tried a brute force attack that involved hijacking an account by trying all possible OTP options using an existing account’s phone number.
Next, I wanted to explore how ticket purchasing worked in relation to API requests and delve into the internals of the APIs involved in this attack. A few reminders about the create user API request: when the application is first opened, a UserKey is generated for the device and requested through this API. After this API request, Moovit stores account information like personal details and credit card data, along with the UserKey that identifies the device. But there is one more essential requirement we missed: the access token. Without it, communicating with the server is impossible.
The access token is generated immediately after the device receives its UserKey. By sending a Register API request, the server responds with a temporary token. With this token, I could request verification from a third-party by providing the token along with the UserKey. In this case I used Google.
The issue now was that I had the user key of an existing account, but didn’t have their access token. This meant I could not make an API request on behalf of the account (e.g. ordering a train ticket). If I wanted to fully impersonate the account without disconnecting it from their device, I needed to obtain an access token.
The access token is actually a JSON web token (JWT). The JWT holds the UserKey identifier associated with the device that received this token. Could I forge this token by modifying the payload with a different UserKey?
No; this approach isn’t possible due to the way JWT works. A JWT consists of three parts: the header, payload, and the encrypted payload. The payload contains the interesting information that I wanted to modify, but the encrypted payload (shown in blue below) is an encrypted version of that payload. If I made changes to the payload, the token would no longer be valid because the data within the encrypted payload wouldn’t match the payload itself.
If I couldn’t forge a JWT, how else could I get a token? Through this research, I’ve learned that sometimes the simple things will work. Since I already had a UserKey of a different account, I wanted to see if I could request another token for this UserKey, even though there was already an active token. To do that, I sent Register and then VerifyCustomToken requests. As it turns out, there was no validation on the access token creation request. This meant I could request as many tokens as I wanted, even though there was an active account and active token.
Now I was able to fully impersonate accounts without disconnecting them from the original device. This also meant I would access all of their personal information. With the information collected through my script, I had the ability to access each of these accounts and retrieve their personal information, including their credit cards and details about their ongoing rides. This would enable me to track the location of users.
I conducted a proof of concept in which I took each of the UserKeys and used them to create a database containing various personal information such as their ID, email address, phone number, home address, and so on. Each account also had a discount profile that determined the percentage of discount it received. For example, people over the age of 75 in Israel get free public transportation. If I used such an account to order a train ticket, there would be no charge for the fare.
In the demonstration below, I provide an overview of what I was able to accomplish once I developed a user interface that allowed me to select the source station, discount profile, and profile to pay for my ride. Ultimately, I was able to get a ticket and pass through the gates for free.
After conducting our research, we responsibly disclosed the vulnerabilities to Moovit. They took immediate action to patch and remediate all vulnerabilities, and no customer action is required.
This research has proven that even big companies can fall victim to simple attacks. This is only one instance that highlights the need for an increased awareness and attention for basic security measures. These large organizations have the ability to affect a wide array of people, as was the case for this public transportation system.
While the findings and explanations in this research are based on the transportation in Israel, it may also apply to other countries. And while I focused on the train services, the techniques may apply to other transport services, meaning the implications could be significant and far-reaching.
When protecting against these types of vulnerabilities, it’s highly important to understand the researcher’s perspective. Challenges can turn into opportunities; limitations can be used to gain positive results. At the end of the day, we must think like our adversaries in order to defend against them.
To help mitigate the potential impact of these vulnerabilities, we have:
- Responsibly disclosed our research findings to Moovit, and they have fixed the vulnerabilities.
- Provided a research repository that includes the scripts I used to verify these types of attacks and can serve as a basis for further research and development.
- Shared our research openly with the broader security community here and at our recent DEF CON presentation in the hopes that transportation organizations will become more vigilant and ensure basic security measures are taken to protect against such vulnerabilities.
For more in-depth information about this research, please:
- Contact your customer success representative if you are a current SafeBreach customer
- Schedule a one-on-one discussion with a SafeBreach expert
- Contact Kesselring PR for media inquiries
Special thank you to Shmuel Cohen, security researcher at SafeBreach, for helping me throughout this research.
About Our Researchers
Omer Attias is a security researcher on the SafeBreach Labs team with over six years of experience in cybersecurity. He started his professional career in the Ministry of Defense and then at the Israel Defense Force (IDF). Most of his work is focused on network research, including Windows Internals and Linux kernel components.