28 September 2016

Hacking Around with AirSerbia's InFlight WiFi

DISCLAIMER:I use the word 'hacking' in the truest sense, I didn't probe the plane network or attempt to get anywhere I wasn't supposed to. I simply captured / repeated traffic that my browser would have been sending if sat on the Captive Portal landing page

I recently flew back from Serbia on an Air Serbia Airbus A319 and noticed that it had WiFi. As with Virgin Atlantic this flight was using the Panasonic Avionics Network system.

The first thing to notice was that this Airbus A319 didn't populate the following fields;

td_id_fltdata_wind_speed
td_id_fltdata_mach
td_id_fltdata_outside_air_temp
td_id_fltdata_date
td_id_flight_phase
td_id_route_id
td_id_media_date

At one point I noticed that td_id_x2_pa_state flipped from 0 to 1 and my phone, which had been on the captive portal page, switched to a full height div with PA Announcement In Progress which was interesting (and providing further ideas for use in a generic PAN app)

Again the GPS was much more jagged from PAN than from somewhere like FlightAware



Altitude etc

Whilst it took a while for the inflight WiFi to kick in after we'd climbed above 10,000 feet it promptly terminated as soon as we dropped below that height.





InFlight Entertainment (and a deeper insight into the Panasonic Aero Network Backend)

AirSerbia doesn't have in-headrest monitors instead encouraging people to install an app. For Apple users it apologises and encourages you to download the app before boarding your next flight, however for Android users they provide the APK from within the plane. To install this you'd have to disable the Google protections regarding 3rd party apps. Now whilst I like this (from a FOSS / it's "my" computer perspective) this is a very bad user pattern to be encouraging and I'd suggest AirSerbia stop doing it.

For reference the APK can be downloaded here (34Mb!).

The app appears to use code from NexStreaming

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<NexPlayerLicense version="2.0">
  <!-- Please feel free to delete the non-encrypted and readable
    list of supported app IDs given right under these lines ("SupportAppIDs" tag).
    The license file will be equally valid in this case -->
  <SupportAppIDs> 
    <appID>com.airserbia.mediaplayer.preview
    </appID>  
  </SupportAppIDs>

  <Config>
    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
  </Config>
</NexPlayerLicense>

Interestingly the apps was signed with a key that possibly has something to do with Panasonic

Owner: CN=Panasonic
Issuer: CN=Panasonic
Serial number: 5ca3f33f
Valid from: Mon May 11 22:22:04 BST 2015 until: Tue Apr 17 22:22:04 BST 2114
Certificate fingerprints:
   MD5:  A0:AD:33:3C:AC:4A:CE:B8:03:B5:76:9F:97:DD:0E:7F
   SHA1: 05:8B:4C:27:91:82:48:63:05:19:42:16:DE:27:38:26:D2:31:2B:6C
   SHA256: C2:CA:55:DB:98:71:33:6C:62:16:00:89:CC:D9:FE:5B:71:6B:00:46:75:98:4C:55:47:F5:9F:DD:46:DD:EE:14
   Signature algorithm name: SHA256withRSA
   Version: 3

Extensions: 

#1: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: 41 EA 03 D1 F8 64 4F B7   21 C1 4E 43 FD A4 ED BD  A....dO.!.NC....
0010: 49 DE 21 4F                                        I.!O
]
]

This link to Panasonic becomes more evident deeper in the app where we find the Android activities are named com.pac.dummyairline.ACTIVITY and the services are named things like aero.panasonic.inflight.services.ifeaodservice.IfeAodService and aero.panasonic.inflight.remote.service

The decompiled APK provides a full suite of code from the aero.panasonic.inflight.* namespace. Digging around here shows various URLs that the system might respond on;

private static final String AIR_SERVER_HOST_NAME = "api.airpana.com";
private static final String GRND_SERVER_HOST_NAME = "cadev.panasonic.aero";
private static final String SERVICE_INFLIGHT_PANASONIC_AERO = "services.inflightpanasonic.aero";

It's worth noting that cadev.panasonic.aero is reachable from the Internet... (Well it is the GRND_SERVER host name)

The file aero/panasonic/inflight/services/service/ServerRequestGenerator.java shows even more endpoint URLs along with descriptive names;

