Hacking networks with Scapy (Part 1): attacking DTP/Spanning Tree

Scapy is a very powerful packet crafting, packet manipulation tool written in Python. With the help of Scapy we can perform many kind of malicious attacks against networking devices. In this post I'm going to show how we can break Layer 2 networks by attacking Dynamic Trunking Protocol (DTP) and Spanning-Tree (STP).

Scapy logo

Disclaimer: Everything I show in this post is for EDUCATIONAL PURPOSES only. Only run these attacks against your own networking equipment, or if you have permission to do so. Don't try to attack other people's network! Otherwise you can get into trouble.

You can download every Python script that I use in this post from this Github repo.

We're going to work with the following very basic topology:

Scapy topology

We have three Cisco IOSvL2 switches connected back-to-back with a trunk link configured between them. For the sake of the simplicity we have a single custom VLAN (VLAN 10), and two hosts: R2 and R3 which are Cisco IOSv routers. The ports which the routers connect to are configured as access ports with portfast enabled, the interfaces connected to other switches are configured as trunk links. This is how I configured SW1 for example:

SW1(config)#int g0/2
SW1(config-if)#switchport mode access
SW1(config-if)#switchport access vlan 10
SW1(config-if)#spanning-tree portfast edge
SW1(config-if)#int range g0/0 - 1
SW1(config-if-range)#switchport trunk encapsulation dot1q 
SW1(config-if-range)#switchport mode trunk

Very basic configuration so far. I also configured SW1 as the Root Bridge for both VLAN 1 (which is the native VLAN by default) and VLAN 10:

SW1(config)#spanning-tree vlan 1 priority 0
SW1(config)#spanning-tree vlan 10 priority 0

Now the link between SW2 and SW3 is blocked by STP, the root port on both SW2 and SW3 is G0/0 which is connected to SW1:

SW2#show spanning-tree 

VLAN0001
  Spanning tree enabled protocol rstp
  Root ID    Priority    1
             Address     0c49.aa84.0000
             Cost        4
             Port        1 (GigabitEthernet0/0)
             Hello Time   2 sec  Max Age 20 sec  Forward Delay 15 sec

  Bridge ID  Priority    32769  (priority 32768 sys-id-ext 1)
             Address     0c91.a201.0000
             Hello Time   2 sec  Max Age 20 sec  Forward Delay 15 sec
             Aging Time  300 sec

Interface           Role Sts Cost      Prio.Nbr Type
------------------- ---- --- --------- -------- --------------------------------
Gi0/0               Root FWD 4         128.1    Shr 
Gi0/1               Desg FWD 4         128.2    Shr 
Gi0/2               Desg FWD 4         128.3    Shr 


          
VLAN0010
  Spanning tree enabled protocol rstp
  Root ID    Priority    10
             Address     0c49.aa84.0000
             Cost        4
             Port        1 (GigabitEthernet0/0)
             Hello Time   2 sec  Max Age 20 sec  Forward Delay 15 sec

  Bridge ID  Priority    32778  (priority 32768 sys-id-ext 10)
             Address     0c91.a201.0000
             Hello Time   2 sec  Max Age 20 sec  Forward Delay 15 sec
             Aging Time  300 sec

Interface           Role Sts Cost      Prio.Nbr Type
------------------- ---- --- --------- -------- --------------------------------
Gi0/0               Root FWD 4         128.1    Shr 
Gi0/1               Desg FWD 4         128.2    Shr 

Remember these, because we'll change that shortly when we attack STP and send custom BPDUs. So what kind of vulnerability does this Layer 2 network have and how can we exploit it? Let's assume that the sysadmin forgot to shut down the port G0/2 on SW2 and G0/2 has default configuration, the sysadmin didn't configure anything explicitly on this port. Maybe it was just enabled by mistake or wasn't shut down properly in the first place after the switch was installed. Either way we connect our Kali machine to port G0/2 and we'll take a look what kind of malicious attacks we can run against this network. We'll see that with the Kali host we can do all kinds of evil things: we can break the whole network with DoS attacks, or perform a Man-in-the-Middle (MiTM) attack and intercept all of the communication between R2 and R3, even if they are in a different VLAN!

Attacking DTP - Establishing a trunk between SW2 and the Kali host

Right after we connect the Kali machine to SW2 we can capture packets with Wireshark:

Capturing DTP packets from SW2
Kali host receives DTP packets from SW2

At this point the attacker doesn't know anything about the hosts or the VLANs in the network. But notice that since SW2 operates with the default configuration on port G0/2: DTP is enabled, and that is something we can exploit. By default every interface is configured with switchport mode dyanamic auto. So if we configure the neighbor port with switchport mode dynamic desirable or switchport mode trunk explicitly with DTP enabled, we can form a trunk link between the switches. SW2 didn't receive any DTP messages on the interface G0/2, so it operates as an access port at this moment. But if we could send a DTP packets from the Kali host and set the Administrative state to Desirable, then we could establish a trunk link between SW2 and the Kali host. First we'll use Scapy to capture a packet with the destination MAC address of 01:00:0c:cc:cc:cc, which is the well-known MAC address of DTP. So let's start the Python interpreter in interactive mode with root privileges by issuing sudo python3, and we run the following commands:

Sniffing DTP packet with Scapy
Sniffing DTP packet with Scapy

Note: CDP also uses the 01:00:0c:cc:cc:cc well-known MAC address, and CDP is also enabled by default, so we might capture a CDP packet first instead of DTP, in that case we simply run the sniff command again. CDP packets are sent much less frequently than DTP, but there is chance that CDP will be captured first, keep that in mind.

With the last command I display the content of the captured DTP packet. Next we're going to rewrite some fields in this DTP packet and resend it to SW2. Technically we could craft a new packet from scratch, but it's much easier to just rewrite the relevant fields in an existing packet.

Establish trunk link with Scapy
Sending the modified DTP packet to SW2 to establish the trunk link

First I change the source MAC address, because we just cannot resend the DTP packet with the same source MAC, use whatever value you want it doesn't really matter. With the second line I change the Administrative state to Desirable, this is equivalent to the command switchport mode dynamic desirable. With the third line I change the trunk type to 802.1Q, because by default we would form an ISL trunk. This is equivalent to the command switchport trunk encapsulation dot1q. Again I display the content of the modified DTP packet and with the last sendp command I send it to SW2. This is the same DTP packet in Wireshark we send to SW2:

Kali sends this DTP packet
Kali sends this DTP packet to SW2: we changed the source MAC, the Administrative Status to Desirable, and the Trunk Type to 802.1Q

Now SW2 forms a trunk with the Kali host:

SW2#show interfaces trunk

Port        Mode             Encapsulation  Status        Native vlan
Gi0/0       on               802.1q         trunking      1
Gi0/1       on               802.1q         trunking      1
Gi0/2       auto             n-802.1q       trunking      1

Port        Vlans allowed on trunk
Gi0/0       1-4094
Gi0/1       1-4094
Gi0/2       1-4094

Port        Vlans allowed and active in management domain
Gi0/0       1,10
Gi0/1       1,10
Gi0/2       1,10

Port        Vlans in spanning tree forwarding state and not pruned
Gi0/0       1,10
Gi0/1       1,10
Gi0/2       none

The mode auto, and the n-802.1q encapsulation basically mean that the trunk was negotiated dynamically with DTP and wasn't configured statically. The none in the last line means that Spanning-Tree has not converged yet on this port. We didn't configure Portfast on this port, so it has to go through the Listening and Learning states, but after about 30 seconds this port will also be able to forward frames and it'll show 1,10 instead of none.

Now we sent one DTP packet, in order to maintain the trunk we must send DTP packets periodically, otherwise it'll simply time out (after ~300 seconds). By default Cisco switches send DTP packets every 30 seconds, so I created a loop which sends the DTP packets indefinitely and we wait 30 seconds between the packets [dtphack.py].

Maintaining the trunk
We need to run the dtphack.py script in the background, and send DTP packets regularly, otherwise the trunk will time out

Discovering VLANs and hosts

