Firewall and NAT bypass: UDP hole punching

Firewall and NAT bypass: UDP hole punching

This post was originally published in 2018

Intro

UDP hole punching is a widely documented firewall bypass technique. It doesn’t exploit any bugs or flaws, it does exploit UDP’s sessionless nature and firewalls’ stateful nature.

The technique relies on the fact that in a UDP conversation there is no session establishment, so in reality there’s no concept of inbound or outbound connection – there’s only inbound or outbound packets. That means that a firewall device has to rely on limited information to let returning packets through the firewall. In fact, it only relies on the UDP 5-tuple: Source IP address, source port, destination IP address, destination port, protocol.

Statefulness is not a state of mind

Firewalls are stateful devices, so they keep track of connections instead of just keeping track of packets. The best example of the difference between a firewall (stateful device) and a router with ACLs (stateless device) would be to compare what do both do when a new connection from an internal host to an external host starts:

  • The internal host creates a new connection to the external host, with source port 12345 and destination port 80 thus sending an outbound SYN packet.
  • Both stateful and stateless device would permit this traffic if they’ve got an allow ACL from any internal host, to any external hosts, any source port, 80/TCP as destination port.
  • The difference is the stateful device would add an entry into its “connections table” to keep track of the connection, while the stateless device would not.
  • The external host replies with a SYN-ACK with source port 80, destination port 12345 as it’s the opposite direction from the SYN!
  • A stateful device would recognise this as a returning packet for a connection in its table (it was expecting a SYN-ACK from the external host on those specific ports *and* with a specific ACK number!), so it will forward it to the internal host without further rule processing.
  • A stateless device would not recognise it as part of a connection as, well, stateless devices don’t keep track of connections. It would check the inbound packet against its ACLs and unless you had a rule permitting all inbound traffic to port 12345, it would drop it. As you know, in most network protocols the chosen source port for a new connection is random (and usually a high number). Filtering by port on a stateless device is, needless to say, a nightmare with little to no benefits.

So what would happen in the above case if the protocol was UDP, a protocol that doesn’t have a 3-way handshake to track connection establishment nor it has SEQ/ACK numbers? In that case an outbound packet would also trigger the creation of a connection (remember UDP is not connection-based!) in the firewall’s connection table, so any inbound packets with the right IP addresses and ports in its headers will be allowed back!

At this point you might already see where we’re going and how UDP hole punching works, so let’s cut to the chase.

Let’s punch a hole through a firewall: a practical scenario.

Just imagine this fairly common scenario:

  • Host A behind firewall A has outbound traffic permitted, inbound traffic denied.
  • Host B behind firewall B has outbound traffic permitted, inbound traffic denied.

UDP hole punching can make host A and host B talk to each other by exploiting the statefulness of the firewall blocking the inbound UDP traffic. How?

I’ve built the above scenario in Azure with two VMs in separate networks. Each VM has its own public IP address and both have applied inbound and outbound rules as Network Security Groups (sort of a platform in-built firewall).

VM1

VM1 rules block inbound traffic by default, except SSH traffic. They also permit all outbound traffic to Internet by default.

VM2

VM1 rules block inbound traffic by default, except SSH traffic. They also permit all outbound traffic to Internet by default.

rules2

Any inbound traffic not explicitly permitted is blocked by the last rule in the ruleset. This means all UDP traffic between these two VMs is blocked inbound at destination, so if you wanted VM1 and VM2 to talk to each other using a UDP protocol you would need to open the relevant ports… right? Of course not! that’s the whole point of this article 🙂

Puncher.go

I’ve written a PoC that sends UDP packets with a number (increasing in 1 for each packet sent) as the payload. The PoC also prints the contents of any UDP traffic it receives in order to confirm we are able to receive traffic from the other peer. It uses UDP port 10001 as destination port, but also as a source port. Why? Because making both ports predictable I can bypass the statefulness of Azure’s Network Security Groups or NSGs, just as it would bypass any other firewall blocking all inbound UDP traffic!

UDP hole puncher PoC
UDP hole puncher PoC. GitHub Gist: instantly share code, notes, and snippets.

The code above sends outbound UDP packets with source port 10001 and destination port 10001. When I run it on host A, I put host B’s IP address in ServerIP. When I run it on host B, I put host A’s IP address in ServerIP.

If I were to run puncher.go in host A, thus sending packets to host B, and run just a UDP listener in host B I would not receive any traffic in host B because inbound UDP is blocked by Azure NSGs in host B. If I leave puncher.go running in host A and then I run it too in host B, Azure NSGs on both sides would create an entry to permit returning UDP traffic on ports 10001 as source and destination, coming from each other’s public IP addresses. This implies that after a packet outbound on each side, the connection will start working in full duplex, thus completely bypassing the block in inbound rules.

It works like a charm:

Implications

This technique has been known for decades, so the implications of it are fairly well understood. In fact it is exploited by legit applications like IPSec VPNs to work around restrictions like NAT.

In some scenarios, a firewall device might be doing NAT for outbound traffic, but not for inbound traffic effectively blocking any inbound packets. UDP hole punching would enable a device behind such NAT to receive traffic from another peer in the same situation. More information on RFC3948 UDP Encapsulation of IPsec ESP packets.

Please also note that a NAT that changes the source port (AKA PAT, overload NAT, dynamic NAT, hide NAT, etc depending on the vendor!) would break this technique… And your VPNs 🙂

Subscribe to Cloud Networking Pro

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe