Security Fixes & Conntrack Hardening in SPR
Anvil Secure recently published a post and whitepaper covering conntrack flaws that are common with many linux routers and linux "multihomed" devices. In this post we'll cover SPR, how our process mitigated the highest risk vulnerabilities, how we fixed the rest and other improvements we're making to be resilient against attacks like this in the future.
Overview
Conntrack is part of Linux Netfilter and is an integral part of a stateful firewall for allowing Network Address Translation on a network. A router uses it to allow clients to establish connections through the uplink interfaces.
Anvil Secure published details on how devices often fail to lock down their firewalls correctly since Conntrack operates at layer 3. External attackers that are one hop away can abuse this to spoof IP addresses and send traffic to internal interfaces on devices and routers for an established connection managed with conntrack. For most of our users, this limits the attack to compromised or hostile ISP providers, which is an uncommon (but not unheard of attack vector). However, since the WiFi Pod can be used as a travel router, it's important to us that they are can withstand being attached to a hostile network.
The riskiest of the attacks happen to not affect SPR.
Potential Attacks
Attack #1: The uplink interface reaching internal services on the router that are not meant to be accessible to the outside
- Severity: High
- Status: Not Vulnerable
SPR's firewall explicitly matches interfaces to the service ports on the router, so the uplink interface can only connect to service ports if they're explicitly set to be externally facing, even with IP spoofing.
Attack #2: Punching holes in a router firewall with NAT-PMP
- Severity: High
- Status: Not Vulnerable
SPR does not run NAT-PMP.
Attack #3: IP Spoofing On External Service Ports
- Severity: Medium
- Status: Patched in v1.0.1
The uplink connection to an externally exposed service port could spoof the IP address to match an established connection and blindly send UDP packets by spamming source ports or the same for TCP (if they can guess the sequence numbers plus the source port). This traffic could then present itself on a connection between SPR and a LAN client.
Attack #4: VLAN Routing with Spoofed IPs against Service Ports
- Severity: Low
- Status: Patched in v1.0.1
Similarly, VLANs could also perform the same attack against each other internally to attempt to inject traffic into an established connection.
Attack Indicators
SPR logs dropped packets and has alerts for MAC spoofing. In the process of running the attack, the source ports must be enumerated to match the established internal connection. This would create nft:drop:mac
events when the attacker scans for an established source port. Users can see dropped packets under the alerts pane. To filter the external interface, the filter can be set to Event.InDev=="eth0"
for example if the uplink interface is eth0
.
On VPN/Docker
Port Shadow attacks are a similar issue. On a shared VPN host with multiple users a malicious adversary could use the VPN's destination port as the source port to confuse stateful firewall rules into redirecting traffic to set up a MITM attack. SPR hardens against this attack by blocking source ports on the wireguard port
SPR also runs as a VPN under a virtualized docker network. In this scenario SPR does not manage the host firewall, and relies on Docker's host rules for forwarding services. We cover network attacks against Container Networks in a prior post. In SPR we harden against attacks on Docker's overly premissive networking rules by limiting API access to the container network interface and subnets.
On Wi-Fi
Anvils' writeups have great information, and one especially good gem of wisdom in the whitepaper is as follows:
"For example, on NAT router[s] supporting both Wi‑Fi and Ethernet, a communications between two Wi‑Fi clients are likely to stay on the Wi‑Fi chip."
This is because with WPA the typical operation is that when station PeerA transmits to PeerB it will encrypt with it's unicast pariwise temporal key (PTK) and send a packet to the AP with Receiver Address(RA)=BSSID and Destination Address (DA). The AP re-encryptes the traffic with PeerB's PTK and sends it without going through the OS networking stack.
Many Guest networks rely on this bridging to be blocked when hostapd has "ap_isolate=1" enabled. Unfortunately most setups don't do any hardening against routing. So if an adversary instead transmits with RA=DA=BSSID and the IP Destination of PeerB, the router will happily route the packet to PeerB over the networking stack, and then re-encrypt the traffic with the PeerB PTK .
As this next part from the whitepaper mentions, bridging mediums is even more susceptible since they must go through the routing tables (and in turn imply a round trip is possible with spoofing attacks to also receive traffic).
"A Wi‑Fi client communicating with an Ethernet host on the other hand, could pass through the NAT router via the bridge"
In SPR we defend against WiFi-based attacks as follows:
- We set ap_isolate=1 to avoid L2 bridging in the chipset where the AP re-encrypts unicast traffic to the other peer
- We use Per-Station VLANs with unique group keys for each to avoid GTK-based communications bypassing the AP
- We support unique device WPA2/3 passwords to block rogue AP attacks as well as passive key derivation on WPA2.
- We use MAC filters to stop IP spoofing without knowing the spoofed device's WiFi password & MAC address.
With conntrack we had some exposure (#4 above), where established connections could be spoofed into SPR's service ports across VLANs to bypass the MAC filter.
The fixes
To address the issues we've added rules to explicitly block LAN IP ranges from/to the uplink interfaces on the INPUT tables [1]. Secondly we've moved the MAC filters [2] before the conntrack rules.
diff --git a/base/scripts/nft_rules.sh b/base/scripts/nft_rules.sh
index 7b96abd6..b03545fd 100755
--- a/base/scripts/nft_rules.sh
+++ b/base/scripts/nft_rules.sh
@@ -1,8 +1,5 @@
#!/bin/bash
-#TBD:
-#- can F_EST_RELATED be moved past MAC spoof check
-
# Disable forwarding
sysctl net.ipv4.ip_forward=0
@@ -246,6 +243,10 @@ table inet filter {
iifname @uplink_interfaces log prefix "wan:in " group 0
iifname != @uplink_interfaces log prefix "lan:in " group 0
+ # block lan ranges from uplink interfaces #[1]
+ iifname @uplink_interfaces ip saddr @supernetworks goto DROPLOGINP
+ iifname @uplink_interfaces ip daddr @supernetworks goto DROPLOGINP
+
# Drop input from the site to site output interfaces. They are only a sink,
# Not a source that can connect into SPR services
counter iifname @outbound_sites goto DROPLOGINP
@@ -270,7 +271,6 @@ table inet filter {
$(if [ "$VIRTUAL_SPR_API_INTERNET" ]; then echo "" ; elif [[ "$WAN_NET" ]]; then echo "counter iifname @uplink_interfaces tcp dport 80, 443 ip saddr != $WAN_NET drop"; fi)
$(if [ "$VIRTUAL_SPR_API_INTERNET" ]; then echo "" ; elif [[ "$WAN_NET" ]]; then echo "counter iifname @uplink_interfaces udp dport 53, 67 ip saddr != $WAN_NET drop"; fi)
- counter jump F_EST_RELATED # [2]
# DHCP Allow rules
# Wired lan
@@ -284,6 +284,8 @@ table inet filter {
# Prevent MAC Spoofing from LANIF, wired interfaces
iifname @lan_interfaces jump DROP_MAC_SPOOF
+ counter jump F_EST_RELATED # [2]
+
# DNS Allow rules
# Docker can DNS
$(if [ "$DOCKERIF" ]; then echo "iif $DOCKERIF ip saddr $DOCKERNET udp dport 53 counter accept"; fi)
@@ -343,6 +345,15 @@ table inet filter {
# Allow DNAT for port forwarding
counter ct status dnat accept
+ # block lan ranges from uplink interfaces
+ # uplinks can not NAT FROM @supernetworks source addresses
+ iifname @uplink_interfaces ip saddr @supernetworks goto DROPLOGFWD
+ # uplinks can not receive @supernetworks destination addresses
+ oifname @uplink_interfaces ip daddr @supernetworks goto DROPLOGFWD
+
+ # Verify MAC addresses for LANIF/WIPHYs
+ iifname @lan_interfaces jump DROP_MAC_SPOOF [2]
+
counter jump F_EST_RELATED
# Do not forward from uplink interfaces after dnat
@@ -355,8 +366,6 @@ table inet filter {
oifname @uplink_interfaces log prefix "wan:out " group 0
oifname != @uplink_interfaces log prefix "lan:out " group 0
- # Verify MAC addresses for LANIF/WIPHYs
- iifname @lan_interfaces jump DROP_MAC_SPOOF [2]
# After MAC SPOOF check, but before rfc1918 check
# These rules allow permits via endpoint verdict maps
@@ -432,6 +441,8 @@ table inet filter {
chain OUTPUT {
type filter hook output priority 0; policy accept
+ oifname @uplink_interfaces ip daddr @supernetworks goto DROPLOGOUTP
+ oifname @uplink_interfaces ip saddr @supernetworks goto DROPLOGOUTP
}
chain DROPLOGFWD {
@@ -444,6 +455,11 @@ table inet filter {
counter drop
}
+ chain DROPLOGOUTP {
+ counter log prefix "drop:output " group 1
+ counter drop
+ }
+
chain F_EST_RELATED {
ip protocol udp ct state related,established counter accept
ip protocol tcp ct state related,established counter accept
@@ -472,11 +488,6 @@ table inet nat {
$(if [ "$LANIF" ]; then echo "elements = { $LANIF }" ; fi )
}