Scanning firewalls for differences in IPv4 and IPv6 rules

Both IPv4 and IPv6 firewalls require separate rule sets and thus more administrative work. I scanned Alexa's Top 1M websites and tried to spot differences in the firewall's configurations.

The questions I had on my mind were:

  1. Are there administrators who configured iptables, but not ip6tables?
  2. Can I spot differences in their configurations?

Preparations

I thought that the Alexa Top 1M websites dataset would be suitable for answering the questions. Let's download the dataset and extract all domains from it:

$> wget http://s3.amazonaws.com/alexa-static/top-1m.csv.zip
$> unzip -p top-1m.csv.zip top-1m.csv | cut -d"," -f 2 > domains.txt 

The next step was to request all A (IPv4) and AAAA (IPv6) addresses and their corresponding PTR records. PTR records are "reversed" A/AAAA records. Usually a name resolves to an ip address, but the ptr record resolves an IP address to a hostname. Additionally, all domains which had less than one IPv4 or IPv6 address were removed.

#!/usr/bin/python
import dns.resolver
import dns.query
import dns.zone
import json 
from IPy import IP 
from multiprocessing import Pool

INPUTFILE = "domains.txt"
OUTPUTFILE = "dns-entries.json"
LOGFILE = "dns-reqs.log"
PROCESSES = 20

def get_host(ip):
  ip = ip.strip()
  try:
    ptr_query = dns.resolver.query(ip,'PTR')
    for p in ptr_query.rrset:
      ptr = str(p)[:-1]
      if ptr is None or ptr == "":
        continue
      return ptr
  except:
    pass
  return None 

def get_v4_v6(domain):
  domain = domain.strip()

  dns_data = {
    'domain': domain,
    'v4': [],
    'v4_rev': [],
    'v4_host': [],
    'v6': [],
    'v6_rev': [],
    'v6_host': []
  }

  try:
    a_query = dns.resolver.query(domain,'A')
    for a in a_query.rrset:
      a_ip = str(a)[:-1]
      if a_ip is None or a_ip == "":
        continue
      dns_data['v4'].append(a_ip)
      rev = reverseName(a_ip)
      dns_data['v4_rev'].append(rev)
      dns_data['v4_host'].append(get_host(rev))
  except Exception as e:
    pass

  try:
    aaaa_query = dns.resolver.query(domain,'AAAA')
    for aaaa in aaaa_query.rrset:
      aaaa_ip = str(aaaa)[:-1]
      if aaaa_ip is None or aaaa_ip == "":
        continue
      dns_data['v6'].append(aaaa_ip)
      rev = reverseName(aaaa_ip)
      dns_data['v6_rev'].append(rev)
      dns_data['v6_host'].append(get_host(rev))
  except Exception as e:
    pass

  if len(dns_data['v4']) < 1 or len(dns_data['v6']) < 1:
    dns_data = None 

  LOGFILE.write("Finished: " + domain + ":" + json.dumps(dns_data) +"\n")
  LOGFILE.flush()

  return dns_data

def reverseName(ip):
  ip = IP(ip)
  return ip.reverseName()[:-1]

def main():
  global PROCESSES, LOGFILE, OUTPUTFILE, INPUTFILE


  domains = open(INPUTFILE, "r").readlines()
  OUTPUTFILE = open(OUTPUTFILE, "w")
  LOGFILE = open(LOGFILE, "w")

  pool = Pool(processes=PROCESSES)

  data = pool.map(get_v4_v6, domains)

  data = filter(lambda x: x != None, data)

  OUTPUTFILE.write(json.dumps(data))
  OUTPUTFILE.flush()

if __name__ == '__main__':
  main()

I had to wait a couple of hours for this process to finish and I hope that my dns servers didn't blacklist me and/or drop my queries.
The first thing that surprised me was that only a merely 56k domains have an AAAA entry / IPv6 address.

>>> import json
>>> d = json.load(open('dns-entries.json'))
>>> len(d)
56722

Anyway, the number will shrink far more after applying the following criteria:

  • Less than one PTR record (hostname) for IPv4 / IPv6 address
  • All hostnames are not None
  • IPv4 and IPv6 hostnames match.

I decided to use the last criterion, because I wanted to make sure that I will scan the firewall of the same host. I assumed that if the PTR records for a IPv4/IPv6 address match that both IPs point to the same system. Otherwise the firewall results wouldn't be comparable, because IPv4 and IPv6 isn't handled by the same server.

The following python script applied the criteria and created a new dataset:

import json 
data = json.load(open("dns-entries.json","r"))

data_not_empty = filter(lambda x: len(x['v4_host']) > 0 and len(x['v6_host']) > 0, data)
data_not_none = filter(lambda x : 'None' not in ''.join(map(str,x['v6_host'])) and 'None' not in ''.join(map(str,x['v4_host'])), data_not_empty) 
data_same_host = filter(lambda x: x['v6_host'] == x['v4_host'], data_not_none)

