NAT is not a security mechanism

Introduction

It’s occasionally argued that Network Address Translation, as commonly used with domestic Internet connections, somehow protects your network from attack. The assumption seems to be that since translation of packets on their way through the router means that external packets that are addressed to “internal” host will not be forwarded.

Although it is normal to configure NATing routers to prohibit ingress to the “internal” network, other than for packets from established connections, it is not the NAT that excludes inbound packets. This is easy to see, by simply configuring a router to perform NAT but omit any further packet filtering.

Configuration

Router Configuration

The router is called wampoon. Its eth0 is attached to 172.17.207.0/24; in a typicaly domestic configuration this would represent the ISP’s network (and might be a point-to-point link rather than an ethernet). Usually this would not use RFC1918 addresses; the fact that I’m doing so here doesn’t make any meaningful difference..

richard@wampoon:~$ ip addr show eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:15:5d:cf:42:07 brd ff:ff:ff:ff:ff:ff
    inet 172.17.207.76/24 brd 172.17.207.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 2001:470:1f09:11ed::4c/64 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::215:5dff:fecf:4207/64 scope link
       valid_lft forever preferred_lft forever

eth1 is attached to 172.20.211.0/24. This represents the internal network.

richard@wampoon:~$ ip addr show eth1
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:15:5d:cf:42:09 brd ff:ff:ff:ff:ff:ff
    inet 172.20.211.1/24 brd 172.20.211.255 scope global eth1
       valid_lft forever preferred_lft forever
    inet6 fe80::215:5dff:fecf:4209/64 scope link
       valid_lft forever preferred_lft forever

…and here’s the routing table:

richard@wampoon:~$ ip route show
default via 172.17.207.1 dev eth0 onlink
172.17.207.0/24 dev eth0 proto kernel scope link src 172.17.207.76
172.20.211.0/24 dev eth1 proto kernel scope link src 172.20.211.1

The router’s packet filtering is established as follows. There’s only one entry, which enables NAT on outbound packets.

#!/bin/sh
set -e
for table in INPUT OUTPUT FORWARD; do
    iptables -P $table ACCEPT
    iptables -F $table
done
for table in POSTROUTING PREROUTING INPUT OUTPUT; do
    iptables -t nat -F $table
done

# NAT connections from the internal network to the “outside”
iptables -t nat -A POSTROUTING -j MASQUERADE -o eth0

Client Configuration

The client behind the router, called tasp, has very simple network configuration. It uses the router’s “internal” (eth1) address as its default gateway.

root@tasp:~# ip addr show eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 00:15:5d:cf:42:0a brd ff:ff:ff:ff:ff:ff
    inet 172.20.211.20/24 brd 172.20.211.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::215:5dff:fecf:420a/64 scope link
       valid_lft forever preferred_lft forever
root@tasp:~# ip route show
default via 172.20.211.1 dev eth0
169.254.0.0/16 dev eth0  scope link  metric 1000
172.20.211.0/24 dev eth0  proto kernel  scope link  src 172.20.211.20

Normal Operation

First, NAT is happening as expected:

root@tasp:~# ping -c2 172.17.207.1
PING 172.17.207.1 (172.17.207.1) 56(84) bytes of data.
64 bytes from 172.17.207.1: icmp_seq=1 ttl=63 time=0.668 ms
64 bytes from 172.17.207.1: icmp_seq=2 ttl=63 time=0.596 ms

--- 172.17.207.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.596/0.632/0.668/0.036 ms

Here’s the untranslated packets on the internal network. Note the internal address, underlined:

root@wampoon:~# tcpdump -npieth1 icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), capture size 262144 bytes
21:11:00.244644 IP 172.20.211.20 > 172.17.207.1: ICMP echo request, id 18811, seq 1, length 64
21:11:00.245087 IP 172.17.207.1 > 172.20.211.20: ICMP echo reply, id 18811, seq 1, length 64
21:11:01.243867 IP 172.20.211.20 > 172.17.207.1: ICMP echo request, id 18811, seq 2, length 64
21:11:01.244313 IP 172.17.207.1 > 172.20.211.20: ICMP echo reply, id 18811, seq 2, length 64
^C

