# Port Shadows via Network Alchemy:(CVE-2021-3773)

Credit: Wikipedia

OpenVPN’s use of Netfilter makes it susceptible to several attacks that can cause denial-of-service, deanonymization of clients, or redirection of a victim client connection to an attacker controlled server. Netfilter is a module within the Linux kernel that implements stateless and stateful firewall mechanisms and network address translation (NAT). Netfilter’s design includes hooks that are called at various points in the networking code to execute, e.g., user-defined firewall rules and NAT code. The NAT portion of Netfilter is designed in such a way that if a machine behind the NAT uses the same source port as an application listening on the same port as the NAT (i.e., the NAT is acting both as a NAT-router and a server), then Netfilter translates and routes received packets intended for the NAT’s own listening port to a host behind the NAT using the same port. This shadowing behavior is not specified in any relevant request for comments (rfc768, rfc793, rfc4787, rfc5382, rfc7857, or any of their successors). The remainder of this disclosure uses the term “port shadow(ing)” when discussing this attack primitive.

Port shadowing’s root cause originates from Netfilter’s lack of coordination with the Linux socket infrastructure to determine whether a port in a listening state (or any particular state) on the NAT creates ambiguity with a machine using the same port behind the NAT. Port shadowing has interesting implications for applications, such as OpenVPN, that rely on Netfilter for NAT. A malicious OpenVPN client can use port shadowing to deanonymize victim machines connected to the same OpenVPN server or escalate privileges from an OpenVPN client to a man-in-the-middle (c2mitm) between another client (the victim) and the OpenVPN server to which both the attacker and victim are connected. The c2mitm variation of the attack can be combined with a recently disclosed, server-side attack against OpenVPN to inject DNS responses, reset, or even hijack TCP connections of the victim, even when that connection is tunneled through the OpenVPN server.

Version information is at the bottom of this note. While we have not yet tested other versions of OpenVPN, any version of Wireguard or strongSwan, or any other VPN server implementations, we believe our attack applies to any version of any VPN that relies on Linux’s Netfilter for NAT. We have
successfully tested the full FreeBSD-13 with natd and OpenVPN. Additionally, we have tested against FreeBSD-13 with IPFW, PF, and IPF and found that, while they are not succesptable to c2mitm, the port shadow still applies. There is no specific implemetation bug with Netfilter, nor presumably FreeBSD’s NAT implementations, that makes port shadowing possible, it is simply an implementation artifact that is apropos of following the RFCs that relate to NAT.

The port shadow attack primitive is not specific to OpenVPN and is fundamentally related to network address translation. OpenVPN is tightly coupled with the concept of NAT and depends on Netfilter for NAT support, hence, the following discussion about OpenVPN focuses on its role as a NAT since our attack arises from NAT related implementation details. Cryptography is not described in any detail since our attack does not target any cryptographic components. The following background subsections provide information on relevant threat models, OpenVPN, Netfilter, and the port shadow primitive.

The two threat models most relevant to this disclosure are adjacent and in-path attackers. Building a port shadow assumes a logically adjacent attacker and the DNS injection, the coup de grace covered at the end of this post, requires an in-path attacker.

MITRE defines a logically adjacent attacker as follows under the attack vector description MITRE:

The vulnerable component is bound to the network stack, but the attack is limited at the protocol level to a logically adjacent topology. This can mean an attack must be launched from the same shared physical (e.g., Bluetooth or IEEE 802.11) or logical (e.g., local IP subnet) network, or from within a secure or otherwise limited administrative domain (e.g., MPLS, secure VPN to an administrative network zone). One example of an Adjacent attack would be an ARP (IPv4) or neighbor discovery (IPv6) flood leading to a denial of service on the local LAN segment (e.g., CVE‑2013‑6014).


|OpenVPN Server(NAT/router)|
/          \
.           .
/             \
|routerA|         |routerV|
|               |
.               .
/                 \
|attacker|             |victim|


It is important to note that logically adjacent and network adjacent are two different threat models. Our attacks do not make any assupmtions about the attacker being network adjacent. In the context of our attacks, for practical purposes logically adjacent simply means the attacker is connected as a client to the same VPN server as the victim.

The in-path attacker is a machine that routes packets between two hosts. For this disclosure, the attacker is between the victim and the OpenVPN server. Readers seeking a formal definition of in-path threats or interesting in-path attacks are referred to Marczak et al.’s work on the Great Cannon.


|OpenVPN Server(NAT/router)|
|
.
|attacker|
|
.
|
|victim|


An attacker can leverage the port shadow primitive to escalate from logically adjacent to in-path against OpenVPN.

OpenVPN uses Netfilter to perform NAT for clients connected to the server. A non-exhaustive list of VPN use-cases is:

1. securing network traffic when using public WiFi at hotels, coffee shops or airports
2. hiding illegally torrented content
3. bypassing geo blocked content filters
4. bypassing restrictive firewalls

Regardless of the specific use-case, a client must first establish a VPN tunnel between herself and the OpenVPN server. During connection establishment, the victim sends an initial UDP or TCP packet to a listening port on the OpenVPN server (typically 1194, but is configurable and known by the client). The subsequent packets exchanged establish a secure channel using SSL/TLS and various configuration parameters, including the client’s virtual IP address, are pushed to the client.


(C)lient                                    OpenVPN (S)erver
|--         {src=C:Cport, dst=S:1194}         ->|             1. First packet Client sends to OpenVPN server
|<-   { Exchange parameters over SSL/TLS }    ->|             2. Server and Client establish tunnel parameters



Fig. 1. Diagram showing the first packet the client sends to the server and the subsequent packets exchanged to establish the client’s tunnel parameters.

Once the client has a virtual-IP address that the OpenVPN server associates with her and her routes are configured to send all originating packets through the tunnel, the client may exchange packets between other globally routable IP addresses and they will assume the traffic originated from the OpenVPN server instead of the client. Also, any in-path machines between the OpenVPN client and server will only see encrypted traffic.



|----------|     |------------------|
| Client A |~~~~~| In-path Machine |
|----------|     |------------------|
|             |------------|
. ~~~~~~~~~~~~| Web Server |
|             |------------|
|----------|     |----------------|
| Client B |~~~~~| OpenVPN Server |
|----------|     |----------------|



