Last month I was over in Norway doing training for ProgramUtvikling, the good folks who run the NDC conferences I’ve become so attached to. I was running my usual “Hack Yourself First” workshop which is targeted at software developers who’d like to get up to speed on the things they should be doing to protect their apps against today’s online threats. Across the two days of training, I cover 16 separate discrete modules ranging from SQL injection to password cracking to enumeration risks, basically all the highest priority security bits modern developers need to be thinking about. I also cover how to inspect, intercept and control API requests between rich client apps such as those you find on a modern smart phone and the services running on the back end server. And that’s where things got interesting.

What the workshop attendee ultimately discovered was that not only could he connect to his LEAF over the internet and control features independently of how Nissan had designed the app, he could control other people’s LEAFs. I subsequently discovered that friend and fellow security researcher Scott Helme also has a LEAF so we recorded the following video to demonstrate the problem. I’m putting this up front here to clearly put into context what this risk enables someone to do then I’ll delve into the details over the remainder of the post:

We elected for me to sit outside in a sunny environment whilst Scott was shivering in the cold to demonstrate just how remote you can be and still control feature of someone else’s car, literally from the other end of the earth. Following is a complete walkthrough of the discovery process, how vehicles in other countries can also be controlled and a full disclosure timeline of my discussions with Nissan.

Connected LEAFs

The LEAF is an electric car which is particularly popular in countries like Norway which offer massive financial incentives to stay away from combustion engines. It does all the things you’d expect of a modern EV and because it’s here in the era of the internet of things, it also has a companion app:

Back at my workshop in Oslo and being the curious type, Jan (not his real name – he requested to remain anonymous) goes back to his hotel after the first day of the course and proxies his iPhone through Fiddler running on his PC as we’d done during the day (this was on January 20). This takes a few minutes to setup and effectively what it means is that he can now observe how the mobile app talks to the online services. Jan then fires up the NissanConnect EV app:

I’m using publicly available screen grabs of the app in part so as not to disclose personal information about Jan and part because his app runs in Norwegian. When the app opens, he observes a request like this (I’ll obfuscate host names and the last five digits of VINs throughout this post):

GET https://[redacted].com/orchestration_1111/gdc/BatteryStatusRecordsRequest.php?RegionCode=NE&lg=no-NO&DCMID=&VIN=SJNFAAZE0U60XXXXX&tz=Europe/Paris&TimeFrom=2014-09-27T09:15:21

Which returns the following JSON response:

This is pretty self-explanatory if you read through the response; we’re seeing the battery status of his LEAF. But what got Jan’s attention is not that he could get the vehicle’s present status, but rather that the request his phone had issued didn’t appear to contain any identity data about his authenticated session. In other words, he was accessing the API anonymously. It’s a GET request so there was nothing passed in the body nor was there anything like a bearer token in the request header. In fact, the only thing identifying his vehicle was the VIN which I’ve partially obfuscated in the URL above.

The VIN is the Vehicle Identification Number which uniquely identifies the chassis of his LEAF. It is by no means a “secret” suitable for authorisation purposes, the significance of which I’ll come back to shortly.

On the surface of it, it looked like anyone could get the battery status of Jan’s vehicle if they knew his VIN. Not ideal, but not exactly serious either as it’s a passive query (it doesn’t actually change anything on the vehicle) and there’s also nothing of a personal or sensitive nature returned in the response beyond potentially telling you when it was last driven based on the OperationDateAndTime field. So Jan kept looking.

He found he could check the status of the climate control using this request:

GET https://[redacted].com/orchestration_1111/gdc/RemoteACRecordsRequest.php?RegionCode=NE&lg=no-NO&DCMID=&VIN=SJNFAAZE0U60XXXXX

Which then returned a similar status result:

This is reflected within the app on this screen:

But again, it’s passive data – is the climate control on or off and as a result, what should the buttons say. But then he tried turning it on and observed this request:

GET https://[redacted].com/orchestration_1111/gdc/ACRemoteRequest.php?RegionCode=NE&lg=no-NO&DCMID=&VIN=SJNFAAZE0U60XXXXX&tz=Europe/Paris