…and here’s the NATed packets as forwarded by the router. The internal address has been replaced with an external address.

root@wampoon:~# tcpdump -npieth0 icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
21:11:00.244688 IP 172.17.207.76 > 172.17.207.1: ICMP echo request, id 18811, seq 1, length 64
21:11:00.245073 IP 172.17.207.1 > 172.17.207.76: ICMP echo reply, id 18811, seq 1, length 64
21:11:01.243918 IP 172.17.207.76 > 172.17.207.1: ICMP echo request, id 18811, seq 2, length 64
21:11:01.244297 IP 172.17.207.1 > 172.17.207.76: ICMP echo reply, id 18811, seq 2, length 64
^C

This is all exactly what is expected from NAT.

The Attack

Now, what happens if the “internal” host (tasp) is pinged from the outside? In the demo setup, this is enabled by configuring another VM (deodand, 172.17.207.12) on the external network to deliver packets for the internal network to the router. The routing table looks like this:

richard@deodand:~$ ip route show
default via 172.17.207.1 dev eth0 onlink
172.17.207.0/24 dev eth0 proto kernel scope link src 172.17.207.12
172.20.211.0/24 via 172.17.207.76 dev eth0
172.25.61.0/24 dev eth0 proto kernel scope link src 172.25.61.3

The underlined route was added for the purposes of the demo; it means that packets from deodand to the “internal” network are routed via wampoon.

The test is straightforward:

richard@deodand:~$ ping -c 2 172.20.211.20
PING 172.20.211.20 (172.20.211.20) 56(84) bytes of data.
64 bytes from 172.20.211.20: icmp_seq=1 ttl=63 time=0.793 ms
64 bytes from 172.20.211.20: icmp_seq=2 ttl=63 time=0.670 ms

--- 172.20.211.20 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 0.670/0.731/0.793/0.067 ms

It works just fine. NAT has not saved us. The packets as seen by the router on the external network:

root@wampoon:~# tcpdump -npieth0 icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
21:20:27.983129 IP 172.17.207.12 > 172.20.211.20: ICMP echo request, id 21370, seq 1, length 64
21:20:27.983468 IP 172.20.211.20 > 172.17.207.12: ICMP echo reply, id 21370, seq 1, length 64
21:20:28.983255 IP 172.17.207.12 > 172.20.211.20: ICMP echo request, id 21370, seq 2, length 64
21:20:28.983418 IP 172.20.211.20 > 172.17.207.12: ICMP echo reply, id 21370, seq 2, length 64

…and on the internal network. No translation or filtering has occurred:

root@wampoon:~# tcpdump -npieth1 icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), capture size 262144 bytes
21:20:27.983180 IP 172.17.207.12 > 172.20.211.20: ICMP echo request, id 21370, seq 1, length 64
21:20:27.983455 IP 172.20.211.20 > 172.17.207.12: ICMP echo reply, id 21370, seq 1, length 64
21:20:28.983287 IP 172.17.207.12 > 172.20.211.20: ICMP echo request, id 21370, seq 2, length 64
21:20:28.983410 IP 172.20.211.20 > 172.17.207.12: ICMP echo reply, id 21370, seq 2, length 64
^C

Traffic flows without any difficulty.

ICMP’s mostly harmless though. How about TCP?

richard@deodand:~$ ssh root@172.20.211.20
The authenticity of host '172.20.211.20 (172.20.211.20)' can't be established.
ECDSA key fingerprint is SHA256:vr1YQG+gN8LdRoweSPMpvBTAI1bpZx8ckzHvBJKlP8o.
Are you sure you want to continue connecting (yes/no)? no
Host key verification failed.

Again, it works fine. Here are the the packet traces, from the outside interface:

root@wampoon:~# tcpdump -npieth1
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), capture size 262144 bytes
21:25:23.447581 IP 172.17.207.12.52670 > 172.20.211.20.22: Flags [S], seq 3306468235, win 29200, options [mss 1460,sackOK,TS val 735802076 ecr 0,nop,wscale 7], length 0
21:25:23.447979 IP 172.20.211.20.22 > 172.17.207.12.52670: Flags [S.], seq 2780826088, ack 3306468236, win 28960, options [mss 1460,sackOK,TS val 2216515 ecr 735802076,nop,wscale 7], length 0
21:25:23.449244 IP 172.17.207.12.52670 > 172.20.211.20.22: Flags [.], ack 1, win 229, options [nop,nop,TS val 735802076 ecr 2216515], length 0
21:25:23.449259 IP 172.17.207.12.52670 > 172.20.211.20.22: Flags [P.], seq 1:33, ack 1, win 229, options [nop,nop,TS val 735802076 ecr 2216515], length 32
21:25:23.449409 IP 172.20.211.20.22 > 172.17.207.12.52670: Flags [.], ack 33, win 227, options [nop,nop,TS val 2216516 ecr 735802076], length 0
[…]

…and the inside interface:

tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
21:25:23.447522 IP 172.17.207.12.52670 > 172.20.211.20.22: Flags [S], seq 3306468235, win 29200, options [mss 1460,sackOK,TS val 735802076 ecr 0,nop,wscale 7], length 0
21:25:23.447993 IP 172.20.211.20.22 > 172.17.207.12.52670: Flags [S.], seq 2780826088, ack 3306468236, win 28960, options [mss 1460,sackOK,TS val 2216515 ecr 735802076,nop,wscale 7], length 0
21:25:23.449230 IP 172.17.207.12.52670 > 172.20.211.20.22: Flags [.], ack 1, win 229, options [nop,nop,TS val 735802076 ecr 2216515], length 0
21:25:23.449230 IP 172.17.207.12.52670 > 172.20.211.20.22: Flags [P.], seq 1:33, ack 1, win 229, options [nop,nop,TS val 735802076 ecr 2216515], length 32
21:25:23.449421 IP 172.20.211.20.22 > 172.17.207.12.52670: Flags [.], ack 33, win 227, options [nop,nop,TS val 2216516 ecr 735802076], length 0
[…]

Mitigation

The issue is easy to fix, and the reason most people don’t have a problem is that a normal router configuration will do so. In this example only a couple of commands are required:

# Forward established connections...
iptables -A FORWARD -m state -j ACCEPT --state ESTABLISHED,RELATED
# ...but nothing else from outside to inside
iptables -A FORWARD -j REJECT -i eth0 -o eth1

With this in place, attempts to enter the network from outside fail:

richard@deodand:~$ ping -c 2 172.20.211.20
PING 172.20.211.20 (172.20.211.20) 56(84) bytes of data.
From 172.17.207.76 icmp_seq=1 Destination Port Unreachable
From 172.17.207.76 icmp_seq=2 Destination Port Unreachable

--- 172.20.211.20 ping statistics ---
2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 1009ms

richard@deodand:~$ ssh root@172.20.211.20
ssh: connect to host 172.20.211.20 port 22: Connection refused

This is exactly the same configuration one would use if there were not NAT in play at all.

Other Arguments

Address Enumeration

It’s sometimes argued that an attacker won’t know what addresses to target. That may be true when they start but in there are only about 1.7 million RFC1918 addresses to search, and we’re in an era when people routinely scan the entire IPv4 Internet, so an attacker probably won’t remain nescient for long.

Routing

The other argument sometimes advanced is that packets addressed to your internal network won’t reach your router in the first place, because none of the intermediate routers know how to get there.

If all is going well this is true. However, anyone who can control the ISP’s router, or the network between it and your router, can (in principle) inject and receive packets of their choice.

People who may be able to control your ISP’s router include:

You may trust your ISP’s staff vetting processes, and the security measures used by your ISP and their equipment vendors, and you may be a law-abiding citizen, who isn’t in some downtrodden minority, in a largely non-oppressive country. If all of these are true then you might well regard the risk here as very low. Personally I think it’s more sensible to add the two lines of router configuration than attempt to assess these risks.


Copyright © 2017 Richard Kettlewell

RJK | Contents