Fig. 2. An example Topology of an OpenVPN Server and two VPN clients. There is also an in-path router between Client A and the OpenVPN server. If Client A (or B or Both) request data from Web Server, Web Server will think the requests came from OpenVPN server, regardless of which client issued the request. In-path Machine will not know if Client A requests a page from Web Server.

A recently disclosed attack by Tolley et al. Tolley2021 demonstrates how an attacker can leak connection information or even hijack connections between an OpenVPN client and an end-server. Their attacks require that the attacker be in-path between the OpenVPN client and the OpenVPN server. While in-path capabilities are typically reserved for nation-states, ISP, or similarly powerful adversaries, using network alchemy, we are able to deanonymize an OpenVPN client or transmute a basic OpenVPN client into an in-path attacker.

We define network alchemy as the creation of states of network connections that should not exist, using a combination of TTL limiting, packet spoofing, RFC ambiguities, and other low-level network primitives.

Conntrack stores state about connections it sees in a single, global hash table called nf_conntrack_hash.

struct hlist_nulls_head *nf_conntrack_hash __read_mostly;
EXPORT_SYMBOL_GPL(nf_conntrack_hash);


Fig. 3. nf_conntrack_hash table declaration.

The table is shared by all protocols and packets, regardless of network namespaces or sockets. The table is initialized at module load time and has a maximum size that is a function of the amount of system memory. The size is also partially controlled by the system variable net.ipv4.netfilter.nf_conntrack_max. The fact that the table is global and does not coordinate with the socket infrastructure are the primary factors contributing to port-shadow behavior.

Individual connections are represented as entries in nf_conntrack_hash by the nf_conn struct.

struct nf_conn {
.
.
.
u32 timeout;
.
.
.
/* XXX should I move this to the tail ? - Y.K */
/* These are my tuples; original and reply */
struct nf_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];

/* Have we seen traffic both ways yet? (bitset) */
unsigned long status;
.
.
.



Fig. 4. nf_conn struct including the fields related to this disclosure.

The metadata inside each nf_conn entry includes a timeout value, a two-entry array of nf_conntrack_tuple_hash structs (one for the ORIGINAL direction and one for the REPLY direction), and a status bitfield. Each nf_conntrack_tuple_hash contains an nf_conntrack_tuple and each nf_conntrack_tuple contains its own copy of the packet’s direction, source and destination ports and IP addresses, and protocol number.

/* Connections have two entries in the hash table: one for each way */
struct nf_conntrack_tuple_hash {
struct hlist_nulls_node hnnode;
struct nf_conntrack_tuple tuple;
};


Fig. 5. nf_conntrack_tuple_hash declaration.


/* The manipulable part of the tuple. */
struct nf_conntrack_man {
union nf_conntrack_man_proto u;
/* Layer 3 protocol */
u_int16_t l3num;
};

/* This contains the information to distinguish a connection. */
struct nf_conntrack_tuple {
struct nf_conntrack_man src;

/* These are the parts of the tuple which are fixed. */
struct {
union {
/* Add other protocols here. */
__be16 all;

struct {
__be16 port;
} tcp;
struct {
__be16 port;
} udp;
struct {
u_int8_t type, code;
} icmp;
struct {
__be16 port;
} dccp;
struct {
__be16 port;
} sctp;
struct {
__be16 key;
} gre;
} u;

/* The protocol. */
u_int8_t protonum;

/* The direction (for tuplehash) */
u_int8_t dir;
} dst;
};



Fig. 6. nf_conntrack_tuple and nf_conntrack_man declarations. Notice that only source portion of the tuple is manipulatable.

When Conntrack processes a packet, it checks to see whether it is in nf_conntrack_hash. If the packet is part of an existing connection, Conntrack pulls the associated nf_conn entry for the remainder of processing, otherwise, a new nf_conn entry is created and inserted into nf_conntrack_hash. If the nf_conn entry is new, processing includes remapping colliding ports and is performed by get_unique_tuple in Netfilter’s NAT module. As we demonstrate in the Attack Methodology section, Netfitler’s port remapping (and, in special cases, lack thereof) make our attacks possible (port shadowing leverages this behavior). As the packet passes through Netfitler’s NAT hook, its source IP address and port may be overwritten, depending on whether or not ports collide.

The direction field represents whether the packet is from the connection originator or replier (i.e., ORIGINAL and REPLY). Conntrack infers the connection’s direction based on whether an nf_conn entry exists for the given source and destination IP addresses, protocol number, and port numbers in the case of UDP and TCP. When Conntrack sees a packet for which no nf_conn entry exists, it assumes that packet is the originator of the connection and assigns nf_conn.tuplehash[ORIGINAL] using the fields of the packet being processed. This idea of connection direction is based on the notion of “start of session”, and Conntrack’s use of ORIGINAL and REPLY directions are consistent with section 2.5. of rfc2663.

2.5. Start of session for TCP, UDP and others

The first packet of every TCP session tries to establish a session and contains connection startup information. The first packet of a TCP session may be recognized by the presence of SYN bit and absence of ACK bit in the TCP flags. All TCP packets, with the exception of the first packet, must have the ACK bit set.

However, there is no deterministic way of recognizing the start of a UDP based session or any non-TCP session. A heuristic approach would be to assume the first packet with hitherto non-existent session parameters (as defined in section 2.3) as constituting the start of new session.

As stated previously, Conntrack stores two copies of the source and destination IP addresses and ports, the protocol number, and the direction in each nf_conn entry in the tuplehash array. In the case where the machine running Conntrack is also the actual sender of packets in a session, the ORIGINAL direction’s source IP address is the machine running Conntrack, the source port is whatever Conntrack’s sending application chose, the destination IP address is some remote host’s IP address, and the destination port is whatever port the remote host is using to communicate with the Conntrack host. In the REPLY direction, the source IP and destination IP are switched and the destination is the Conntrack machine’s IP address (i.e., the source in the ORIGINAL direction). This detail is important because with OpenVPN, from Conntrack’s perspective, the ORIGINAL direction for tunnel initializations should always the OpenVPN client’s IP address. According to Netfilter, maintaining two entries is an optimization for faster lookups and translations. Fig. 7 below provides a generic example of what the nf_conntrack_hash and some nf_conn entry might look like.

nf_conntrack_hash {
H .-> nf_conn[.] -> {
timeout = T;
tuplehash = {
[ORIGINAL] .-> {src={u3=Conntrack Machine, udp.port=sport}, dst={u3=Remote Server, udp.port=dport}},
[REPLY]    .-> {src={u3=Remote Server, udp.port=dport}, dst={u3=Conntrack Machine, udp.port=sport}}
status = S;
};
}
}