METHOD_GET_CHALLENGE = "/inflight/services/auth/v1/challenge";
METHOD_GET_VALIDATE_TOKEN = "/inflight/services/auth/v1/validate_token";
METHOD_POST_SOLUTION = "/inflight/services/auth/v1/solution";
AI_MULTI_AIRPORT_INFO_MSG = "/inflight/services/airport_info/v1/multi_airport_info";
AI_URL_AIRPORT_INFO_BASE = "/inflight/services/airport_info/v1/";
A_ANALYTICS_MSG = "/inflight/services/analytics/v1/log";
A_URL_ANALYTICS_BASE = "/inflight/services/analytics/v1/";
COMPONENT_UPLOAD_URL_BASE = "/inflight/services/cmi/lms/v2/";
CREW_AUTHENTICATION_URL_BASE = "/inflight/services/cmi/auth/login/";
EXTV_METADATA_AVAILABLE_STATIONS_MSG = "/inflight/services/extv_metadata/v1/stations";
EXTV_METADATA_BASE = "/inflight/services/extv_metadata/v1/";
EXTV_METADATA_COMMISSIONING_STATUS_MSG = "/inflight/services/extv_metadata/v1/commissioning_status";
EXTV_METADATA_STATION_STATUS_MSG = "/inflight/services/extv_metadata/v1/station_status";
FD_FLIGHTDATA_MSG = "/inflight/services/flightdata/v2/flightdata";
FD_URL_FLIGHTDATA_BASE = "/inflight/services/flightdata/v2/";
FMI_FLIGHT_MAP_AVIALABLE_RESOLUTION_MSG = "/inflight/services/maps/v1/flight_map";
FMI_FLIGHT_MAP_IMAGE_MSG = "/inflight/services/maps/images/";
FMI_URL_FLIGHTMAP_IMAGE_BASE = "/inflight/services/maps/";
FMI_URL_FLIGHTMAP_RESOLUTION_BASE = "/inflight/services/maps/v1/";
HSP_CATALOGS = "/inflight/services/catalogs/hospitality/browse/v1/catalogs";
HSP_CATEGORIES = "/inflight/services/catalogs/hospitality/browse/v1/categories";
HSP_IMAGES = "/inflight/services/catalogs/hospitality/browse/v1/images/";
HSP_ITEMS = "/inflight/services/catalogs/hospitality/browse/v1/items";
HSP_URL_BASE = "/inflight/services/catalogs/hospitality/browse/v1/";
MD_IMAGES_MSG = "/inflight/services/metadata/v1/images/";
MD_METADATA_CATEGORY_MEDIA_MSG = "/inflight/services/metadata/v1/category_media";
MD_METADATA_CATEGORY_MSG = "/inflight/services/metadata/v1/categories";
MD_METADATA_CHILD_MEDIA_MSG = "/inflight/services/metadata/v1/child_media";
MD_METADATA_MSG = "/inflight/services/metadata/v1/media_metadata";
MD_METADATA_SEARCH_MSG = "/inflight/services/metadata/v1/search";
MD_URL_MEDIA_METADATA_BASE = "/inflight/services/metadata/v1/";
PACKAGE_UPLOAD = "/inflight/services/cmi/lms/v2/lms_package_upload";
PC_PARENTAL_CONTROL_RATING_ALL_MSG = "/inflight/services/parental_control/v1/ratings";
PC_PARENTAL_CONTROL_RATING_MSG = "/inflight/services/parental_control/v1/rating";
PC_URL_PARENTAL_CONTROL_BASE = "/inflight/services/parental_control/v1/";
SD_AVAILABLE_SERVICE_MSG = "/inflight/services/service_discovery/v1/servicesservices";
SD_URL_SERVICE_DISCOVERY_BASE = "/inflight/services/service_discovery/v1/services";
SEAT_PAIRING_INITIATE_MSG = "/inflight/services/remote_comm/v1/initiate_pair";
SEAT_PAIRING_INITIATE_WITH_PASSCODE_MSG = "/inflight/services/remote_comm/v1/ped_pair";
SEAT_PAIRING_MODE_MSG = "/inflight/services/remote_comm/v1/switch_pair_mode";
SEAT_PAIRING_STATUS_MSG = "/inflight/services/remote_comm/v1/pair_status_v2";
SEAT_PAIRING_UNPAIR_MSG = "/inflight/services/remote_comm/v1/ped_unpair";
SEAT_REMOTE_CONTROL_MSG = "/inflight/services/remote_comm/v1/send_message";
SH_CATALOGS = "/inflight/services/catalogs/shopping/browse/v1/catalogs";
SH_CATEGORIES = "/inflight/services/catalogs/shopping/browse/v1/categories";
SH_IMAGES = "/inflight/services/catalogs/shopping/browse/v1/images/";
SH_INVT_ITEM = "/inflight/services/cmi/catalogs/shopping/inventory/v1/item";
SH_INVT_ITEM_ADST = "/inflight/services/cmi/catalogs/shopping/inventory/v1/adjust_item_quantity";
SH_INVT_ITEM_LIST = "/inflight/services/cmi/catalogs/shopping/inventory/v1/all_items/";
SH_INVT_URL_BASE = "/inflight/services/cmi/catalogs/shopping/inventory/v1/";
SH_ITEMS = "/inflight/services/catalogs/shopping/browse/v1/items";
SH_URL_BASE = "/inflight/services/catalogs/shopping/browse/v1/";
SP_URL_SEAT_PAIRING_BASE = "/inflight/services/remote_comm/v1/";
SYS_AVAILABLE_SER_INFO_MSG = "/inflight/services/service_control/v1/all_services";
SYS_URL_SYS_INFO_BASE = "/inflight/services/service_control/v1/";