Now we can receive tagged frames, and we receive every Broadcast frame (ARP messages for example), so we can discover the VLANs and possibly the hosts as well just by sniffing the packets with Wireshark:

Receiving tagged broadcast frames
Sniffing ARP packets on the trunk link, gathering information

Remember that Cisco uses Per-VLAN Spanning-Tree (PVST+, or Rapid PVST+) by default, so just by capturing the STP packets we can discover which VLANs are used in the network. In this case above we learned the IP address and the MAC address of R2 (10.0.0.2) and also we know that R2 is in VLAN 10. If you are not lucky with ARP and Broadcast messages, later I'll show how we can discover hosts with nmap ARP scan.

Creating subinterface on the Kali host

Let's say we want to attack the Spanning-Tree in VLAN 10 and send tagged frames to the hosts in VLAN 10. To be able to send 802.1Q tagged frames from the Kali host we've to create a subinterface and edit the /etc/network/interfaces file:

Creating subinterface
Creating a subinterface (eth0.10) to send tagged frames

This is very similar how we create the subinterface on a Cisco router, we use eth0.10 for VLAN 10. The IP address doesn't really matter here, if you don't know which subnet  the hosts use, use whatever you like. We won't send IP packets at this point because we'll attack STP. After editing the file, we need to restart the networking service on the Kali host. In this case I like just restarting the whole machine, but after booting up we've to start the dtphack.py script again. Remember we need to send the DTP frames constantly to maintain the trunk.

Discovering hosts with nmap

Now that we can send tagged frames we can discover hosts in VLAN 10 with nmap ARP scans. nmap sends out ARP Requests for the hosts or subnets we specify. So we have to know or at least have a guess which subnets the hosts might use. Scanning the whole 10.0.0.0/8 network for example takes a lot of time and also it'll be very noisy. In this case I run an ARP scan in the 10.0.0.0/24 subnet, and I was able to discover the two hosts in VLAN 10:

nmap ARP scan
Discovering hosts with nmap ARP scan

It's important to specify the interface explicitly with the -e eth0.10 flag, if you use the eth0 interface, you won't be able to send tagged frames and discover the hosts.

Attacking Spanning-Tree - Changing the Root Bridge

Now if we capture packets on the eth0.10 interface in VLAN 10 we can discover that the switches run Rapid PVST+ and information about the Root Bridge:

Receiving BPDUs in VLAN 10
Capturing Rapid PVST+ packets on eth0.10 interface in VLAN 10

Rapid PVST uses the destination MAC address of 01:00:0c:cc:cc:cd, now just as we've done with DTP hacking, we capture the first BPDU with the help of Scapy:

Sniff BPDU in VLAN 10 with Scapy
Capturing a Rapid STP BPDU with Scapy and displaying its content

We can see that we set the priority to 0 on the Root Bridge previously: the rootid is basically the Priority + VLAN ID (0 + 10). So with the same rootid we need a lower rootmac to become the Root Bridge, so we're going to change the rootid to a numerically lower MAC address. Also we need to rewrite the source MAC address and bridgemacto something else:

Creating superior BPDU for VLAN 10
Rewriting fields in the BPDU we've captured with Scapy

Our bridgeid, and pathcost doesn't really matter, to take over the role of the Root Bridge we need a lower rootid or a lower rootmac with the same rootid. Remember that we have to specify the eth0.10 interface when we send out the packet, otherwise it won't be tagged with VLAN 10. This is the same packet in Wireshark:

BPDU sent out by the Kali host
Superior BPDU sent out by the Kali host to SW2

I started a debug on SW2 before sending out the packet above, let's take a look what happened on SW2:

SW2#debug spanning-tree events 
Spanning Tree event debugging is on

*Oct  9 20:39:13.669: RSTP(10): updt roles, received superior bpdu on Gi0/2 
*Oct  9 20:39:13.669: RSTP(10): Gi0/2 is now root port
*Oct  9 20:39:13.669: STP[10]: Generating TC trap for port GigabitEthernet0/2
*Oct  9 20:39:13.669: RSTP(10): Gi0/0 is now designated