Fig. 7. nf_conn entry structure for entries in the nf_conntrack_hash table where the NAT is the actually end-point in a connection session.

The following case the machine running Conntrack acts as a NAT. When a host behind the NAT (with a private IP) sends a packet out the NAT, the nf_conntrack_tuple[ORIGINAL] direction’s source IP address is the private IP address of the client sending the packet while the source of the REPLY direction is the NAT’s public IP address. Assuming the port of this packet does not collide with other ports NAT is translating, port numbers are swapped without being manipulated. This behavior is related to the c2mitm attack’s use of the port shadowing primitive. Fig. 7 provides a graphical representation of this situation.

nf_conntrack_hash {
H .-> nf_conn[.] -> {
timeout = T;
tuplehash = {
[ORIGINAL] .-> {src={u3=Machine Behind NAT, udp.port=sport}, dst={u3=Remote Server, udp.port=dport}},
[REPLY]    .-> {src={u3=Remote Server, udp.port=dport}, dst={u3=Conntrack Machine, udp.port=sport}}
};
status = S;
}
}


Fig. 7. nf_conn entry structure for NAT entries in the nf_conntrack_hash table.

If there are port collisions, then NAT will remap the ports, as depicted in the following figure.

nf_conntrack_hash {
H1 .-> nf_conn[.] -> {
timeout = T;
tuplehash = {
[ORIGINAL] .-> {src={u3=Remote Server, udp.port=Rport}, dst={u3=Conntrack Machine, udp.port=1194}},
[REPLY]    .-> {src={u3=Conntrack Machine, udp.port=1194}, dst={u3=Remote Server, udp.port=Rport}}
};
status = S;
}
H2 .-> nf_conn[.] -> {
timeout = T;
tuplehash = {
[ORIGINAL] .-> {src={u3=Machine Behind NAT, udp.port=1194}, dst={u3=Remote Server, udp.port=Rport}},
[REPLY]    .-> {src={u3=Remote Server, udp.port=NAT(Rport)}, dst={u3=Conntrack Machine, udp.port=1194}}
};
status = S;
}
}


Fig. 8. Depicts nf_conn structure for NAT entries in the nf_conntrack_hash table when there is a port collision.

Fig. 8., assume that H1 was in the table before H2. Notice that in the REPLY direction of the H2 entry that src.udp.port is a modified version of Rport, denoted by a function NAT that remaps that port. This is a simplified example of what happens when Netfilter’s NAT module resolves ports that collide in this way. The collision happens because H1 in the ORIGINAL direction and H2 in the REPLY direction would hash to the same entry and hence generate a non-unique tuple which cannot happen if NATing needs to function correctly. We found that a malicious machine connected to the OpenVPN server can shadow the OpenVPN server’s listening port (typically 1194) by sending packets with the same source port as the OpenVPN server’s listening port to arbitrary destination IP address and ports. Because these entries are consulted for NAT before most packet processing and because they are used to reroute packets, any packets matching an entry of this type bypasses any listening sockets (such as the one OpenVPN uses) it was intended for. We will show how an attacker can build a port shadow to deanonymize another OpenVPN client or perform a c2mitm attack against a victim in the Attack Methodology section.

One challenge the attacker faces is uncertainty about when a victim will connect to the OpenVPN server.

nf_conn entries are kept in the table until their timeout value expires. Conntrack’s garbage collector runs periodically and evicts entries whose timeout has expired or if the table is filled beyond 95\%. In the latter case, Conntrack can evict entries before their timeout has expired, but only if the entry’s status does not have the ASSURED bit set. As packets are exchanged and as Netfilter processes them, the timeout value and status get updated. An attacker can take advantage of this to place entries in the ASSURED state, and periodically send packets through Conntrack to ensure the timeout value never expires which is necessary for the c2mitm attack.

Placing an entry in the ASSURED state is protocol dependent and Conntrack requires that each protocol module maintain its state according to that protocol’s requirements. There is no RFC to dictate how this should or must be done, so the module authors make a best effort to determine when the status bits should be updated. In the case of UDP, the ASSURED bit is set whenever packets are exchanged in both directions.

/* Returns verdict for packet, and may modify conntracktype */
int nf_conntrack_udp_packet(struct nf_conn *ct,
struct sk_buff *skb,
unsigned int dataoff,
enum ip_conntrack_info ctinfo,
const struct nf_hook_state *state)
{
.
.
.

/* If we've seen traffic both ways, this is some kind of UDP
* stream. Set Assured.
*/
.
.
.
/* Also, more likely to be important, and not a probe */
if (!test_and_set_bit(IPS_ASSURED_BIT, &ct->status))
nf_conntrack_event_cache(IPCT_ASSURED, ct);
.
.
.
return NF_ACCEPT;
}


Fig. 9. ASSURED bit handling for UDP packets in Conntrack.

For TCP, Conntrack maintains a shadow TCP state machine of a TCP connection between two hosts whose packet’s the machine running Conntrack routes. This is accomplished using the tcp_conntrack state transition table, a function to determine whether the incoming packet should be routed based on Conntrack policy (nf_conntrack_tcp_packet), and the nf_conn.status field. The nf_conntrack_tcp_packet function contains a code-path that sets the ASSURED bit to 1.

/* Returns verdict for packet, or -1 for invalid. */
int nf_conntrack_tcp_packet(struct nf_conn *ct,
struct sk_buff *skb,
unsigned int dataoff,
enum ip_conntrack_info ctinfo,
const struct nf_hook_state *state)
{
.
.
.

} else if (!test_bit(IPS_ASSURED_BIT, &ct->status)
&& (old_state == TCP_CONNTRACK_SYN_RECV
|| old_state == TCP_CONNTRACK_ESTABLISHED)
&& new_state == TCP_CONNTRACK_ESTABLISHED) {
/* Set ASSURED if we see valid ack in ESTABLISHED
after SYN_RECV or a valid answer for a picked up
connection. */
set_bit(IPS_ASSURED_BIT, &ct->status);
nf_conntrack_event_cache(IPCT_ASSURED, ct);
}
.
.
.
return NF_ACCEPT;
}


Fig. 10. ASSURED bit handling for UDP packets in Conntrack.