That request returned this response:

This time, personal information about Jan was returned, namely his user ID which was a variation of his actual name. The VIN passed in the request also came back in the response and a result key was returned.

He then turned the climate control off and watched as the app issued this request:

GET https://[redacted].com/orchestration_1111/gdc/ACRemoteOffRequest.php?RegionCode=NE&lg=no-NO&DCMID=&VIN=SJNFAAZE0U60XXXXX&tz=Europe/Paris

All of these requests were made without an auth token of any kind; they were issued anonymously. Jan checked them by loading them up in Chrome as well and sure enough, the response was returned just fine. By now, it was pretty clear the API had absolutely zero access controls but the potential for invoking it under the identity of other vehicles wasn’t yet clear.

Connecting to other vehicles

When Jan came into the workshop the following day, he also brought in a picture he’d managed to locate by searching the web:

This was the vehicle’s VIN which clearly, left us curious (obfuscation is mine, it’s legible in its entirety on the web).

Let me clarify something before going any further and it’s something I harp on about in my workshops too; when a potential security flaw is identified, you’ve got to think very carefully about how you proceed with verification. You need to have a sufficient degree of confidence that it’s a legitimate flaw before reporting it ethically (which is what we ultimately did), but you also need to ensure you don’t breach someone else’s privacy or impact them adversely in any way. We wouldn’t, for example, want to start operating mechanical features of someone else’s car such as turning on the climate control nor would we want to retrieve personal information about them, even if it was just their username.

The VIN above differed merely by the last 5 digits. We grabbed the number and plugged it into the request to get the battery status – a request that didn’t change anything nor disclose anything private – and got this response:

This appeared to indicate that the response couldn’t be processed but it wasn’t clear why. On reflection, it’s possible that the VIN hadn’t been registered for the app. It could also be possible that one of the query string parameters in the first URL I shared above wasn’t valid for that VIN. For example, the RegionCode field may not have matched with the vehicle’s location. Without a positive result from the API, we couldn’t emphatically conclude that there was indeed a lack of authorisation.

The thing about VINs though is that they’re easily enumerable. Both Jan’s and the VIN found on the web were identical except for the last 5 digits which meant we could easily test for other matches using a tool like Burp suite. We proxied Chrome through Burp then issued the battery status request again:

We then sent it over to the Intruder feature and added one position for payload insertion:

This was the last five digits of the VIN, those being the ones which differed across both Jan’s and the number found online. (Note: not all LEAF VINs necessarily differ by just the last 5 digits, the VIN specification allows for the range to be broader, i.e. it may be the last 6 digits. Our test simply kept the range constrained between known numbers for the sake of time.) We then configured Burp to randomise those last 5 digits and choose integers between 10,000 and 30,000 which is the range both Jan’s and the VIN online fell within:

This gave us the ability to issue requests one after the other, each differing only by a unique VIN in the payload column. We didn’t need to test all 20,000 possible VINs within that range, we just had to issue requests until we found one that returned the battery status of another vehicle. We started Burp issuing the requests:

Request 0 in the screen above is the one to Jan’s car which returned a response size of 631 bytes. The subsequent responses with the randomised VINs mostly returned 288 bytes and the response you see in the screen above. Until we found one that didn’t:

This wasn’t Jan’s car; it was someone else’s LEAF. Our suspicion that the VIN was the only identifier required was confirmed and it became clear that there was a complete lack of auth on the service.

Of course it’s not just an issue related to retrieving vehicle status, remember the other APIs that can turn the climate control on or off. Anyone could potentially enumerate VINs and control the physical function of any vehicles that responded.That’s was a very serious issue. I reported it to Nissan the day after we discovered this (I wanted Jan to provide me with more information first), yet as of today – 32 days later – the issue remains unresolved. You can read the disclosure timeline further down but certainly there were many messages and a phone call over a period of more than four weeks and it’s only now that I’m disclosing publicly, right after I received an email from a Canadian follower…