Vulnerabilities in ATM Milano's mobile app

August 18, 2020Versione in taliano

Some design flaws left ATM Milano’s mobile app vulnerable to attacks: anyone could access any users’ data and tickets by just knowing their e-mail address. Meanwhile, some apparent security features made the vulnerabilities harder to spot and to exploit.

Background

ATM, short for Azienda Trasporti Milanesi, is the public transport company of Milan. ATM Milano’s app, available for both Android1 and iOS2, provides users with some useful features, such as the ability search for bus stops and view the estimated time of arrival; receive traffic news via push notifications; look for directions to a given destination. It also allows purchasing tickets that can be used on the public transport network and which users can pay either via SMS, credit card or PayPal.

Screenshot of ATM Milano’s app

The reason why I started looking into this app is simple. As an ATM user, I wanted to extract and document the API in order to use the features provided by the app on different platforms than those for which the app was available. Just to give you an idea, I wanted to have a widget showing the ETA of the tram that stops near my home on the desktop of my computer.

Traffic Analysis

As always, my reverse engineering started with an HTTPS proxy configured on the smartphone I had installed the app on, in order to have a look at the app’s traffic. Often this is all that’s needed to gather the required information and no further research is needed. This seemed the case: since the app didn’t even perform any certificate pinning3, I thought I’d finish my work in a matter of minutes.

Requests made by the app during login to the reserved area, from which users can buy tickets, unexpectedly caught my attention. Let’s have a look.

Login Request

Everything begins with the app submitting user’s credentials to the backend, as expected.

POST /v2/en/Membership/ValidateUser HTTP/1.1
ContentType: application/json;charset=utf-8
Timestamp: Tue, 12 Jun 2018 19:17:42 GMT
User-Agent: Android/6.0.1 (Nexus 5) it.atm.appmobile/3.4
Authentication: ATMApp:rDrN5r2icWXNuK1txneyDZlqiOicDYjuFan13Eaivmg=
Content-Type: application/json; charset=utf-8
Content-Length: 59
Host: atm-be.sg.engitel.com
Connection: Keep-Alive
Accept-Encoding: gzip

{
    "username": "[email protected]",
    "password": "mypassword"
}

Here is the server response:

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 1
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.5
Request-Context: appId=cid-v1:815dcd44-dd4b-45ce-b0e0-cc8a66a1f0c5
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Tue, 12 Jun 2018 19:17:43 GMT

0

Almost everything looks right so far. I say “almost” because despite looking fine at a first glance, the most observant among you will notice that something’s missing from the server response.

Someone may also ask what’s the matter with that 0 in the response body. We can investigate by repeating the login with different credentials and look at the result.

Here’s what it looks like:

  • 0 is returned when the login was successful (as in the case above).
  • 1 is returned when the username does not exist.
  • 4 is returned when the username does exist but the password is incorrect.

Using distinct status codes for these latter two cases is quite an odd design choice, behind which there are probably some valid (yet unknown) reasons. The fact that this is the intended behavior is anyway confirmed by the app UI, which displays two different error messages in the two situations. But let’s move on.

Wallet Request

Now that we’re logged in, the app requests our wallet, that is the list of purchased tickets.

GET /v2/en/ticketing/wallet HTTP/1.1
ContentType: application/json;charset=utf-8
Timestamp: Tue, 12 Jun 2018 19:17:43 GMT
User-Agent: Android/6.0.1 (Nexus 5) it.atm.appmobile/3.4
Authentication: [email protected]:rBm2e1qft7FUZMBbHJLOLC27xzBM/IbUJ3ihiOX8LGI=
Host: atm-be.sg.engitel.com
Connection: Keep-Alive
Accept-Encoding: gzip

When I captured this request my wallet contained an unused single urban ticket.

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 582
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.5
Request-Context: appId=cid-v1:815dcd44-dd4b-45ce-b0e0-cc8a66a1f0c5
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Tue, 12 Jun 2018 19:43:44 GMT

