
Signal-Server v20261007.0.0 - High - Remote - Privilege leftover - unauth Dropwizard admin :8081
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · signal-server-dropwizard-admin
Class: Privilege leftover Reach: Remote
Signal-Server v20261007.0.0 (a15d5cd) - Signal Messenger, LLC
Dropwizard 5.0.2 DefaultServerFactory ships an HTTP admin connector on port 8081 with bindHost unset (all interfaces) and no admin authentication. Signal-Server never overrides that. service/config/sample.yml has no server: block. A YAML that only lists TLS applicationConnectors still inherits plaintext 8081 on 0.0.0.0. server plus the three ServerCommand workers start it. ExternalRequestFilter is on the application context only.
Unauth HTTP to :8081 is the operational admin plane. Not RCE. Not account takeover. Not sealed-message read.
| ID | no CVE yet |
| Class | Privilege leftover (unauthenticated Dropwizard admin) |
| Reach | Remote (all-interfaces :8081, no auth) |
| CWE | CWE-306, CWE-1188 |
| CVSS | High: 7.3 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L |
| Product | Signal-Server |
| Affected | v20261007.0.0 (a15d5cd) Dropwizard 5.0.2. In-tree sample / omitted adminConnectors. |
| Auth | none on the admin connector |
| License | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1 only |
Anyone who can open TCP to the admin port (default 0.0.0.0:8081, no extra config, no Authorization header) gets every stock Dropwizard 5 admin servlet plus Signal's extra task. That is the full privilege set. Nothing in this list is RCE, ATO, or a read of sealed message bodies.
| Method | Path | Privilege |
|---|---|---|
| GET | /ping | Confirm the admin listener is up. Body is pong. |
| GET | /metrics | Read the full Codahale registry as JSON: JVM heap and non-heap, GC counts, thread counts, Jetty connector meters, 4xx/5xx gauges, timers. Use this to fingerprint the process, estimate load, and watch the effect of later tasks. |
| GET | /threads | Full JVM thread dump: every thread name, id, state, and stack frames. In the lab this showed dw-admin-* plus java.base nio frames. On a live Signal node the same dump can include class names, logger names, and any string that happens to sit on a stack (file paths, hostnames, in-flight argument toString). It is not a heap dump and it is not a message-queue dump. |
| GET | /healthcheck | Run registered health checks. This tree never registers extras, so the body is the default deadlock check (deadlocks.healthy). A 200 vs 500 tells you whether the JVM thinks it is deadlocked. |
| GET | /tasks | List admin tasks. On this pin: gc, log-level, set-request-logging-enabled. |
| Method | Path | Privilege |
|---|---|---|
| POST | /tasks/log-level?logger=<name>&level=<LEVEL> | Set any Logback logger to TRACE, DEBUG, INFO, WARN, ERROR, or OFF. TRACE can make application loggers print more (including values a developer logged). OFF on a security or access logger goes dark. The attacker does not receive the log stream over this HTTP. They change what the process writes to disk or stdout. Lab oracle: logger name SIGNAL-SERVER-DROPWIZARD-ADMIN-WITNESS to TRACE. |
| POST | /tasks/gc | Call System.gc(). Response is Running GC... / Done!. That is a stop-the-world nudge, not process death. |
| POST | /tasks/set-request-logging-enabled?enabled=true | Signal-only task registered in WhisperServerService.run. Enables the request access-log filter (RequestLogManager). After that, HTTP to the application connector can write method, URI, and headers (including Authorization Basic) to the access log on disk. The admin client does not get those lines back. On message-persister-service / APN / idle-job workers the application port has no Signal Jersey resources, so this task matters on server. enabled=false turns it off again. |
ProcessBuilder, no Runtime.exec.GET /heapdump is 404.sample.yml or to bind addresses.A sibling container on the same Docker network reached http://vuln:8081/ping (200 pong) and could not reach http://patched:8081/ping when bindHost was 127.0.0.1. That is the Remote half.
I pinned signalapp/Signal-Server v20261007.0.0. Eighteen source hunts. Chat, keys, messages, backups, donations, calling, MFA, gRPC omnibus, and outbound HTTP came back empty at Critical/High. The workers/admin hunt did not. Dropwizard 5.0.2 HttpConnectorFactory.admin() still uses port 8081 and a null bind host. Signal never sets adminConnectors. Caddy's default admin is localhost. This one is not.
Wrong turn: trying to boot full Signal-Server. sample.yml passes check and still needs FoundationDB, Dynamo, Redis, and a secrets bundle to run. The leftover is the framework connector that ServerCommand starts before any of that. The lab is Dropwizard 5.0.2 with the same empty server: YAML and Signal's extra task name.
cd lab
./run.sh
Loopback 127.0.0.1:19010. Compose project signal-server-dropwizard-admin. Witness SIGNAL-SERVER-DROPWIZARD-ADMIN-WITNESS. Gadget YAML has no server: block. Negative YAML sets bindHost: 127.0.0.1. Last line:
SUCCESS signal-server-dropwizard-admin how=unauth-admin-8081 ping=200 metrics=200 threads=200 healthcheck=200 tasks=200 log-level=TRACE gc=200 set-log=enabled bind=* remote-vuln=200 remote-patched=fail SIGNAL-SERVER-DROPWIZARD-ADMIN-WITNESS
Bind admin to 127.0.0.1, set adminConnectors: [], or put authentication / mTLS on the admin context. Do not ship a YAML that only lists application connectors and assume 8081 stayed off.
Abraxas Labs - abraxaslabs.tech - github.com/abraxas - @abraxas_null - [email protected]