Using this information, an attacker can keep her entries in the Conntrack table long enough to perform the c2mitm attack.

In addition to modifying the status bits of the nf_conn entry, Netfilter’s TCP state machine makes forwarding decisions based on the TCP header and Netfilter’s view of the TCP state between the two TCP end-points. This has implications for which TCP packets can cross the NAT-external/ NAT-internal address realms. TCP simultaneous open in particular causes security issues for OpenVPN clients because an attacker can leverage Netfilter’s behavior when processing SYNs to trick the victim into connecting to some TCP service through the VPN server, back to the client. In the Attack Methodology section, we demonstrate how an attacker can combine c2mitm and DNS injection with simultaneous open to strip the OpenVPN’s tunnel encryption and establish a TCP session with the victim.

In summary, we have described Netfilter’s stateful Connection tracking and network address translation implementation (Conntrack and NAT) as they relate to port shadowing. We described how entries are created, stored, used and destroyed by Conntrack. Next, we provide a description of how the attacker builds the port shadowing exploit primitive.

We identified an exploit primitive, port shadowing, that an attacker can use to either deanonymize a client connected to an OpenVPN server or perform a privilege escalation from client to man-in-the-middle. The attacks are related to each other and differ mainly in the context of their use of the exploit primitive as well as whether a client connects to the OpenVPN server before or after the attacker.

The attacker builds the exploit primitive in two steps. The process is the same for both attacks.

1. Connect to the OpenVPN server.
2. Send packets to a victim’s candidate, public IP address using the OpenVPN server’s listening port as the source port to all destination ports in the IANA ephemeral port range.

After step (2.) the OpenVPN server’s nf_conntrack_hash contains 16383 entries. Each entry is capable of routing packets matching the tuples in nf_conntrack_hash back to the attacker’s OpenVPN tunnel interface. We describe in the Attack Methodology section how an attacker uses this to deanonymize the victim or become a man-in-the-middle between the victim and the OpenVPN server.

We now describe our testing environment, the deanonymization and c2mitm attacks, and a third attack that demonstrates that network alchemy can be applied to TCP in addition to UDP connections.

We tested the attacks in a virtual environment and on the real Internet. Both environments ran OpenVPN server version 2.4.4 on an Ubuntu 18.04 headless server running Linux kernel version 4.15.0-142. We packaged our virtual environment as a vagrant script and include the required software dependencies and code to reproduce our attack (to the best of our abilities though some assembly may be required). Our live attack was performed against a server we setup and configured ourselves. We deployed our OpenVPN server on a machine with 2048 MB RAM, 1 core, and 55 GB storage using vultr cloud services. We manually installed OpenVPN 2.4.4 on the Ubuntu 18.04 vultr instance using Digital Oceans instructions. Our use of our own server for testing was to eliminate any interference our attacks may create with production clients using commercial or consumer VPN providers.

Assumptions

The deanonymization attack makes the following assumption:

1. The Victim (V) connects to the OpenVPN server (S) before the Attacker (A) carries out any attempt to see if they are connected. This is somewhat obvious, but we specify it as an assumption to make the exact nature of the attack clear. In this example, we assume the VPN uses UDP, however, these attacks are also possible using TCP.
nf_conntrack_hash {
H1 .-> nf_conn[.] -> {
timeout = 179; // seconds
tuplehash = {
[ORIGINAL] .-> {src={u3=V, udp.port=Vport}, dst={u3=S, udp.port=1194}},
[REPLY]    .-> {src={u3=S, udp.port=1194}, dst={u3=V, udp.port=Vport}}
};
status = {ASSURED};
},
}


Fig. 11. S’s Conntrack table with V’s legitimate tunnel entry.

1. A has two abilities: their own credentials to connect to the same OpenVPN server as V, and the ability to spoof packets on the Internet. A may or may not use two different computers for the attack.

Execution

The deanonymization attack is executed in four steps as follows:

1. A connects to S

 nf_conntrack_hash {
H1 .-> nf_conn[.] -> {
timeout = 179; // seconds
tuplehash = {
[ORIGINAL] .-> {src={u3=V, udp.port=Vport}, dst={u3=S, udp.port=1194}},
[REPLY]    .-> {src={u3=S, udp.port=1194}, dst={u3=V, udp.port=Vport}}
};
status = {ASSURED};
},
H2 .-> nf_conn[.] -> {
timeout = 179; // seconds
tuplehash = {
[ORIGINAL] .-> {src={u3=A, udp.port=Aport}, dst={u3=S, udp.port=1194}},
[REPLY]    .-> {src={u3=S, udp.port=1194}, dst={u3=A, udp.port=Aport}}
};
status = {ASSURED};
},
}


Fig. 12. S’s Conntrack table with V and A’s legitimate tunnel entries.

2. A sends UDP packets from herself to a candidate IP address of V. Each packet has the source port equal to whatever the OpenVPN server’s listening port is (assume 1194), a unique destination port in the IANA ephemeral port range, and is TTL limited such that the UDP packets do not reach the candidate IP.

The attacker must cycle through the victim’s ephemeral port space only once. This requires 16383 packets per candidate victim IP (assuming the operating system uses the IANA dynamic port range, some operating systems the victim may be using have a larger range). The attacker can check candidate victim IPs in parallel and send at any reasonable speed, and note also that the attacker does not need to check every one of the roughly 4 billion possible IPv4 addresses if they have a profile of the victim, such as what city they connect to the VPN from. We spoof responses in subsequent steps, which imposes a 180 second timeout (for the conntrack entry to be garbage collected) before we can check again if this particular candidate victim IP has connected.

 nf_conntrack_hash {
H1 .-> nf_conn[.] -> {
timeout = 179; // seconds
tuplehash = {
[ORIGINAL] .-> {src={u3=V, udp.port=Vport}, dst={u3=S, udp.port=1194}},
[REPLY]    .-> {src={u3=S, udp.port=1194}, dst={u3=V, udp.port=Vport}}
};
status = {ASSURED};
},
H2 .-> nf_conn[.] -> {
timeout = 179; // seconds
tuplehash = {
[ORIGINAL] .-> {src={u3=A, udp.port=Aport}, dst={u3=S, udp.port=1194}},
[REPLY]    .-> {src={u3=S, udp.port=1194}, dst={u3=A, udp.port=Aport}}
};
status = {ASSURED};
},
.
.
.
Hn .-> nf_conn[.] -> {
timeout = 179; // seconds
tuplehash = {
[ORIGINAL] .-> {src={u3=tun0A, port=1194}, dst={u3=V, udp.port=Vport}},
[REPLY]    .-> {src={u3=V, port=NAT(Vport)}, dst={u3=S, udp.port=1194}}
};
status = {ASSURED};
},
}