*Oct  9 20:39:19.669: RSTP(10): Gi0/2 rcvd info expired
*Oct  9 20:39:19.669: RSTP(10): updt roles, information on root port Gi0/2 expired
*Oct  9 20:39:19.669: RSTP(10): we become the root bridge
*Oct  9 20:39:19.669: RSTP(10): Gi0/2 is now designated
*Oct  9 20:39:19.671: RSTP(10): updt roles, received superior bpdu on Gi0/0 
*Oct  9 20:39:19.671: RSTP(10): Gi0/0 is now root port
*Oct  9 20:39:19.671: STP[10]: Generating TC trap for port GigabitEthernet0/0

We've successfully changed the Root port of SW2, now the link between SW1 and SW3 is blocking, and all of the traffic flows through SW2. But notice that 6 seconds after receving the BPDU, SW2 considers its neighbor lost on G0/2 and changes its port states again. So just as Cisco switches, the Kali host also has to send out its BPDUs periodically every 2 sends to maintain its role, so I created loop for that in my script stphack.py.

If we take a look at the STP port states of SW1 for example, we can see that also SW1 (the previous Root Bridge) acknowledges the Kali host as the Root Bridge:

SW1#show spanning-tree 
<output omitted>
VLAN0010
  Spanning tree enabled protocol rstp
  Root ID    Priority    10
             Address     0000.0000.0001
             Cost        8
             Port        1 (GigabitEthernet0/0)
             Hello Time   2 sec  Max Age 20 sec  Forward Delay 15 sec

  Bridge ID  Priority    10     (priority 0 sys-id-ext 10)
             Address     0c49.aa84.0000
             Hello Time   2 sec  Max Age 20 sec  Forward Delay 15 sec
             Aging Time  300 sec

Interface           Role Sts Cost      Prio.Nbr Type
------------------- ---- --- --------- -------- --------------------------------
Gi0/0               Root FWD 4         128.1    Shr 
Gi0/1               Desg FWD 4         128.2    Shr 
Gi0/2               Desg FWD 4         128.3    Shr Edge 

Launching DoS attack exploiting the Spanning-Tree

Now we can wreak more havoc if we modify the stphack.py script: we increase the interval between the superior BPDUs to a little bit more than 6 seconds, I set it to 6.1 seconds in my example [stpdos.py]. What is going to happen? The Spanning-Tree Root Bridge and the port states will be changing constantly. After 6 seconds SW2 deems the Kali host dead, and the switches elect SW1 as the new Root Bridge, 0.1 second later SW2 receives a superior BPDU from the Kali host and the Kali host will be the new Root Bridge again, every switch changes the port states again, and this process keeps repeating constantly... Rapid STP is fast, but it is not that fast, in my topology in GNS3 this stpdos.py script resulted in complete Denial of Service. R2 and R3 couldn't communicate with each other. This might be different in a real environment, but I'm sure we can cause serious disruptions, especially if the users run real-time applications such as voice calls, or video streaming. Not to mention the original 802.1D STP: if the switches run the original STP the switches have to go through the Listening and Learning states (2 x 15 seconds) which could result in complete Denial of Service.

Mitigation techniques

Disabling DTP

How can we protect our devices against these malicious attacks I showed so far? First of all we should disable DTP. DTP is enabled by default, the administrative state of each port is dynamic auto by default, if the switch receives a DTP message with desirable or static trunk state from its neighbor, they'll form a trunk. This was the state of the interface G0/2 on SW2 before I sent out the DTP packets from the Kali host:

SW2#show dtp interface g0/2
DTP information for GigabitEthernet0/2:
  TOS/TAS/TNS:                              ACCESS/AUTO/ACCESS
  TOT/TAT/TNT:                              NATIVE/NEGOTIATE/NATIVE
  Neighbor address 1:                       000000000000
  Neighbor address 2:                       000000000000
  Hello timer expiration (sec/state):       25/RUNNING
  Access timer expiration (sec/state):      never/STOPPED
  Negotiation timer expiration (sec/state): never/STOPPED
  Multidrop timer expiration (sec/state):   never/STOPPED
  FSM state:                                S2:ACCESS
  # times multi & trunk                     0
  Enabled:                                  yes
  In STP:                                   no

