Reverse Engineering a REST API - Nexus Metro Real Time Information

Reverse Engineering a REST API - Nexus Metro Real Time Information

Many transport systems make real-time information on the position of trains or their estimated arrival at a station. In my city of Newcastle, we are blessed with the Tyne and Wear Metro - not-always reliable, not-always fast light-rail system.

Map of the Tyne and Wear Metro System

Nexus makes make real-time information (RTI) of this sort available through their Pop app, available for Android and iOS. Timetables are available on the website and elsewhere, however these aren’t always to be relied on.

It would be great if this information was available for use in other projects. My personal goal is to build a widget for my Smart Mirror, that is able to provide at a glance information on my local station, instead of having to open the app and fill in a form every time I want to know.

Based on speculation and a limited knowledge of the API, the API queries a model that takes in the positions of all trains on the network (signal block or otherwise) over a time series and makes a maximum-likelihood estimate of when these trains will arrive at a station.

Otherwise, in this post I will document the steps I took to reverse engineer and document the API from the app, so that you can do the same for other undocumented APIs!

🚨
PSA — It is worth noting that this may break the End User License Agreement of the Pop App - Nexus please don’t take me to court! 😝

The Man In The Middle

Intercepting HTTPS Traffic

💡
You can follow this guide if you want to try this yourself.

Clearly, we need to intercept the traffic in order to observe the request flow. It would be great from our perspective, though poor from a development and security perspective, if the app was using plaintext HTTP to encode requests. However this is incredibly unlikely, so the starting assumption is that the API is accessed over HTTPS.

We can approach this by placing a HTTPS proxy between the device running the app and the API endpoint. I used mitmproxy (man-in-the-middle), a free and open source swiss-army knife for debugging, testing, privacy measurements, and penetration testing. The proxy terminates all SSL connections, generating on-the-fly self-signed certificates for any SNI it comes across in a request. It then forwards the request to the original endpoint and the response back to the device.

Because the proxy acts as it’s own certificate authority (CA), it is required to install the certificate generated by the proxy into your device so that it can be trusted. (Security and privacy → More security and privacy → Encryption and credentials → Install a certificate) Then, you can set up your device to send all HTTP traffic through the proxy. (Wi-Fi Settings → Proxy)

🚨
As an aside - since Android 7 apps ignore user provided certificates unless they are configured to use them. As most applications do not explicitly opt-in to use user certificates, it is required to place our CA certificate in the system certificate store or patch apps individually. See the documentation for workarounds.

Did it work?

It would be great if this was all that was required, and we would simply launch the mitmweb dashboard and examine the traffic however the app can be configured to use SSL Certificate Pinning to defeat a MITM attack.

The app holds a copy of either the server certificate or the corresponding public key, and compares this with the provided certificate in the connection request. If they do not match, nothing happens! We therefore can’t see the traffic with just this step, and must patch the app to be effective.

App Patching

APK Runtime Exploration

Fortunately, tools exist that can patch android APKs to remove pinned certificates and trust user-installed CAs. For this I used Objection, a runtime mobile exploration toolkit powered by Frida.

💡
You can follow this guide in conjunction with the below steps. I found most issues I had were largely down to outdated dependencies.
  1. Install objection and obtain up to date dependencies aapt, adb, jarsigner, and apktool.
  2. After connecting the device via adb, you can obtain the package files. First we find the package name. Then we get the file paths from the device and obtain them using $ adb pull $PATH.
$ adb shell pm list packages | grep nexus
package:uk.co.nebulalabs.nexusnextgeneration
$ adb shell pm path uk.co.nebulalabs.nexusnextgeneration
package:/data/app/uk.co.nebulalabs.nexusnextgeneration/base.apk
package:/data/app/uk.co.nebulalabs.nexusnextgeneration/split_config.arm64_v8a.apk
package:/data/app/uk.co.nebulalabs.nexusnextgeneration/split_config.en.apk
package:/data/app/uk.co.nebulalabs.nexusnextgeneration/split_config.xxxhdpi.apk
  1. We can then patch the app, embedding the Frida gadget within the app. In this case, we need to add a flag to use aapt2 because the app uses a specific resource in it’s manifest.
$ objection patchapk --source base.apk --use-aapt2
  1. If this is successful the app is patched, however because the app is supplied as a bundle (multi-apk), the certificates are now mismatched. Luckily, objection provides a command to update the signing of the other APKs.
