Last time I reverse engineered how AirMatters controls air purifiers which are in the same local network. This time we will look into how AirMatters controls devices using the Philips cloud and how secure it is.
The network communication is over SSL, so the first step is to bypass the SSL verification in AirMatters.
The application loads its CA certificates from
/resources/assets and tries to establish a trust chain to them.
My favourite tool for sniffing SSL traffic is
mitmproxy, so I simply took
~/.mitmproxy/mitmproxy-ca-cert.cer from my machine and re-packaged AirMatters with it.
This is what I found after capturing the network traffic:
- Calling the cloud services requires logging with
- AirMatters is using hardcoded credentials to provision a new account and obtain
- Every request contains an
AuthorizationHTTP header which is verified by the server
- You need to pair the
client_idwith the purifier’s
device_idand then you can control it over the internet. The pairing is done by sending a special pairing request to the device and another pairing request to the corresponding cloud service.
The problem with this pairing mechanism is that it is completely silent and doesn’t require any physical interaction with the device. This means that an attacker needs one time network access to your device to pwn it forever. As far as I can tell, there is no way to “unpair” a device or to see how many accounts your device is paired with.
The only missing piece in the above is how the
Authorization HTTP header is calculated. This is how it looks like:
Authorization: CBAuth Type="SSO", Client="000000fff10af31f", RequestNr="2", Nonce="kj3PUx56Cpo3UdcIiICoPQ==", SSOToken="kcK0TgzC..", Authentication="XAR9WB443KTDT0A7pNUQZA=="
Nonce from the response of the login request. The
Authentication is 16 bytes base64 encoded value which is a calculated in
I used Ghidra to reverse engineer this native library and find how the authentication bytes are computed. This is how the relevant function looks like after being decompiled:
It does SHA1 hash of client_id, host, sso_key, the request number, nonce and then it takes the first 16 bytes of the digest:
And this is how I implemented it in Python:
m = hashlib.sha1() m.update(b'\x05') m.update(client_id.encode('ascii')) m.update(host.encode('ascii')) m.update(base64.b64decode(sso_key)) m.update(struct.pack('<I', request_num)) m.update(base64.b64decode(nonce)) key = m.digest()[:16] auth = base64.b64encode(key).decode('ascii')
Open source client
With this I put together
cloudctrl which can be used together with
airctrl to provision accounts, pair and control purifiers over the internet.
Check out the README of my project for more information.
After publishing my previous post, I have been contacted by Philips and they told me about their coordinated vulnerability disclosure process. So this time I coordinated my new findings with them before making it public. As a result they put me into their hall of honors :)