The TOS (Trunk Operative State) and the TNS (Trunk Negotiation State) is ACCESS meaning that the switchport operates in access mode at the moment, because it didn't receive any DTP messages yet. The TAS (Trunk Administrative State) is how the port configured (switchport mode dynamic auto). The TOT (Trunk Operative State) and TNT (Trunk Negotiation State) is NATIVE meaning that the switches prefer ISL to 802.1Q, that's why I changed it to 802.1Q in my script. And this was the state of the interface after Kali host started sending out the DTP messages:

SW2#show dtp interface g0/2
DTP information for GigabitEthernet0/2:
  TOS/TAS/TNS:                              TRUNK/AUTO/TRUNK
  TOT/TAT/TNT:                              802.1Q/NEGOTIATE/802.1Q
  Neighbor address 1:                       0C91A2010002
  Neighbor address 2:                       000000000000
  Hello timer expiration (sec/state):       29/RUNNING
  Access timer expiration (sec/state):      299/RUNNING
  Negotiation timer expiration (sec/state): never/STOPPED
  Multidrop timer expiration (sec/state):   never/STOPPED
  FSM state:                                S6:TRUNK
  # times multi & trunk                     0
  Enabled:                                  yes
  In STP:                                   no

How can we disable DTP? Configure the interface as static access explicitly: switchport mode access. Or issue the switchport nonegotiate command:

SW2(config-if)#switchport nonegotiate 
Command rejected: Conflict between 'nonegotiate' and 'dynamic' status on this interface: Gi0/2
SW2(config-if)#switchport mode access 
SW2(config-if)#switchport nonegotiate 

But this only works if we configure the port state statically (access or trunk), it doesn't work if the switchport is in dynamic mode (auto or desirable). But the switchport mode access command already disables DTP, so this command only makes sense if you configure switchport mode trunk on the switchport.

BPDU Guard

How can we protect our Spanning-Tree? I've already showed in this post how Root Guard works, BPDU Guard is very similar. It is configured on a switchport which we want to protect and that is only used to connect host devices:

SW2(config)#int g0/2
SW2(config-if)#spanning-tree bpduguard enable 

Hosts and servers don't send BPDUs, only switches do. If the switch receives a BPDU (any kind of BPDU) on this port, it puts the port in error-disabled state, effectively it shuts down the port. These are the Syslog messages we receive when we send out the BPDU from the Kali host.

%SPANTREE-2-BLOCK_BPDUGUARD: Received BPDU on port Gi0/2 with BPDU Guard enabled. Disabling port.
%PM-4-ERR_DISABLE: bpduguard error detected on Gi0/2, putting Gi0/2 in err-disable state
%LINEPROTO-5-UPDOWN: Line protocol on Interface GigabitEthernet0/2, changed state to down
%LINK-3-UPDOWN: Interface GigabitEthernet0/2, changed state to down

Now G0/2 is in err-disabled state:

SW2#show interfaces status | include conn|err-dis|Status
Port      Name               Status       Vlan       Duplex  Speed Type 
Gi0/0                        connected    trunk        auto   auto unknown
Gi0/1                        connected    trunk        auto   auto unknown
Gi0/2                        err-disabled 1            auto   auto unknown

And it stays in this state until we issue a shut and no shut on the interface, or we enable the errordisable recovery feature globally:

SW2(config)#errdisable recovery cause bpduguard 

SW2#show errdisable recovery | exclude Disabled
ErrDisable Reason            Timer Status
-----------------            --------------
bpduguard                    Enabled

Timer interval: 300 seconds

Interfaces that will be enabled at the next timeout:

Interface       Errdisable reason       Time left(sec)
---------       -----------------       --------------
Gi0/2                  bpduguard          268

Now after 300 seconds the switchport will be automatically enabled again.

In the next post we'll poison the ARP cache of R2 and R3, and perform a MiTM attack. I'll also show how we can run a CAM table overflow attack (MAC flooding), DHCP Starvation, and VLAN hopping.