Fig. 13. S’s Conntrack table with V and A’s legitimate tunnel entry and A’s entry whose destination port in the ORIGINAL direction collides with V’s legitimate Conntrack entry’s source port in the Conntrack table. The entries are denoted by entries H1, H2, and Hn, respectively.

3. From a seperate machine, A spoofs UDP responses for each of the UDP packets sent in (2.) from the candidate IP to S.

4. A’s machine connected to S collects the responses sent in (3.) routed back to her by S.

If A receives responses for every ephemeral port sent out in step (2.), then she knows the candidate IP is not connected to the same OpenVPN server (S) as her. If A observes one missing ephemeral port response, then she knows the candidate IP is V and is connected to the same OpenVPN server as her (she can repeat missing probes as needed to account for packet loss). This is because if V is connected to the same OpenVPN server (S) as her, then S’s Conntrack table will have an nf_conn entry with S’s IP and OpenVPN port (1194) and V’s public IP address and ephemeral port. This entry makes it impossible for packets sent from A:1194 to V:Vport to be differentiated from the original tuple. Conntrack/NAT must select a new destination port for the packet sent in step (2.) to maintain transparency and operational requirements specified in the RFCs.

Assumptions

The c2mitm attack makes the following assumptions:

1. The attacker (A) knows the candidate, public IP address for a potential Victim (V).

2. V does not connect to the OpenVPN server (S) until after (A) builds the exploit primitive.

Assumption (1.) is viable in two situations. The first is if A has performed the deanonymization attack in the past, and enumerated one or more victims that includes V. The second is if V is at the same coffee shop or hotel as A. The hotel or coffee shop is likely running its own NAT to share internet resources with all the customers using the network. A can easily discover the public IP address of the establishment. We imagine (2.) holding because, as stated above, A can hold nf_conn entries in the OpenVPN server’s table indefinitely.

Execution

The c2mitm attack is executed in three stages:

1. A connects to S

 time
|         (A)ttacker               OpenVPN (S)erver           (V)ictim
|          |---{src=A:Aport, dst=S:1194}--->|                          1.1.
|          |<--{src=S:1194, dst=A:Aport}----|                          1.2.
|   tun0   |=============TUNNEL=============|                          1.3.
V


Fig 14. High-level space-time network diagram of A establishing OpenVPN tunnel to S.

 nf_conntrack_hash {
H1 .-> nf_conn[.] -> {
timeout = 179; // seconds
tuplehash = {
[ORIGINAL] .-> {src={u3=A, udp.port=Aport}, dst={u3=S, udp.port=1194}},
[REPLY]    .-> {src={u3=S, udp.port=1194}, dst={u3=A, udp.port=Aport}}
};
status = {ASSURED};
}
}


Fig 15. S’s nf_conntrack_hash table after A establishes her VPN tunnel.

2. A sends UDP packets to a victim’s public IP address with S’s OpenVPN server listening port as A’s source port, creating a Conntrack entry that S uses for routing packets back to A with. A sends a packet for each ephemeral port of V.

The c2mitm attack requires that the attacker constant cycle through the ephemeral space to keep entries fresh until the victim attempts to connect to the VPN server. The conntrack entries may expire, so the attacker can either spoof replies to the VPN server with matching UDP parameters or refresh at a faster rate. Spoofing will place the entries in the ASSURED state where they cannot be evicted before 180 seconds, but adds the requirement that the attacker has the ability to spoof packets on the Internet and doubles the number of packets per port for the initial cycle. Since spoofing is already a requirement for the c2mitm attack it makes sense to do so, and somewhat reduces the necessary rate (about 182 packets per second, assuming the IANA dynamic port range). The attacker may also keep entries alive by cycling through the ephemeral port space every 30 seconds with a packet for each port (546 packets/second) and not spoof in this step, if they so desire. If the victim’s OS complies with rfc6056, section 3.2, then this is the worst case and the packet rate to keep conntrack entries fresh for all possible victim ephemeral ports is only about 717 packets per second. Remember that these are empty packets, half of which are encrypted and they are split across tens of thousands of flows.

 time
|         (A)ttacker                                              OpenVPN (S)erver                        (V)ictim
|          |-------------------{src=A:Aport, dst=S:1194}------------------->|                                |       1.1.
|          |<------------------{src=S:1194, dst=A:Aport}--------------------|                                |       1.2.
|   tun0   |===========================TUNNEL===============================|                                |       1.3.
|          |---{src=A:Aport, dst=S:1194, <src=tun0A:1194, dst=V:Vport1>}--->|                                |       2.1.
|          |                                                                |---{src=S:1194, dst=V:Vport1}-X |       2.2.
:          :                                                                :                                :
|          |---{src=A:Aport, dst=S:1194, <src=tun0A:1194, dst=V:VportN>}--->|                                |       2.3.
|          |                                                                |---{src=S:1194, dst=V:VportN}-X |       2.4.
V


Fig 16. Space-time digram depicting execution step (2.) for c2mitm.

 nf_conntrack_hash {
H1 .-> nf_conn[.] -> {
timeout = 179; // seconds
tuplehash = {
[ORIGINAL] .-> {src={u3=A, udp.port=Aport}, dst={u3=S, udp.port=1194}},
[REPLY]    .-> {src={u3=S, udp.port=1194}, dst={u3=A, udp.port=Aport}}
};
status = {ASSURED};
},
H2 .-> nf_conn[.] -> {
timeout = 29; // seconds
tuplehash = {
[ORIGINAL] .-> {src={u3=tun0A, udp.port=1194}, dst={u3=V, udp.port=Vport1}},
[REPLY]    .-> {src={u3=V, udp.port=Vport1}, dst={u3=S, udp.port=1194}}
};
status = {UNREPLIED};
}
:
:
Hk .-> nf_conn[.] -> {
timeout = 29; // seconds
tuplehash = {
[ORIGINAL] .-> {src={u3=tun0A, udp.port=1194}, dst={u3=V, udp.port=VportN}},
[REPLY]    .-> {src={u3=V, udp.port=VportN}, dst={u3=S, udp.port=1194}}
};
status = {UNREPLIED};
}
}


Fig 17. S’s Conntrack table after executing step (2.) for c2mitm.

3. V sends an OpenVPN connection request to S.

time
|         (A)ttacker                                              OpenVPN (S)erver                        (V)ictim
|          |-------------------{src=A:Aport, dst=S:1194}------------------>|                               |       1.1.
|          |<------------------{src=S:1194, dst=A:Aport}-------------------|                               |       1.2.
|   tun0   |===========================TUNNEL==============================|                               |       1.3.
|          |---{src=A:Aport, dst=S:1194, <src=tun0A:1194, dst=V:Vport>}--->|                               |       2.1.
|          |                                                               |---{src=S:1194, dst=V:Vport}-x |       2.2.
|          |                                                               |<--{src=V:Vport, dst=S:1194}---|       3.1.
|          |<--{src=V:1194, dst=A:Aport, <src=V:Vport, dst=tun0A:1194>}----|                               |       3.2.
|          |-------------------{src=A:Vport, dst=S:1194}------------------>|                               |       3.3.
|          |<------------------{src=S:1194, dst=A:Vport}-------------------|                               |       3.4.
|          |---{src=S:1194, dst=A:Vport, <src=tun0A:1194, dst=V:Vport>}--->|                               |       3.5.
|          |                                                               |---{src=S:1194, dst=V:Vport}-->|       3.6.
|          |                                                               |<--{src=V:Vport, dst=S:1194}---|       3.7.
|          |<--{src=V:1194, dst=A:Aport, <src=V:Vport, dst=tun0A:1194>}----|                               |       3.8.
|          |---{src=S:1194, dst=A:Vport, <src=tun0A:1194, dst=V:Vport>}--->|                               |       3.9.
|          |                                                               |---{src=S:1194, dst=V:Vport}-->|       3.10.
V


Fig. 18. Network space-time diagram of the c2mitm attack to completion.

nf_conntrack_hash {
H1 .-> nf_conn[.] -> {
timeout = 179; // seconds
tuplehash = {
[ORIGINAL] .-> {src={u3=A, udp.port=Aport}, dst={u3=S, udp.port=1194}},
[REPLY]    .-> {src={u3=S, udp.port=1194}, dst={u3=A, udp.port=Aport}}
};
status = {ASSURED};
},
H2 .-> nf_conn[.] -> {
timeout = 29; // seconds
tuplehash = {
[ORIGINAL] .-> {src=u3{=tun0A, udp.port=1194}, dst={u3=V, udp.port=Vport}},
[REPLY]    .-> {src={u3=V, udp.port=Vport}, dst={u3=S, udp.port=1194}}      ******
};
status = {ASSURED};
}
}


Fig. 19. Shows the server, S’s Conntrack table just after V sends a connection request to S and before A has relayed V’s connection request. This is at steps 3.1 and 3.2 of the Fig. 19 above. The asterisks indicate a state of network connection that should not exist, because V will attempt to initiate a connection but match a NAT rule as a REPLY.

nf_conntrack_hash {
H1 .-> nf_conn[.] -> {
timeout = 179; // seconds
tuplehash = {
[ORIGINAL] .-> {src={u3=A, udp.port=Aport}, dst={u3=S, udp.port=1194}},
[REPLY]    .-> {src={u3=S, udp.port=1194}, dst={u3=A, udp.port=Aport}}
};
status = {ASSURED};
},
H2 .-> nf_conn[.] -> {
timeout = 29; // seconds
tuplehash = {
[ORIGINAL] .-> {src=u3{=tun0A, udp.port=1194}, dst={u3=V, udp.port=Vport}},
[REPLY]    .-> {src={u3=V, udp.port=Vport}, dst={u3=S, udp.port=1194}}
};
status = {ASSURED};
},
H3 .-> nf_conn[.] -> {
timeout = 179; // seconds
tuplehash = {
[ORIGINAL] .-> {src=u{3=A, udp.port=Vport}, dst={u3=S, udp.port=1194}},
[REPLY]    .-> {src={u3=S, udp.port=1194}, dst={u3=A, udp.port=Vport}}
};
status = {ASSURED};
},
}


Fig. 20. Shows S’s Conntrack table just after V sends a connection request to S and after A has relayed the V’s connection request, successfully become a man-in-the-middle between V and S (steps 3.4-3.10).

As we have shown, both the deanonymization and c2mitm attacks are possible because of implementation details in Conntrack’s NATing functionality. The difference in attacks is dependent on the order in which a victim connects to the same OpenVPN server as the attacker. Each case is harmful in its own right. The deanonymization attack is harmful because an often cited use case for VPN software is anonymity. The c2mitm attack places the attacker in-path for the victim’s connection to the VPN server, which could lead to DNS or TCP hijacking, traffic analysis, and other attacks that normally would be outside the reach of an off-path attacker who is just another VPN client.

Not only is the victim deanonymized, but the attacker is also positioned in the network in-path. From this position, the attacker can leverage a recently disclosed attack (Tolley2021 ) against OpenVPN servers. This attack assumes an in-path attacker. The attacker spoofs packet to the OpenVPN server from some other globally routable IP address such as a website or DNS server. The in-path attacker can use the DNS redirect primitive to send the victim to an attacker controlled server. If the server the victim was attempting to access is plaintext HTTP or the attacker has a compromised SSL/TLS certificate, the attacker has carte blanche over the victim.

Although the attacks we have described above enable the attacker to deanonymize the victim and redirect their web connections to an attacker-controlled server, demonstrating the c2mitm attack with TCP shows that the problem is not UDP specific and may open up other possible attacks that involve other services and ports.

Assumptions

1. Attacker is in-path
2. Attacker can spoof packets from a NAT-external DNS server to the OpenVPN server.

Execution

The attack is executed in two steps:

1. Send TTL limited TCP SYN packets to the victim’s public IP address through the VPN.
 time
|         (A)ttacker               OpenVPN (S)erver               (V)ictim
|          |----{src=A:80, dst=V:pA}-->|---{src=S:80, dst=V:pA}-x  |   1.1. TCP SYN
|          |----{src=A:80, dst=V:pB}-->|---{src=S:80, dst=V:pB}-x  |   1.2. TCP SYN
.          .                           .                           .
|          |----{src=A:80, dst=V:pN}-->|---{src=S:80, dst=V:pN}-x  |   1.3. TCP SYN
V