$ objection signapk split_*.apk
  1. If all is successful, you can now uninstall the original app and replace it with yours!
$ adb uninstall uk.co.nebulalabs.nexusnextgeneration
  1. Launch the app and it will hang, waiting for you to launch the Objection runtime CLI.
$ objection explore
     _   _         _   _
 ___| |_|_|___ ___| |_|_|___ ___
| . | . | | -_|  _|  _| | . |   |
|___|___| |___|___|_| |_|___|_|_|
      |___|(object)inject(ion) v1.11.0

     Runtime Mobile Exploration
        by: @leonjza from @sensepost

[tab] for command suggestions
...nebulalabs.nexusnextgeneration on (google: 14) [usb] # android
Unknown or ambiguous command: `android`. Try `help android`.
...nebulalabs.nexusnextgeneration on (google: 14) [usb] # android sslpinning disable
(agent) Found okhttp3.CertificatePinner, overriding CertificatePinner.check()
(agent) Found okhttp3.CertificatePinner, overriding CertificatePinner.check$okhttp()
(agent) Found com.android.org.conscrypt.TrustManagerImpl, overriding TrustManagerImpl.verifyChain()
(agent) Found com.android.org.conscrypt.TrustManagerImpl, overriding TrustManagerImpl.checkTrustedRecursive()
(agent) Registering job 378778. Type: android-sslpinning-disable
  1. Because of the patching, the application has been told to trust any user-installed CA, and we have just disabled SSL pinning. We can now return to the mitmproxy web interface and start recording and observing the API request flow as we use the app’s features!

The API

Documenting the Nexus RTI API

RSS

There is an RSS live feed of travel updates at:
https://rss.nexus.org.uk/

RTI API

The RTI API is served from https://metro-rti.nexus.org.uk/api/ with a number of endpoints.

Status Endpoint

Supplies information about the current API and model:
GET https://metro-rti.nexus.org.uk/api/statuses

{
    "api_version":          "1.4.2.5",
    "current_model_time":   "2024-04-01T12:00:00.0000000+01:00",
    "initialisation_time":  "2024-04-01T03:00:39.3918183+01:00",
    "model_status":         "Initialised",
    "model_version":        "3.5.4.5",
    "operational_day":      "2024-04-01T03:00:00.0000000+01:00",
    "timetable_service_id": "1439"
}

Stations Endpoint

Provides station names and their corresponding codes:
GET https://metro-rti.nexus.org.uk/api/stations

{
    "APT": "Airport",
    "BDE": "Bede",
    "BFT": "Bank Foot",
    "BTN": "Benton",
    "BYK": "Byker",
    ...
}

Platforms Endpoint

Provides information about platforms at each station:
GET https://metro-rti.nexus.org.uk/api/stations/platforms

{
    "APT": [
        {
            "direction": "IN",
            "helperText": "Towards South Hylton",
            "platformNumber": 1
        },
        {
            "direction": "OUT",
            "helperText": "Towards Airport",
            "platformNumber": 2
        }
    ],
    "BDE": [
        {
            "direction": "IN",
            "helperText": "Towards South Shields",
            "platformNumber": 1
        },
        {
            "direction": "OUT",
            "helperText": "Towards St. James via Whitley Bay",
            "platformNumber": 2
        }
    ],
    ...
}

RTI Endpoint

Gives estimated train arrival, uses URL encoding for station code and platform number.
GET https://metro-rti.nexus.org.uk/api/times/$$sta$$/$$pno$$

[
    {
        "destination": "South Hylton",
        "dueIn": -1,
        "lastEvent": "ARRIVED",
        "lastEventLocation": "Pelaw Platform 1",
        "lastEventTime": "2024-04-01T12:00:00.0000000+01:00",
        "line": "GREEN",
        "trn": "154"
    },
    {
        "destination": "South Shields",
        "dueIn": 7,
        "lastEvent": "DEPARTED",
        "lastEventLocation": "Heworth Platform 2",
        "lastEventTime": "2024-04-01T19:42:12.0000000+01:00",
        "line": "YELLOW",
        "trn": "162"
    },
    {
        "destination": "South Hylton",
        "dueIn": 14,
        "lastEvent": "APPROACHING",
        "lastEventLocation": "Pelaw Platform 2",
        "lastEventTime": "2024-04-01T19:42:36.1200000+01:00",
        "line": "GREEN",
        "trn": "155"
    },
    ...
]