[
    {
        "MobileTicketId": "PXSL9GUUH",
        "ValidationTimeStamp": "1997-01-01T00:00:00",
        "ExpirationTimeStamp": null,
        "QrCodeData": null,
        "ValidationGuid": null,
        "Description_ENG": null,
        "Description_IT": null,
        "Duration": 90,
        "Price": 1.5,
        "TariffId": 2095,
        "MaxValidationsAllowed": 1,
        "Description": "Single Urban Ticket",
        "Instruction": "Valid for a single journey on the underground or rail network, including the urban rail lines of Trenord and the 'Passante Ferroviario' (Urban Railway Network).",
        "DurationDescription": "90'",
        "Duration_IT": null,
        "Duration_ENG": null,
        "Instruction_IT": null,
        "Instruction_ENG": null
    }
]

One Suspicious Header, One Vulnerability

If you didn’t notice anything missing so far, and if mentioning cookies doesn’t ring you a bell, have another look while asking yourself how a session is being maintained along those stateless requests. Or, more explicitly: how does the server make sure that users can only request their own wallet?

Since cookies are nowhere to be found and there does not seem to be any other session token either, let’s focus on what looks like the only security feature in place: the Authentication header.

It’s easy to spot that the header consists of two parts separated by a colon, the first part being the username of the logged-in user (or ATMApp if no user is logged in, as seen before), and the second being some base64-encoded data which changes at each request.

While we don’t know the meaning of the base64-encoded data yet, one question arises: what happens if we edit a request (e.g. by using proxy tools) and replace our username with someone else’s?

I decided to try. I called one of my colleagues and explained to him what I was doing; then I asked him to buy a ticket on his account and to give me the e-mail address he was registered with. I configured the proxy rewrite tool in order to replace the username in the Authorization header with his e-mail address, then I repeated the wallet request.

Quite surprisingly, the server didn’t complain and handed me my colleague’s wallet, which contained the ticket he had just bought. Notice the different MobileTicketId (aka PNR4).

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 582
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.5
Request-Context: appId=cid-v1:815dcd44-dd4b-45ce-b0e0-cc8a66a1f0c5
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Tue, 12 Jun 2018 20:32:12 GMT

[
    {
        "MobileTicketId": "E57C41F19",
        "ValidationTimeStamp": "1997-01-01T00:00:00",
        "ExpirationTimeStamp": null,
        "QrCodeData": null,
        "ValidationGuid": null,
        "Description_ENG": null,
        "Description_IT": null,
        "Duration": 90,
        "Price": 1.5,
        "TariffId": 2095,
        "MaxValidationsAllowed": 1,
        "Description": "Single Urban Ticket",
        "Instruction": "Valid for a single journey on the underground or rail network, including the urban rail lines of Trenord and the 'Passante Ferroviario' (Urban Railway Network).",
        "DurationDescription": "90'",
        "Duration_IT": null,
        "Duration_ENG": null,
        "Instruction_IT": null,
        "Instruction_ENG": null
    }
]

At this point I felt I had to dig deeper and understand the details of second part of the Authorization header too.

Integrity Checks?

The first thing I noticed is that the data length was fixed, 256 bit long, and that immediately brought SHA-256 to my mind. Then I noticed a Timestamp header also being sent with each request, and I recognized a design pattern that is often used to authenticate requests with HMAC5. In few words, some critical parts of each request are joined together with the timestamp of the request and with a shared secret that is only known to the client and to the server. The result is hashed and sent to the server along with the request.

Before processing it, the server can take the same parts of the request and hash them, just like the client did. If the resulting hash matches the one sent by the client, the integrity of the involved pieces of data is verified. If, on the other hand, we modify any critical part of the request, then the server will obtain a different hash, detect that something’s wrong and deny access. Finally, the timestamp is used to prevent requests from being repeated after some specific amount of time.

Unfortunately there is no standard defining which parts of a request should be considered critical and thus included while computing the HMAC. What we certainly know so far is that the username of the logged-in user is not among them, because if it was then my attempt to get my colleague’s wallet would have failed. In order to dig deeper into the details of this implementation, we need to decompile the app.

Diving Into the App

Extracting a smartphone app – an Android app in this case – is as easy as decompressing a zip file. I would normally use jadx6 or some similar tool in order to extract the apk file and decompile the binary in a single step. That would provide me with a good approximation of the app’s Java source code.

However, in this case taking a look at the contents of the apk file reveals something interesting:

it.atm.appmobile.apk
├── assemblies
│   ├── ATM.dll
│   ├── Microsoft.AppCenter.dll
│   ├── Microsoft.CSharp.dll
│   ├── Mono.Android.dll
│   ├── Mono.Security.dll
│   ├── Xamarin.Android.Arch.Core.Common.dll
│   ├── Xamarin.Android.Support.v4.dll
...

What are those DLL files doing there? The answer is simple: this app is made with Xamarin7, a framework by Microsoft that enables developers to write cross-platform apps in C#. This means that those DLL files make up the whole logic of the application, while the app binary is there just to create the Microsoft .NET runtime environment.

This is actually great news. Binaries and libraries built for Microsoft .NET are notoriously easy to decompile, and the resulting source code is often so much close to the original one that it can be recompiled. Furthermore, several programs exist that can do the job, such as dotPeek8 and dnSpy9. For both of them dragging-and-dropping the DLLs is all that’s needed in order to get some very nice and readable source code.

No More Secrets

While looking through the reconstructed sources, we can easily spot the code we’re looking for.

Here’s the function that joins requests’ parameters that should be included in the HMAC computation (comments were added by me):

public string Autentication {
    get {
        return string.Format("{0}\n{1}\n{2}\n{3}", new object[4] {
            (object) this.Method,              // Request method
            (object) this.DateUTC,             // Current timestamp
            (object) this.UriAction.ToLower(), // Request path
            (object) this.Parameters           // Query string
        });
    }
}

And here’s the function that computes the actual hash, taking the shared “secret” and the output of the function above as parameters:

public static string ComputeHash(string hashedPassword, string message) {
    UTF8Encoding utF8Encoding = new UTF8Encoding();
    HMACSHA256 hmacshA256 = new HMACSHA256(utF8Encoding.GetBytes(hashedPassword.ToUpper()))
    str = Convert.ToBase64String(hmacshA256.ComputeHash(utF8Encoding.GetBytes(message)));
    return str;
}

I put the word “secret” in quotes because the string is actually hard-coded into the app source code.

private const string AuthenticationHeaderName = "Authentication";
private const string TimestampHeaderName = "Timestamp";
private const string ConsumerKey = "ATMApp";
private const string ConsumerSecret = "jn2ic5az"; // Shared secret

I would like to emphasize that there is no secure way to store such a secret on the client side. Hard-coding it is not bad design per se; in fact, putting it elsewhere or trying to obfuscate it wouldn’t have made the app any more secure. It would have just made my task harder.

This information answers the only open question we had left. We’re dealing with an HMAC-SHA256 indeed, and now we know all the ingredients we need in order to calculate the correct hash for any request. This means we can craft new requests and customize them in every aspect, and the server will still accept them. Before knowing how the HMAC is built, instead, we couldn’t tamper with the request path and query string. I made a proof of concept that can prove this ability, and you can find it near the end of this post.

We can now summarize the vulnerabilities I found.

Vulnerabilities

An HMAC Is the Only Security Feature in Place

As we saw, the app doesn’t keep track of a session using cookies, as it’s commonly done; instead the only security feature it implements is based on a shared “secret”. Since it’s impossible to safely store a secret on the client side, the integrity that HMAC should provide cannot be guaranteed. A malicious user could extract the secret and use it to sign custom requests, just as we did.

The HMAC Implementation Is Not Secure Enough

Request parameters used for HMAC generation are:

  • request method (GET, POST, etc.)
  • current timestamp
  • request path (e.g. /v2/en/ticketing/wallet)
  • query string

Given that the username of the (possibly) logged-in user is not among them, even if the previous vulnerability didn’t exist, it would still be possible to tamper with requests in order to retrieve data and tickets belonging to other users, as we did before.

Since the server returns tickets with their PNR, it would also be possible for a malicious user to redeem tickets belonging to another user by simply entering the code on an automated ticket selling machine. Or he could directly abuse the API in order to validate the ticket and obtain the QR code to be used at the turnstiles.

I could also add that the body of POST requests is also not considered in HMAC generation, potentially leaving other endpoints vulnerable to attacks.

The Solution