f = open("dns-entries-cleaned.json","w")
f.write(json.dumps(data_same_host))
f.close()

We're left with a total of 387 sites.

Running nmap

Now I wanted to run nmap against the IPv4 and IPv6 address. I first tried to use the python-nmap plugin, but it didn't work within a process pool, so I used a simple subproces.Popen(). Another issue was IPv6 connectivity, because my ISP does not provide it. I used a DigitalOcean.com VM to fix this. They're also fine with nmap scans as long as you take responsibility for incoming abuse mails:

Port scanning is not explicitly against our ToS, however you are responsible for any abuse complaints filed against your account that are sent in by other administrators.

[...]

I'd say that we use common sense and normally a single complaint wouldn't get your service suspended. However the details and specific situation can change the outcome. I think we're very fair and we try our best to be as accommodating as possible.

DigitalOcean Support

I first started with a -sT -sU scan, but that took ages, so I aborted it and decided to go with the -F (fast scan) option.

import subprocess 
import json

def scan_v4(domain):
	ipv4 = domain['v4'][0]
	scan = subprocess.Popen("nmap -PN -oG - -F " + ipv4, shell=True)
	domain['v4_nmap'] = scan
	return domain 

def scan_v6(domain):
	ipv6 = domain['v6'][0]
	scan = subprocess.Popen("nmap -6 -PN -oG - -F " + ipv6, shell=True)
	domain['v6_nmap'] = scan
	return domain 

data = json.load(open("dns-entries-cleaned.json","r"))
new_data = []

for item in data:
	item = scan_v4(item)
	item = scan_v6(item)
	new_data.append(item)

with open("scan-data-fast.json","w") as f:
	f.write(json.dumps(new_data))

So all we have now is a json file with nmap scans for the IPv4/IPv6 firewalls of a host.

Results

Let's have a look at the results. Here comes one last python script to parse the open ports from the nmap scans and remove sites where both scans have the same results.

import json 
import re

d = json.load(open("scan-data-fast.json"))

RE_OPEN_PORTS = re.compile("(\d+)/open")

def get_open_ports(nmap_scan):
	p = []
	for m in RE_OPEN_PORTS.findall(nmap_scan):
		p.append(int(m))
	return p

def parse_nmap(item):
	item['v4_ports'] = get_open_ports(item['v4_nmap'])
	item['v6_ports'] = get_open_ports(item['v6_nmap'])
	return item

d = map(parse_nmap, d)
d = filter(lambda x: x['v4_ports'] != x['v6_ports'], d)

print len(d)
for item in d:
	print "===== {} ====".format(item['domain'])
	print get_open_ports(item['v4_nmap'])
	print get_open_ports(item['v6_nmap'])

Here are some things that caught my eye and are going to answer the second question (Can I spot differences in their configurations?): There are indeed differences in the firewall configurations.

The output below has open IPv4 ports in the first row and IPv6 in the second.

No anti-portscan protection

Some hosts seem to use some kind of anti-portscan techniques (all ports reported as open), but this isn't configured for the IPv6 firewall.

[7, 9, 13, 21, 22, 23, 25, 26, 37, 53, 79, 80, 81, 88, 106, 110, 111, 113, 119, 135, 139, 143, 144, 179, 199, 389, 427, 443, 444, 465, 513, 514, 515, 543, 544, 548, 554, 587, 631, 646, 873, 990, 993, 995, 1025, 1026, 1027, 1028, 1029, 1110, 1433, 1720, 1723, 1755, 1900, 2000, 2001, 2049, 2121, 2717, 3000, 3128, 3306, 3389, 3986, 4899, 5000, 5009, 5051, 5060, 5101, 5190, 5357, 5432, 5631, 5666, 5800, 5900, 6000, 6001, 6646, 7070, 8000, 8008, 8009, 8080, 8081, 8443, 8888, 9100, 9999, 10000, 32768, 49152, 49153, 49154, 49155, 49156, 49157]
[22, 80, 81, 443]

So nmap'ing the IPv6 address is quite useful in this scenario.

No open IPv6 ports

It looks like some hosts have an IPv6 address, but block all incoming traffic. So where's the point in adding it to the DNS in the first place?

[21, 22, 25, 80, 110, 143, 443, 465, 993, 995]
[]

or

[80, 81, 443]
[]

Not all IPv6 ports open

Either some services can't handle IPv6 and/or the ports are closed for the IPv6 network.

[21, 22, 25, 80, 443]
[21, 80, 443]

Conclusion

To my surprise it looks like the answer to the first question (Are there administrators who configured iptables, but not ip6tables?) is "No".
Nevertheless, it's interesting to see that there are differences between the configurations of the IPv4/IPv6 firewalls. However, my nmap scan results base on the limited set of ports ( -F) and a very limited set of test systems, so maybe one can find more interesting stuff by analyzing more systems in more detail.

Update:
It looks like my colleague Patrik did similar research with similar results. I recommend watching his talk, although the audio is a bit messed up :(

-=-