Category: Security Research

  • Reverse Engineering Hozelock Cloud Controller

    I recently bought the Hozelock Cloud Controller to automate watering in my garden. While trying to integrate it with OpenHAB, I discovered a number of useful and slightly concerning things about the REST API used by the service.

    This post reflects what I found in 2019, so it should be read as a snapshot of the product and API behaviour at the time rather than a current assessment.

    Product page: https://www.hozelock.com/product/cloud-controller/

    The code I wrote at the time can be found here: https://github.com/martynjsimpson/HozelockAPI

    Overview

    The Cloud Controller is made up of two parts: the hub, which connects to your router/LAN, and the controller, which connects to the hub and is attached to the outdoor tap.

    Discovery

    By using the Hozelock cloud support page and entering a valid Hub ID, you can retrieve the status of the hub and controller. Inspecting the browser traffic showed a GET request to the following endpoint, returning a JSON array:

    GET http://hoz3.com/restful/support/hubs/{hubId}

    The first thing I noticed was that there appeared to be no authentication on the endpoint, you only needed a valid Hub ID.

    If you make a direct call using an invalid Hub ID, the endpoint is kind enough to tell you the expected regular expression for a valid ID.

    GET http://hoz3.com/restful/support/hubs/xxxxxx
    
    {
        "errorCode": 134611463,
        "httpStatus": 400,
        "errorMessage": "GET request to '/restful/support/hubs/xxxxxx' failed due to invalid parameter 'hubID': Expected a valid Hub ID matching [0-9A-Z]{6}",
        "cause": {
            "method": "GET",
            "url": "/restful/support/hubs/xxxxxx",
            "contentType": null
        }
    }

    If anybody wants to do the maths on the total number of possible permutations, I would still be interested.

    It is also worth noting that this all worked over HTTP. HTTPS was supported, but it was not forced and HTTP did not appear to redirect. Throughout this post I may mix HTTP and HTTPS, but the point is that both appeared possible unless stated otherwise.

    From there, I started looking at what else the API supported.

    Hub

    OPTIONS https://hoz3.com/restful/support/hubs/{hubId}
    
    {
        "errorCode": 0,
        "allowedMethods": [
            "GET",
            "OPTIONS"
        ]
    }

    There is nothing especially exciting here from an action point of view, but the data returned for the hub can include potentially sensitive information such as location if the user has configured it. From memory, this appeared to include things like city, country, local time, and time zone.

    Next, I worked through the rest of the JSON returned by GET /{hubId}.

    Schedules

    GET https://hoz3.com/restful/support/hubs/{hubId}/schedules

    This returns an array of all schedules for the hub. Note that this does not necessarily mean the schedule is currently applied to the controller.

    In the sample I captured, the watering was configured for sunrise and sunset, which meant two entries per day.

    Timestamps appeared to use Unix epoch milliseconds.
    -2000 appeared to mean sunrise and -1000 appeared to mean sunset (or an offset from them). Duration was also expressed in milliseconds.

    [
        {
            "scheduleID": "{scheduleId}",
            "name": "{scheduleName}",
            "description": null,
            "scheduleDays": {
                "Monday": {
                    "dayOfWeek": "Monday",
                    "wateringEvents": [
                        {
                            "startTime": -2000,
                            "endTime": 898000,
                            "duration": 900000,
                            "enabled": true
                        },
                        {
                            "startTime": -1000,
                            "endTime": 899000,
                            "duration": 900000,
                            "enabled": true
                        }
                    ]
                }
            }
        }
    ]

    You can also retrieve a specific schedule by appending the scheduleId to the URL.

    GET https://hoz3.com/restful/support/hubs/{hubId}/schedules/{scheduleId}

    Looking at what was supported at the schedules endpoint:

    OPTIONS https://hoz3.com/restful/support/hubs/{hubId}/schedules/
    
    {
        "errorCode": 0,
        "allowedMethods": [
            "GET",
            "HEAD",
            "OPTIONS"
        ]
    }

    Not much of use there, although HEAD could still be interesting.

    However, the specific schedule endpoint was more revealing.

    OPTIONS https://hoz3.com/restful/support/hubs/{hubId}/schedules/{scheduleId}
    
    {
        "errorCode": 0,
        "allowedMethods": [
            "DELETE",
            "GET",
            "HEAD",
            "PATCH",
            "POST",
            "PUT",
            "OPTIONS"
        ]
    }

    This was far more interesting, as POST, PUT, and PATCH appeared to allow schedule creation, replacement/upsert, and updates. Even DELETE was available.

    At the time I was able to adjust a schedule using PATCH by changing the name, but I never fully worked out the complete payloads for creating, updating, or deleting schedules.

    Controllers

    One or more controllers can be associated with a single hub, for example, one for the front garden and one for the rear.

    GET https://hoz3.com/restful/support/hubs/{hubId}/controllers
    
    {
        "controllers": [
            {
                "name": "{controllerName}",
                "image": null,
                "controllerID": "0",
                "scheduleID": "{scheduleId}",
                "schedule": {
                    "scheduleID": "{scheduleId}",
                    "name": "{scheduleName}"
                },
                "hasWaterNowEvent": false,
                "pause": null,
                "adjustment": null,
                "waterNowEvent": null,
                "currentWateringEvent": null,
                "nextWateringEvent": {
                    "startTime": 1556219700000,
                    "endTime": 1556220600000,
                    "duration": 900000,
                    "enabled": true
                },
                "lastCommunicationWithServer": 1556211067000,
                "nextCommunicationWithServer": 1556212140000,
                "batteryStatus": "OK",
                "signalStrength": "GOOD",
                "overrideScheduleDuration": null,
                "isChildlockEnabled": false,
                "isWatering": false,
                "isPanelRemoved": false,
                "isTested": true,
                "isAdjusted": false,
                "isScheduleUpToDate": true,
                "isPaused": false
            }
        ],
        "inPairingMode": false,
        "lastServerContactDate": 1556211067000,
        "hubResetRequired": false,
        "controllerResetRequired": false,
        "isUresponsive": false
    }

    Again, this returned an array of all controllers, and you could address a specific controller by appending the controllerId to the URL:

    GET https://hoz3.com/restful/support/hubs/{hubId}/controllers/{controllerId}

    Both the controllers endpoint and the specific controller endpoint appeared to support only GET, HEAD, and OPTIONS.

    OPTIONS https://hoz3.com/restful/support/hubs/{hubId}/controllers/{controllerId}
    OR
    OPTIONS https://hoz3.com/restful/support/hubs/{hubId}/controllers
    
    {
        "errorCode": 0,
        "allowedMethods": [
            "GET",
            "HEAD",
            "OPTIONS"
        ]
    }

    Actions

    This section is largely credited to anthonyangel (see Credits below).

    There appeared to be an undocumented part of the API called Actions, which allowed tasks to be submitted for the hub, and therefore the controller, to pick up.

    GET http://hoz3.com/restful/support/hubs/{hubId}/controllers/actions/
    
    {
        "errorCode": 0,
        "actions": [
            "pause",
            "unpause",
            "adjust",
            "unadjust",
            "waterNow",
            "stopWatering",
            "setMode",
            "ping"
        ]
    }

    Using the request structures worked out by anthonyangel, it was possible to send a POST with a body to issue a waterNow command.

    POST http://hoz3.com/restful/support/hubs/{hubId}/controllers/actions/waterNow
    
    Request Body
    {
        "controllerIDs": [{controllerId}],
        "duration": 300000
    }
    
    Response
    {
        "errorCode": 0
    }

    Sure enough, after the controller’s polling delay, up to around 20 minutes, the phone app reported that watering had started.

    Available actions

    • pause — pauses watering for a number of days
    • unpause — removes a pause
    • adjust — appears to increase watering duration by a percentage for a period of time
    • unadjust — removes the adjustment
    • waterNow — starts a watering session
    • stopWatering — stops watering
    • setMode — seemed to expect a mode parameter, though I did not fully decode it
    • ping — despite being listed, it did not appear to be implemented when I tested it

    One useful pattern here was that if you built a payload with only controllerIDs, the error responses often told you what additional data was missing. Having the mobile app to hand also helped when trying to infer how API calls mapped to UI actions.

    Conclusions

    As is often the case with newer IoT products, security seemed to have been treated as secondary. Admittedly, the impact of somebody tampering with a garden watering system is not exactly catastrophic, perhaps they waste some water or kill a few plants, but it still reflects poor design choices.

    That said, the controller → hub → cloud model appeared to limit the risk of this leading directly to compromise of the wider LAN.

    Basic measures such as forcing HTTPS appeared to have been missed. Enabling and enforcing TLS at the service edge is hardly unusual, although I accept there may have been implementation constraints on the hub side that were not obvious from outside.

    Authentication on the endpoints, even for GET operations, should have been a minimum expectation. Relying on obscurity is not a sensible control. Others had also reported an apparent lack of rate limiting, which meant brute-forcing Hub IDs looked technically feasible.

    At the time, I also wondered whether companies like Hozelock should provide proper developer portals, API documentation, and supported integration mechanisms. That would not remove the need for real security controls, but it would at least reduce the incentive for enthusiasts to reverse engineer the platform just to make it work with their home automation stack.

    Overall, I really liked the product. I am terrible at remembering to water the garden, and this solved a genuine problem for me. Initial setup was a bit painful, I suspect because of previous pairing attempts, but a full factory reset of the hub and controller fixed it.

    More broadly, it felt like another example of an IoT product reaching market before security had been given the attention it deserved. Companies building connected devices need to treat security design with the same seriousness they would apply to their internal applications and services.

    At the time, I looked for somewhere to report these issues but could not find an obvious route.

    Credits

    The original inspiration for digging into this came from the Home Assistant community, particularly a post by anthonyangel:

    https://community.home-assistant.io/t/having-hozelock-cloud-controller-kit-intergration/55694/3

    He appears to have reported the lack of security around July 2018, and it did not look like much came of it.