Car Park Occupancy API

Provides the occupancy levels of Nexus owned car parks, sorted by distance if it is provided.

GET https://hce-prod1.nexusaws.net/api/parking/spaces/find?latitude=12.34567890123456&longitude=-12.34567890123456
[
    {
        "capacity": 393,
        "description": "Low Heworth Lane, Heworth, Gateshead, NE10 0YJ",
        "distance": 1053,
        "free_spaces": 390,
        "idCode": "VMSLCP002",
        "is_live": true,
        "latitude": 54.9533745486823,
        "longitude": -1.55266667253195,
        "name": "Heworth Interchange (Long Stay)",
        "occupancy": 3,
        "status": "SPACES"
    },
    ...
]

Timetable API

Provides timetable information at each station at a given time.

POST https://ken.nebulalabs.cc/timetable/stop/
Body:
{
    "date": "2024-04-01T12:00:00",
    "stop_id": "1:$$STATION_CODE$$_$$PLATFORM_NO$$" // 
}
[
    {
        "arrival_date": "2024-04-01T12:00:00",
        "date": "2024-04-01T12:00:00",
        "departure_date": "2024-04-01T12:01:00",
        "line": "GREEN",
        "name": "To South Hylton Platform 2",
        "route_id": "1:154",
        "time_string": "12:00:00",
        "timestamp": 43200,
        "trip_id": "1:48B94425097131ADD3453F9923E91A7F"
    },
    ...
]

Journey API

Provides journey options from one station to another.

POST https://ken.nebulalabs.cc/journey/plan/
Body:
{
    "arrive_by": false,
    "date": "2024-04-01T12:00:00",
    "end": {
        "latitude": 54.95259455,
        "longitude": -1.542170635
    },
    "start": {
        "latitude": 54.95734882,
        "longitude": -1.486040092
    }
}
{
    "start_point": {
        "latitude": 54.9573488,
        "longitude": -1.4860401,
        "name": "Origin",
        "vertexType": "NORMAL"
    },
    "end_point": {
        "latitude": 54.9525946,
        "longitude": -1.5421706,
        "name": "Destination",
        "vertexType": "NORMAL"
    },
    "journey_options": [
        {
            "duration": 318,
            "end_time": "2024-04-01T19:20:30",
            "start_time": "2024-04-01T19:16:30",
            "start_timestamp": 1711998990000,
            "steps": [
                {
                    "duration": 240.0,
                    "end_point": {
                        "arrival": "2024-04-01T19:20:30",
                        "departure": "2024-04-01T19:20:30",
                        "helper_text": "Towards Airport and St. James via Whitley Bay",
                        "latitude": 54.9526329,
                        "longitude": -1.5419919,
                        "name": "Pelaw Platform 2",
                        "platform_number": 2,
                        "station_id": "PLW",
                        "station_name": "Pelaw",
                        "stopId": "1:PLW_2",
                        "stopIndex": 12,
                        "stopSequence": 6306,
                        "vertexType": "TRANSIT"
                    },
                    "end_time": "2024-04-01T19:20:30",
                    "line": "YELLOW",
                    "start_point": {
                        "arrival": "2024-04-01T19:16:30",
                        "departure": "2024-04-01T19:16:30",
                        "helper_text": "Towards Airport",
                        "latitude": 54.9574509,
                        "longitude": -1.485661,
                        "name": "Fellgate Platform 2",
                        "platform_number": 2,
                        "station_id": "FGT",
                        "station_name": "Fellgate",
                        "stopId": "1:FGT_2",
                        "stopIndex": 11,
                        "stopSequence": 6304,
                        "vertexType": "TRANSIT"
                    },
                    "start_time": "2024-04-01T19:16:30",
                    "stops_between": [],
                    "train_name": "Heworth - South Shields - Heworth",
                    "train_number": "161"
                }
            ],
            "transfers": 0
        },

Conclusion

If you read through all this you should now have a good idea of how to reverse engineer an undocumented back-end API. Do with this knowledge what you will, personally, the purpose of this endeavor was to develop my own 'at-a-glance' RTI widget app, so keep a lookout for a post about this in the future!