Vulnerabilities described here have been solved by ATM with the use of JWT10. In current versions of the app, the Authentication header for logged-in users does not contain the plaintext username and HMAC anymore. Instead, the username is encoded in a signed token.

GET /v3/en/ticketing/wallet HTTP/1.1
Host: atm-be.sg.engitel.com
Connection: keep-alive
Authentication: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImphY29wby5qQGV4YW1wbGUuY29tIiwiaXNzIjoiQVRNQkUiLCJhdWQiOiJBVE1BcHAiLCJleHAiOjE1OTk5OTA4MzUsIm5iZiI6MTU5NzM5ODgzNX0=.Q5e4wX1m49PZsyxSViqFiDopg8ouXaRnW_FZOhIUiR8:T+TP/xbbr67dwdY6Zj3GVm3GPaGuLd/bB4b3ks+08zw=
Accept: */*
ContentType: application/json;charset=utf-8
User-Agent: iOS/14.0 (iPhone) it.atm.iATMMilano/7.4.1
Timestamp: Mon, 17 Aug 2020 13:39:39 GMT
Accept-Language: en-us
Accept-Encoding: gzip, deflate, br

I won’t enter the details of JWT specifications here. Let’s just notice that the token consists of three parts joined by a single dot, and that each part is base64 encoded. The first part encodes some information about the signature algorithm used in this implementation:

{
  "typ": "JWT",
  "alg": "HS256"
}

The second part contains the username and some token validity constraints:

{
  "username": "[email protected]",
  "iss": "ATMBE",
  "aud": "ATMApp",
  "exp": 1599990835,
  "nbf": 1597398835
}

Finally, the third part is the signature of the first two. We are once again talking about a HMAC-SHA256 signature in this case, so what’s different from before? Why is this approach more secure? The answer stands with the fact that the signed token is returned by the server during login.

This means that the client never gets to know the signing secret, which is no longer shared but only known to the server. This makes it impossible to extract the secret and use it to sign custom requests.

Even tho the use of cookies was not taken into consideration (and the API was left strictly stateless) an effective countermeasure was implemented in order to prevent users from impersonating somebody else. HMAC was left there, but it is no longer in charge of protecting the API from this kind of abuses.

Responsible disclosure timeline

  • May 27, 2018: I find out about the vulnerability and reach out to ATM.
  • May 29, 2018: ATM gets in touch; I show them the problems that should be addressed by the app developers.
  • June 14, 2018: I hand over to ATM a proof of concept demonstrating the ability of “stealing” tickets from other users.
  • July 11, 2018: I meet the managers of ATM’s Information Systems department at their headquarters in order to further discuss the vulnerabilities.
  • During fall 2018 ATM releases an update that fixes the vulnerabilities I found. Vulnerable endpoints are left up and running for backwards compatibility with older versions of the app.
  • During summer 2020 vulnerable endpoints are taken down.
  • August 11, 2020: I learn that vulnerabilities are not accessible anymore and that my proof of concept has stopped working.
  • August 18, 2020: I publish this post.

Open Source Proof of Concept

Along with this post I’m open sourcing the proof of concept I made for ATM, which is available on my GitHub. It’s a single PHP page that allowed to exploit vulnerabilities I found by showing the wallet of any user given his e-mail address. Of course the code is not working now that the vulnerabilities have been fixed. I still believe it may be interesting for anyone wanting to give a closer look to how they could be exploited.


  1. https://play.google.com/store/apps/details?id=it.atm.appmobile ↩︎

  2. https://apps.apple.com/us/app/iatm-milan/id415637297 ↩︎

  3. Certificate pinning, or public key pinning, adds a mild security layer by preventing proxies to decode requests made to the backend. Further details ↩︎

  4. Passenger Name Record - it’s a code that uniquely identifies tickets on the transportation network. ↩︎

  5. More in-depth information about HMAC ↩︎

  6. https://github.com/skylot/jadx ↩︎

  7. https://dotnet.microsoft.com/apps/xamarin ↩︎

  8. https://www.jetbrains.com/decompiler/ ↩︎

  9. https://github.com/0xd4d/dnSpy ↩︎

  10. https://jwt.io ↩︎

atmmilanoappzeroday
Share this post:

Does Apple really log every app you run? A technical look

Reverse engineering Trenitalia's mobile application