Fig. 21. Space-time diagram of A sending TTL limited SYN packets to V on all ephemeral ports.

 nf_conntrack_hash {
H1 .-> nf_conn[.] -> {
timeout = 179; // seconds
tuplehash = {
[ORIGINAL] .-> {src={u3=A, tcp.port=80}, dst={u3=V, tcp.port=pA}},
[REPLY]    .-> {src={u3=V, tcp.port=pA}, dst={u3=S, tcp.port=80}}
};
protocol.tcp.state={SYN_SENT}
status = {UNASSURED, UNREPLIED};
},
H2 .-> nf_conn[.] -> {
timeout = 179; // seconds
tuplehash = {
[ORIGINAL] .-> {src={u3=A, tcp.port=80}, dst={u3=V, tcp.port=pB}},
[REPLY]    .-> {src={u3=V, tcp.port=pB}, dst={u3=S, tcp.port=80}}
};
protocol.tcp.state={SYN_SENT}
status = {UNASSURED, UNREPLIED};
},
.
.
Hn .-> nf_conn[.] -> {
timeout = 179; // seconds
tuplehash = {
[ORIGINAL] .-> {src={u3=A, tcp.port=80}, dst={u3=V, tcp.port=pN}},
[REPLY]    .-> {src={u3=V, tcp.port=pN}, dst={u3=S, tcp.port=80}}
};
protocol.tcp.state={SYN_SENT}
status = {UNASSURED, UNREPLIED};
},
}


Fig. 21. S’s Conntrack table after all of A’s N SYN packets traverse Conntrack.

2. DNS inject the URL with the OpenVPN server’s IP address
3. Victim initates 3-way handshake to OpenVPN server
time
|         (A)ttacker                   OpenVPN (S)erver                                          (V)ictim
|        |<----{src=V:pA, dst=S:80, tcp.flags=SYN}----|<--{src=V:pA, dst=S:80, tcp.flags=SYN}-----|         1.1.
V


Fig. 22. Space-time diagram of V sending a SYN for as part of the TCP three-way handshake after a successful DNS injection by the attacker.

nf_conntrack_hash {
H1 .-> nf_conn[.] -> {
timeout = 179; // seconds
tuplehash = {
[ORIGINAL] .-> {src={u3=A, tcp.port=80}, dst={u3=V, tcp.port=pA}},
[REPLY]    .-> {src={u3=V, tcp.port=pA}, dst={u3=S, tcp.port=80}}
};
protocol.tcp.state={SYN_SENT2}
status = {REPLIED};
},
H2 .-> nf_conn[.] -> {
timeout = 179; // seconds
tuplehash = {
[ORIGINAL] .-> {src={u3=A, tcp.port=80}, dst={u3=V, tcp.port=pB}},
[REPLY]    .-> {src={u3=V, tcp.port=pB}, dst={u3=S, tcp.port=80}}
};
protocol.tcp.state={SYN_SENT}
status = {UNASSURED, UNREPLIED};
},
.
.
Hn .-> nf_conn[.] -> {
timeout = 179; // seconds
tuplehash = {
[ORIGINAL] .-> {src={u3=A, tcp.port=80}, dst={u3=V, tcp.port=pN}},
[REPLY]    .-> {src={u3=V, tcp.port=pN}, dst={u3=S, tcp.port=80}}
};
protocol.tcp.state={SYN_SENT}
status = {UNASSURED, UNREPLIED};
},
}


Fig. 23. S’s Conntrack table after V’s SYN packet.

time
|         (A)ttacker                   OpenVPN (S)erver                                          (V)ictim
|        |<----{src=V:pA, dst=S:80, tcp.flags=SYN}----|<--{src=V:pA, dst=S:80, tcp.flags=SYN}-----|         1.1.
|        |---{src=V:pA, dst=S:80, tcp.flags=SYN/ACK}->|--{src=V:pA, dst=S:80, tcp.flags=SYN/ACK}->|         1.1.
V


Fig. 24. Space-time diagram of A sending a SYN/ACK as part of the TCP three-way handshake.

nf_conntrack_hash {
H1 .-> nf_conn[.] -> {
timeout = 179; // seconds
tuplehash = {
[ORIGINAL] .-> {src={u3=A, tcp.port=80}, dst={u3=V, tcp.port=pA}},
[REPLY]    .-> {src={u3=V, tcp.port=pA}, dst={u3=S, tcp.port=80}}
};
protocol.tcp.state={SYN_RECV}
status = {REPLIED};
},
H2 .-> nf_conn[.] -> {
timeout = 179; // seconds
tuplehash = {
[ORIGINAL] .-> {src={u3=A, tcp.port=80}, dst={u3=V, tcp.port=pB}},
[REPLY]    .-> {src={u3=V, tcp.port=pB}, dst={u3=S, tcp.port=80}}
};
protocol.tcp.state={SYN_SENT}
status = {UNASSURED, UNREPLIED};
},
.
.
Hn .-> nf_conn[.] -> {
timeout = 179; // seconds
tuplehash = {
[ORIGINAL] .-> {src={u3=A, tcp.port=80}, dst={u3=V, tcp.port=pN}},
[REPLY]    .-> {src={u3=V, tcp.port=pN}, dst={u3=S, tcp.port=80}}
};
protocol.tcp.state={SYN_SENT}
status = {UNASSURED, UNREPLIED};
},
}


Fig. 25. S’s Conntrack table after A’s SYN/ACK packet.

time
|         (A)ttacker                   OpenVPN (S)erver                                          (V)ictim
|        |<----{src=V:pA, dst=S:80, tcp.flags=SYN}----|<--{src=V:pA, dst=S:80, tcp.flags=SYN}-----|         1.1.
|        |---{src=V:pA, dst=S:80, tcp.flags=SYN/ACK}->|--{src=V:pA, dst=S:80, tcp.flags=SYN/ACK}->|         1.1.
|        |<----{src=V:pA, dst=S:80, tcp.flags=ACK}----|<--{src=V:pA, dst=S:80, tcp.flags=ACK}-----|         1.1.
V


Fig. 26. Space-time diagram of V sending the final ACK, completing the TCP three-way handshake.

