Prflxion is a logical exploit of a sanitization bug in WebRTC that leaks the local IPv4 to the web application.
Until 2019, Using WebRTC candidates for this kind of leak was well known and used by vendors to accurately target users behind a NAT.
In 2019, following public pressure, the Chromium Team decided to implement an obfuscation method using the mDNS Protocol.
In short, mDNS uses the ip multicast feature to resolve local hostnames in a LAN. To make sure it does not cross routers, its TTL is limited to 1.
The exploit manipulates the generated candidates to circumvent the IP obfuscation by leveraging a common OS kernel feature.
The vulnerability lies within the code in the function Connection::MaybeUpdateLocalCandidate.
This function is called whenever a STUN_BINDING_RESPONSE
is received on a STUN
connection.
The function gets the XOR_MAPPED_ADDRESS
sent by the STUN
server - which is the client's IP address as seen by the server when receiving the bind request.
If the address is found within the current candidates, it changes the connection's local candidate to the one it found. Otherwise, it creates a new local candidate (one that has not been generated or sent by the STUN
) and assumes it to be a prflx
candidate.
The first problem is that the new candidate isn't Sanitized by the mDNS Obfuscation.
This sanitization is done mainly in BasicPortAllocatorSession::OnCandidateReady
when SignalCandidateReady is called.
But the function MaybeUpdateLocalCandidate
does not signal this. Instead, it calls AddPrflxCandidate which adds it without sanitization to the candidate list.
At first sight there seems to be no trivial way to exploit this. i.e. to create an un-sanitized candidate with the local IPv4 address. To achieve that we would have to somehow represent the local IPv4 in a way that is
on one hand different from candidate addresses created by the interface enumeration, and on the other hand needs to be supported by the network stack.
To aid us in this task comes IPv6.
As can be seen here,
there and everywhere,
when a client tries to connect in IPv4 to a server with dual-stack enabled that listens to an IPv6 (AF_INET6
)
defined socket on in6addr_any
(::
), a padding of the IPv4 address to an IPv6 address takes place in the kernel.
this padded IPv6 address is returned from getaddrinfo()
api (or recvfrom
in our case) as the remote host.
for example, assume the following:
- A Client with only IPv4 stack with ip
192.168.1.24
. - A dual-stack enabled Server with IPv4 address
192.168.1.25
and IPv6 address2002:a00:3::1006
that is listening on anAF_INET6
, UDP (SOCK_DGRAM
) socket. Bound to address::
and port 1337.
When the client connects to 192.168.1.25:1337
, the Server's Kernel pads the IPv4 of the client
because the server is expecting an IPv6 source IP when recvfrom
is called.
Thus, the Server will see the client as ::ffff:192.168.1.24
.
This is the address which will be returned as the XOR_MAPPED_ADDRESS
in the following exploitation.
So, to exploit this we would need to initiate a STUN_BIND_REQUEST
that will create a STUN_BIND_RESPONSE
that has the 4in6 local IPv4 as XOR_MAPPED_ADDRESS
.
First, we don't define any STUN
server so the STUN_BIND_REQUEST
s will be sent directly to the other peer without translation.
Fortunately, the WebRTC NetworkManagerBase
class binds two UDP sockets - one on INADDR_ANY
(0.0.0.0
), and another on in6addr_any
(::
). These Sockets are used for the STUN
negotiation.
So, when we create a RTCPeerConnection
instance, we would expect that two mDNS candidates will be created (and obfuscated) - one for the IPv4 address and another for the IPv6 address (each with its own UDP port)
These two candidates would have two different mDNS names (as per creation).
After the two candidates are collected, the exploit replaces the IPv6 candidate mDNS hostname (in JS) with the IPv4 candidate mDNS hostname. A second local connection is passed the malicious IPv6 candidate through RTCPeerConnection.addIceCandidate()
.
This results in a local mDNS name resolving of the IPv4 address, but the port that the second connection will try to connect with is the IPv6 defined socket, which means the address reported between the peers through STUN
will be a 4in6 translated address.
This results in adding un-sanitized candidate to the WebRTC stats which are available via the RTCPeerConnection.getStats()
method.