The file aero/panasonic/inflight/services/ifeservice/ServerEventList.java exposes that the app seems to use different JSON fields to the web portal;

   private static final String SERVER_ALTITUDE = "core.flightdata.altitude_feet";
    private static final String SERVER_CURRENT_COORDINATES = "core.flightdata.current_coordinates";
    private static final String SERVER_DECOMPRESSION = "core.flightdata.td_id_decompression";
    private static final String SERVER_DEPARTURE_COORDINATES = "core.flightdata.departure_coordinates";
    private static final String SERVER_DEPARTURE_IATA = "core.flightdata.departure_iata";
    private static final String SERVER_DEPARTURE_ICAO = "core.flightdata.departure_icao";
    private static final String SERVER_DEPARTURE_UTC_OFFSET = "core.flightdata.departure_utc_offset_minutes";
    private static final String SERVER_DESTINATION_COORDINATES = "core.flightdata.destination_coordinates";
    private static final String SERVER_DESTINATION_IATA = "core.flightdata.destination_iata";
    private static final String SERVER_DESTINATION_ICAO = "core.flightdata.destination_icao";
    private static final String SERVER_DESTINATION_UTC_OFFSET = "core.flightdata.destination_utc_offset_minutes";
    private static final String SERVER_DISTANCE_COVERED_PERCENTAGE = "core.flightdata.distance_covered_percentage";
    private static final String SERVER_DISTANCE_FROM_DEPARTURE = "core.flightdata.distance_from_departure_nautical_miles";
    private static final String SERVER_DISTANCE_TO_DESTINATION = "core.flightdata.distance_to_destination_nautical_miles";
    private static final String SERVER_ESTIMATED_ARRIVAL_TIME = "core.flightdata.estimated_arrival_time_utc";
    private static final String SERVER_FLIGHT_MAP_UPDATE = "core.maps.map_update";
    private static final String SERVER_FLIGHT_NUMBER = "core.flightdata.flight_number";
    private static final String SERVER_FLIGHT_SPEED_MACH = "core.flightdata.flight_speed_mach";
    private static final String SERVER_GROUND_SPEED = "core.flightdata.ground_speed_knots";
    private static final String SERVER_HEAD_WIND_SPEED = "core.flightdata.head_wind_speed_knots";
    private static final String SERVER_LOCAL_NETWORK_AVAILABLE = "local.network.available";
    private static final String SERVER_LOCAL_NETWORK_UNAVAILABLE = "local.network.unavailable";
    private static final String SERVER_NOP = "core.event_server.nop";
    private static final String SERVER_OUTSIDE_AIR_TEMPERATURE = "core.flightdata.outside_air_temp_celsius";
    private static final String SERVER_PA = "core.flightdata.td_id_x2_pa_state";
    private static final String SERVER_PA_ANNOUNCEMENT = "core.flightdata.passenger_anouncement";
    private static final String SERVER_PA_ANNOUNCEMENT_NONMANDATORY = "core.flightdata.passenger_anouncement_nonmandatory";
    private static final String SERVER_ROUTE_ID = "core.flightdata.route_id";
    private static final String SERVER_TAIL_NUMBER = "core.flightdata.tail_number";
    private static final String SERVER_TAKEOFF_TIME = "core.flightdata.takeoff_time_utc";
    private static final String SERVER_TIME_AT_DESTINATION = "core.flightdata.time_at_destination";
    private static final String SERVER_TIME_AT_ORIGIN = "core.flightdata.time_at_origin";
    private static final String SERVER_TIME_TO_DESTINATION = "core.flightdata.time_to_destination_minutes";
    private static final String SERVER_TRUE_HEADING = "core.flightdata.true_heading_degree";
    private static final String SERVER_WEIGHT_ON_WHEELS = "core.flightdata.weight_on_wheels";
    private static final String SERVER_WIND_DIRECTION = "core.flightdata.wind_direction_degree";
    private static final String SERVER_WIND_SPEED = "core.flightdata.wind_speed_knots";

There is probably much more to be learnt from this APK and the associated code but I'll leave that to others...