Máquina com SPA React que carrega a anon key do Supabase hardcoded no bundle JavaScript. Com a RLS desativada, a chave anônima lê todas as tabelas via PostgREST, incluindo uma credencial SSH em plaintext na tabela
access_logs. A escalada de privilégio sai decap_dac_overrideno binário dogdb, que permite escrever em/root/.ssh/authorized_keys.
Recon
Port scan
Começo com um scan de portas com o rustscan, deixando o -- -sV repassar o fingerprint de serviços ao nmap. Além do SSH e do Apache na 80, o alvo expõe o PostgreSQL na 5432, um pooler na 6543 e o gateway Kong nas 8000 e 8443. Kong na frente do PostgreSQL é a assinatura de um Supabase self-hosted.
rustscan -a anomaly.hc -- -sV
The application
A porta 80 serve a RHTech, uma plataforma de RH escrita como SPA React. A landing tem uma tela de login corporativo e nada mais acessível sem autenticação.
Sem credenciais, o caminho é o frontend. O index.html é só um shell que carrega o bundle principal da aplicação em /assets/index-DAiJsT9Q.js, e é nesse bundle que mora a lógica de comunicação com o backend.
Foothold
JS bundle: hardcoded Supabase anon key
Toda SPA que fala com Supabase precisa instanciar o cliente em algum ponto do código, e esse cliente carrega a URL do projeto e a anon key. Como o bundle roda no navegador, as duas coisas chegam ao cliente. Baixo o bundle e procuro a chamada de inicialização: a URL http://anomaly.hc:8000 e a anon key (um JWT) estão fixas no código. Decodificando o JWT, o role é anon e o exp cai em 2123, ou seja, uma chave de longuíssima duração.
curl -s http://anomaly.hc/assets/index-DAiJsT9Q.js | grep -oE 'anomaly.hc:8000.{1,4}eyJ[A-Za-z0-9_.-]+'
cut -d. -f2 loot/.jwt | base64 -d | jq .
A anon key não é um segredo por si só. Ela só vira problema quando a Row Level Security está desligada, porque é exatamente a RLS que deveria limitar o que o papel anon consegue ler.
RLS disabled: dump via PostgREST
O PostgREST expõe um documento OpenAPI na raiz de /rest/v1/, e a seção definitions lista todas as tabelas que o papel atual enxerga. Com a anon key, vejo quatro tabelas, entre elas settings e access_logs. Uma anon key conseguir enumerar e ler tabelas internas é o sinal claro de que a RLS não está aplicada.
curl -s "http://anomaly.hc:8000/rest/v1/" -H "apikey: $(cat loot/.jwt)" | jq ".definitions | keys"
Levo a anon key para o Burp e consulto a tabela settings pelo Repeater. A resposta entrega a configuração da plataforma, incluindo o endereço do servidor de backup, o usuário desse backup (rhbackup) e o usuário administrativo do banco (supabase_admin).
access_logs: SSH credential in plaintext
A tabela access_logs registra ações da plataforma com um campo details em JSON. Filtrando pelas entradas SSH_BACKUP, uma delas guardou o comando completo de um backup automatizado por cron, com a senha do rhbackup embutida na própria linha do sshpass.
A credencial foi gravada apontando para o servidor de backup, mas o mesmo par usuário e senha autentica no host da própria aplicação. Reuso a senha no SSH de anomaly.hc e caio numa sessão como rhbackup.
Com a sessão aberta, a flag de usuário está no home do rhbackup.
ls -la ~/user.txt
Privilege Escalation
Enumeration: GDB with cap_dac_override
Capabilities são uma forma de dar a um binário um pedaço específico do poder do root sem o SUID inteiro, e é o tipo de coisa que vale procurar logo no início. Listando as capabilities do sistema, o gdb carrega cap_dac_override.
getcap -r / 2>/dev/null | grep -v snap
A cap_dac_override faz o processo ignorar as checagens de permissão de leitura, escrita e execução em qualquer arquivo, independente de dono ou modo. Num binário que embute um interpretador completo como o gdb (que tem Python integrado), isso vira uma primitiva de escrita arbitrária em todo o sistema de arquivos.
Exploit: writing to root’s authorized_keys
Em vez de tentar editar arquivos sensíveis na mão, deixo o Python do gdb fazer a escrita herdando a capability. Gero um par de chaves na minha máquina e uso o gdb para anexar a chave pública ao authorized_keys do root. O cap_dac_override cobre tanto a travessia de /root (modo 700) quanto a escrita no arquivo.
ssh-keygen -t ed25519 -f anomaly_root -N ''
gdb -batch -ex "python open('/root/.ssh/authorized_keys','a').write('<MINHA_CHAVE_PUBLICA>\n')"
Com a chave plantada, autentico por SSH como root usando a chave privada correspondente.
A flag de root fecha a máquina.
ls -la /root/r0Ot.txt
Lessons Learned
- A anon key do Supabase é pública por definição (vive no frontend). A barreira de segurança é a Row Level Security, e desligá-la transforma a chave anônima numa chave de leitura total do banco.
- Dados operacionais nunca deveriam guardar credenciais. Logar a linha de comando inteira do backup gravou a senha do
rhbackupem texto puro numa tabela legível pela anon key. - Reúso de credencial entre serviços (a senha do “backup server” valendo no host da aplicação) encurta a cadeia: um segredo destinado a uma máquina abriu outra.
cap_dac_overridenum binário com interpretador embutido (gdb, e seu Python) é equivalente a escrita arbitrária como root. Capabilities precisam ser tão auditadas quanto binários SUID.
Impact
- A RHTech expõe dados pessoais de candidatos e funcionários: qualquer um com a anon key pública lê o banco inteiro de RH, um vazamento sujeito à LGPD.
- Com root no host, o atacante controla aplicação, banco e os backups — perde-se a base confiável de recuperação, e conter o incidente exige reconstruir o servidor e rotacionar todas as credenciais de backup.