nf_conntrack_hash {
H1 .-> nf_conn[.] -> {
timeout = 179; // seconds
tuplehash = {
[ORIGINAL] .-> {src={u3=A, tcp.port=80}, dst={u3=V, tcp.port=pA}},
[REPLY]    .-> {src={u3=V, tcp.port=pA}, dst={u3=S, tcp.port=80}}
};
protocol.tcp.state={SYN_RECV}
status = {UNASSURED, REPLIED};
},
H2 .-> nf_conn[.] -> {
timeout = 179; // seconds
tuplehash = {
[ORIGINAL] .-> {src={u3=A, tcp.port=80}, dst={u3=V, tcp.port=pB}},
[REPLY]    .-> {src={u3=V, tcp.port=pB}, dst={u3=S, tcp.port=80}}
};
protocol.tcp.state={SYN_SENT}
status = {UNASSURED, UNREPLIED};
},
.
.
Hn .-> nf_conn[.] -> {
timeout = 179; // seconds
tuplehash = {
[ORIGINAL] .-> {src={u3=A, tcp.port=80}, dst={u3=V, tcp.port=pN}},
[REPLY]    .-> {src={u3=V, tcp.port=pN}, dst={u3=S, tcp.port=80}}
};
protocol.tcp.state={SYN_SENT}
status = {UNASSURED, UNREPLIED};
},
}


Fig. 27. S’s Conntrack table after V’s final ACK packet. Notice that the matching conntrack entry for the TCP connection (H1), is still in the UNASSURED and SYN_RECV state. This is because, from Netfitler’s perspective, A has not sent the final ACK to complete the simultaneous open. This also means the connection cannot be in the ASSURED state because that connection is technically not established from Netfilter’s perspective.

In addition to using the DNS redirect primitive to send the victim to some NAT-external server, the attacker can even redirect the victim back to the in-path machine through which the victim’s traffic are already being routed. In this ouroboros configuration the attacker uses a similar methodology for c2mitm, except this time, she sends TTL limited TCP SYN packets through the VPN server to the victim with whatever port the client thinks the server is using, e.g., port 80 or 443. Next, when the victim’s (forged) DNS query returns the VPN server’s IP address, the victim sends a SYN packet to the VPN server. Because the attacker primed the Conntrack table with SYN packets to each of the victim’s ephemeral ports, the table is full of nf_conn entries in the SYN_SENT state. When the victim’s SYN packet is translated using the matching nf_conn entry, that entry is updated to be in the SYN_SENT2 state and the victim’s SYN packet is sent to the attacker. Next, the attacker sends an SYN/ACK to the victim, who finally responds back to the attacker with the final ACK. From the victim’s perspective, she is initiating a 3-way handshake, but from the OpenVPN server (NAT)’s perspective, a simultaneous open is happening. In this way, the attacker tricks the victim into connecting back to the in-path router. Note that similar tricks can be used to connect to services running on a victim behind a NAT or perform various port scans against such a victim. Simultaneous open, described in rfc794, section 3.4, is a less common connection establishment procedure where two machines initiate a connection between eachother by both exchanging SYNs, SYN/ACKs, and ACKs similar to how the three-way handshake works.

There are two obvious ways to mitigate these issues, to some extent:

1. The server could add firewall rules to prevent the port the VPN service is listening on from being used as a source port by clients.
2. The NAT functionality could be changed (e.g., as the VPN server is sending decrypted packets over the virtual interface, or by changing the conntrack module of Netfilter upstream) so that any port not designated as “Dynamic and/or Private” by IANA is translated into such a port.

Either of these changes would prevent the specific attacks presented above. However, one can envision other attacks that do not involve specific ports, such as attacks on BitTorrent users.

A comprehensive fix to this vulnerability would entail somehow modifying the notions of garbage collection, connection direction, connection status, and simultaneous open in NAT implementations to be more consistent with the security and privacy requirements of VPNs.

The RFCs mentioned throughout this disclosure make various references to security and privacy. However, the level of security provided by a NAT based on network configuration is not clearly defined. Furthermore, the NAT-router + server situation, as in the OpenVPN case, gives rise to this particular attack. This host configuration is not explicitly addressed in the RFCs, though some do state that the NAT-external IPs and ports should be managed “appropriately” using firewall rules. The lack of end-to-end security is known and stated in the RFCs as is the fact that NAT brakes end-to-end model of IP and is simply a work-around to prolonging IPv4’s lifetime. The following are some specific examples of security considerations related to our attack.

rfc 2663, section 9, states:

Many people view traditional NAT router as a one-way (session) traffic filter, restricting sessions from external hosts into their machines.

However, our port shadowing examples, particularly our abuse of TCP simultaneous open, demonstrate that an attacker can bypass one-way traffic filtering in specification conditions.

rfc 2993, section 10

Determine need for public toward private connections, variability of destinations on the private side, and potential for simultaneous use of public side port numbers. NAPTs increase administration if these apply.

Port shadowing demonstrates why this point is critical to preserving client security and privacy.

rfc 4787, section 13 states:

This document recommends that the NAT filters be specific to the external IP address only (see REQ-8) and not to the external IP address and UDP port. It can be argued that this is less secure than using the IP and port. Devices that wish to filter on IP and port do still comply with these requirements.

However, port shadowing clearly requires some port-dependent action be taken by the NAT to mitigate unintentionally forwarding packets to an attacker.

NAT related RFCs:

Protocol RFCs:

While this write-up covers Netfilter and Linux in detail, we have also tested our attacks on FreeBSD 13 using natd, ipfw, ipf, and pf. We found that all the implementations are susepctible to port shadowing, however, only natd appears to succeptable to a c2mitm using OpenVPN the same was as Netfilter on Linux.

The following CPE information is the software setup for our tests.

1. part=”a”, vendor=”Netfilter”, product=”Netfilter”
2. part=”o”, vendor=”Linux”, product=”Linux”, version=”4.15.0-142”
3. part=”a”, vendor=”OpenVPN”, product=”OpenVPN Server”, version=”2.4.4”,
4. part=”o”, vendor=”FreeBSD”, product=”FreeBSD-13”
5. part=”a”, vendor=”FreeBSD”, product=”natd”

We would like to thank our colleagues, William J. Tolley, Beau Kujath, and Jeffrey Knockel for their input and feedback during the analysis and attack execution phases of this work.

This project received funding from the following sources:

This material is based upon work supported by the U.S. National Science Foundation under Grant Nos. 1801613 and 2007741 . Any opinions, findings and conclusions or recommendations expressed in this material do not necessarily reflect the views of the National Science Foundation.