Repository: secdev/scapy

Issue:
Problem in handling Radius EAP Fragmentation
## Brief description
When handling RADIUS-AVP fragmented EAP packets the fragmentation detection might not work, since random values may be checked.

#### Environment

-   Scapy version: latest master branch
-   Python version: `2.8.5`
-   Operating System: `Arch Linux`

#### How to reproduce

Attached is a python script and a capture file with a single radius packet. The packet consists of multiple AVP fields, with four of them representing an EAP message, that is fragmented due to AVP length constrains.
When parsing this packet, scapy decides if the eap message is fragmented, by checking the third and fourth byte of the EAP fragment. This works if the EAP Packet is only fragmented into two AVP fields, since then these two bytes actually represent the length of the EAP fragment itself. After the second fragment, the decision if the AVP field is a fragment is based on random data in the EAP message. 

The code in radius.py:
```class _EAPPacketField(PacketLenField):

    """
    Handles EAP-Message attribute value (the actual EAP packet).
    """

    def m2i(self, pkt, m):
        ret = None
        eap_packet_len = struct.unpack("!H", m[2:4])[0]
        if eap_packet_len < 254:
            # If the EAP packet has not been fragmented, build a Scapy EAP
            # packet from the data.
            ret = EAP(m)
        else:
            ret = conf.raw_layer(m)
        return ret

```
This could be fixed by using pkt.len as length indicator.

However, if this is done, the last EAP Fragment is considered as an EAP message with size < 256 and therefore parsed as an EAP Packet. This makes no sense, since it is only the last fragment of the whole Packet and therefore the parsing results in random values.

Sorry if the issue may be a bit confusing, since i am rather new to scapy. I try to debug a little bit further and hopefully i can issue a PR with a fix. Any suggestions how this kind of fragmentation is usually handled and where to look further are much appreciated.

#### Actual result
```
###[ Ethernet ]### 
  dst       = 00:00:00:00:00:00
  src       = 00:00:00:00:00:00
  type      = IPv4
###[ IP ]### 
     version   = 4
     ihl       = 5
     tos       = 0x0
     len       = 1096
     id        = 34276
     flags     = 
     frag      = 0
     ttl       = 64
     proto     = udp
     chksum    = 0xf2be
     src       = 127.0.0.1
     dst       = 127.0.0.1
     \options   \
###[ UDP ]### 
        sport     = radius
        dport     = 39306
        len       = 1076
        chksum    = 0x248
###[ RADIUS ]### 
           code      = Access-Challenge
           id        = 28
           len       = 1068
           authenticator= 255ba5110bdc8f94f2e0018aac4e498e
           \attributes\
            |###[ EAP-Message ]### 
            |  type      = EAP-Message
            |  len       = 255
            |  \value     \
            |   |###[ Raw ]### 
            |   |  load      = '\x01\x97\x03\xec\r\xc0\x00\x1a\x15\xd0`\xa9P2\xdd\x8e\x1ff\xbc\x1f\x1b\xb2\xc3Y~dIF\xdb;i\xaf\x01F\xa2\xeb\x88\xde\xbd\xb1\xa3SG\xa7\xab;\xf6\xa9P\xd7\x8d\xfc\xfb\xcf\xf5r\xf7u\x15\x0e\xc8w^\n\xb6\xe4\x9b\xd3[\xb6\x10\xde\x0b\x02\x88\x98\xbbJ&"\xb5\x99\xa6\xbf(A"\xf4\xaf6\x82j\xb3\xa9=\xf7\xb8\xf9\xfb\xe1\xdf\x81<U\x17\t7\x0e\xe9\xcfg\xac6p\x9c\xb3\x94Y`\xdb\xe4\x90;G\xde\x16=\x89\x9b\x87l=8\xdd\xee\xd1\xe7e:\xd3*\xe7\xe7\xd1\xd83_F\xf8\xbeWf\xd1+\xa4\xddS\x15p\xa67|Y\xacgN\xccr\xa7\x99\xa6=\x0c\xd8*^\xde\xff\x12\xa1\xe27\x07\xbf\xdf\x1a\x88Kem\x86S\xf6)(\xe9\xc4Q/\x0f\xd9\xc2\x11\xcf\xbcg\x1c\x82%\xa5\x01\xb2\xb9\xddW\\\xd9\xf0\x10\xe4rO\xa9op\xecu\x1czI,\x9d}\xf1\x00\x1b\xef(*Z\x02\xbe:\x0f\x03,\xd5iB\x95{\r\xd6'
            |###[ EAP-Message ]### 
            |  type      = EAP-Message
            |  len       = 255
            |  \value     \
            |   |###[ EAP ]### 
            |   |  code      = 120
            |   |  id        = 129
            |   |  len       = 103
            |   |  type      = 75
            |###[ Padding ]### 
            |     load      = 'O\xffc\x11\xc4\xc1\x11\x1d\x9f\xaf\x98\x0475\xbf\xd6\xe0T\xf9T\x08R,zF\xe9\xf2\x03\n*\x0c?\x81w \x91\xdb\xf0\x99\xe4\xf1\xbe\x95Vs\x8b|\x8eP\xd1[\xba%\xe2 \x9dF<\xbeJ\x19\xcd\xa9\xac\x98\xb6[x\xeeU#\x151\x1c\x91\\=\xf0.\xbc\xbd7, R\xda\xffF\x94C\x1e\x91S\xb7\x8f{\x18\xbd\xcce\xe0\xfb\xd1J\xe0E@\xa4\xbc\xa0\xccD\xa3_\xcfO\x0b\xc9sb"W/\x8b"\xdcl\xf2\x87\x89\x1c\xaf\x0b1}f\xf9\x8a\x02\x97\xde\x0c\xba\x9aK\x1f\xa3s-\xc7\x19(wY\x10q\x04\xd8u\xc6\x86^]x\x90\x88\xf9}\xcc\xd1Y8\xd6\xdd=xy(\xca\x85t\x1a\xe4\xff\x104\x88(\x13\x08\x82\'\x8a\xd3\xf4\x7f+N\x02e\x91<\x84@,\xd4\xe7P\xe9$\x16L~k`\x82\xf9\xcd\xffp\xa4\xf28VCN\x05\tG\x94\xbc\x8a\xe6\xf6\xa2 u\xac\xc8\xe6\xfd\xb0\xc1\x9e\xa1S\x15{\xabW\xaf\x89\x82\x90\xceO\xf7\xe5\xb6=\x00\xbc\x95\x10A\xf6\xd1\xa2\x88\xdat\x0f%W\xaa\x84\x04Z\x95x]\xcc\xd9\x16\xfe\n\xc6k\xcby\xc2\',\xf5\xb3\xder-\x01\x93\x0f\xb6\n\xd3\xed\x838\x00<\xc8/\xc8\x1a\xf6;\xf2W\xbd\xf5\x04\xd6\x9d\xf2\xef\xf7fo\xae3.\'Q\xdb\xabB\x94\x93\x1bD\x08\x13\x9a\x13\xd8\xbd/\x14\x93\x0et\xea)\xe9t\xa4p5@\x0e\xabdD\xa1\x9a\xa6}\xc6 {\xf0\xd3\xaa)\x96T^\xa2#\x05\xd5s\x85?>m\xd5<\xd6\x16.Z6\xa1KD\xe4\x96\xa5\xbc\xc3f\xb4\xa7\xa2\xd6\x02\x99\xf6\xc3\xb2\x0f*\xc9\xa7\xceH\xa6\x9b\t\x1b\xe7\xbax\x8f\xf5\xf1?\xfcG,y\xcd\xb7\xaa\x10\xe2\xd8\t\xca\xce\xd4S\xe6\xd7\x0b\x91\x9d\xeb\x1f\x9c\xd6,Q\xb3z\x08\xb9M\xf8n\xe8\x94o\xcf\xcb\xbe\xed\x01\xa8R\xe4-\xbc&\xf5\xa1\xef4\x87\xd0\xdf\xf4f\xe6\x00~\x10\xab\xb4\xec\x14\x12h\xc2\xe5~zVFg\xef\xb8P\x12n\x14\xd1\x9f\xa8\xf3\t\xe4\xc0\x82\xd6\x07AB\xd5\xf5\x18\x12\x19\xd6\x9eX\x05A\x93jo\x9a\t:\xa9g_\xc2'
            |###[ Radius Attribute ]### 
            |  type      = 247
            |  len       = 178
            |  value     = 'qO\xf0\xe5\xfe\xc8\xa3_F\xfcef\xfb\xcc\xe8\x14\xca\x8d~=\xe4a\x92\x84\x12\x005\x89\xc8a"/m]7\xdf\xf9n\xf0\x01\xd4\xc9+B\xdf\x03z\'\xac\x19CF\xd6~\r\x8eQ\xe7\x02\x07\xf1\xf6\xac/\x93\xa8\xbfZ\x10\xc4\xc8$\x08\xe4\x0bL0\xdd\xef\xc4)\xb9\x88\xa4\xad\xbb\xa9E\xa2G&}H\x031*\x01I\x02\x0e\xfdK\x05C|\xc7\xbb\x97\x15F\xdd\xe3\'u5)\xde)\xddGN7\x05\x98y\xa7\x0c\rY0\xbc\xba\xfcW\xa57\x05\\\xda\xd4P\x04\xd0;t\x92\xde\xbf\x8c\xf5\xb3\xe6m\xab\x1f7\xbf\xb6w\x04\x99\x7f\xb9\xb0j\xc1\x14ZT\xf1x\xd9\x15\x87M\xcd'
            |###[ PMIP6-Home-LMA-IPv4-Address ]### 
            |  type      = PMIP6-Home-LMA-IPv4-Address
            |  len       = 28
            |  value     = 120.92.212.44
            |###[ Radius Attribute ]### 
            |  type      = PMIP6-Visited-IPv4-HoA
            |  len       = 59
            |  value     = 'k\xb8\xc0#w\xa6\xeft\xf6\xcb\xde\xfb<_r \xdc\xce\x90\xaa\xe5\x16\x88\x1e\xe1D\xe4\xe6\xce\x16#\x9aPG\xdc\x0e?{N\xab\xb1\xa5\x81\x13\xf6\xd8\xf7\x13\xf2\xcc5.,\x1eq\x16\xe3'
            |###[ Raw ]### 
            |  load      = '\x0c\x8c\x1a=\x07'

```
#### Expected result
![wireshark](https://user-images.githubusercontent.com/10732757/94190589-96706600-feac-11ea-90c7-dc6f20dd13a4.png)


#### Related resources
test.py.txt: Simple testscript with binary repr. of a packet that triggers the error.
test.txt: pcap file with the packet

[test.py.txt](https://github.com/secdev/scapy/files/5278694/test.py.txt)
[test.txt](https://github.com/secdev/scapy/files/5278690/test.txt)





Discussion:
I was able to build a rather ugly fix, that uses two global variables. One variable checks if the last segment belongs to a fragmented message and the second variable stores the fragments in a buffer, before returning the reassembled message as an EAP message.

It is also kinda wrong, since the last fragment now consists of the whole EAP message, instead of only the fragment, but it may clear up what the problem is.

```
diff --git a/scapy/layers/radius.py b/scapy/layers/radius.py
index bc1cd9a7..4444fb67 100644
--- a/scapy/layers/radius.py
+++ b/scapy/layers/radius.py
@@ -993,7 +993,8 @@ class RadiusAttr_NAS_Port_Type(_RadiusAttrIntEnumVal):
     """RFC 2865"""
     val = 61
 
-
+frag = False
+frag_buf = b''
 class _EAPPacketField(PacketLenField):
 
     """
@@ -1001,13 +1002,21 @@ class _EAPPacketField(PacketLenField):
     """
 
     def m2i(self, pkt, m):
+        global frag, frag_buf
         ret = None
-        eap_packet_len = struct.unpack("!H", m[2:4])[0]
+        eap_packet_len = pkt.len
         if eap_packet_len < 254:
             # If the EAP packet has not been fragmented, build a Scapy EAP
             # packet from the data.
-            ret = EAP(m)
+            if not frag:
+                ret = EAP(m)
+            else:
+                ret = EAP(frag_buf+m)
+                frag_buf = b''
+            frag = False
         else:
+            frag = True
+            frag_buf += m
             ret = conf.raw_layer(m)
         return ret

```
Thanks for the bug report ! (and sorry for the delay)

As you can probably guess, global vars aren't acceptable but it's nice that you got a PoC working !
Yea I thought so. I am a little bit lost, since we probably need to keep a state somehow. On the bright sight the whole message should appear in a single radius packet. Can you give me a hint how such things are usually solved within scapy ? 

I assume the best solution would be to display all EAP fragments as a single field and an additional field with the reassembled EAP message. But this would involve some sort of back tracking after the last fragment is encountered.

This would involve to keep a state at the level of the radius AVP‘s. E. G.:

- If the first EAP Avp has length 255. parse all future EAP avp‘s as fragments
- else parse the avp directly as EAP

This would at least fix the bug. 

Additionally we need to reassemble the EAP message. Should this be done on dissect time or on display time ?
Hi there can  you assign this to me for that i can resolve this one
Feel free to provide a fix, no one is currently working on this.
That would be great. If you need additional information/test cases etc. feel free to leave a message.

Resolving patch:
diff --git a/scapy/layers/radius.py b/scapy/layers/radius.py
--- a/scapy/layers/radius.py
+++ b/scapy/layers/radius.py
@@ -6,6 +6,10 @@
 
 """
 RADIUS (Remote Authentication Dial In User Service)
+
+To disable Radius-EAP defragmentation (True by default), you can use::
+
+    conf.contribs.setdefault("radius", {}).setdefault("auto-defrag", False)
 """
 
 import struct
@@ -21,7 +25,6 @@
 from scapy.config import conf
 from scapy.error import Scapy_Exception
 
-
 # https://www.iana.org/assignments/radius-types/radius-types.xhtml
 _radius_attribute_types = {
     1: "User-Name",
@@ -995,7 +998,6 @@ class RadiusAttr_NAS_Port_Type(_RadiusAttrIntEnumVal):
 
 
 class _EAPPacketField(PacketLenField):
-
     """
     Handles EAP-Message attribute value (the actual EAP packet).
     """
@@ -1016,7 +1018,6 @@ class RadiusAttr_EAP_Message(RadiusAttribute):
     """
     Implements the "EAP-Message" attribute (RFC 3579).
     """
-
     name = "EAP-Message"
     match_subclass = True
     fields_desc = [
@@ -1026,11 +1027,29 @@ class RadiusAttr_EAP_Message(RadiusAttribute):
             None,
             "value",
             "B",
-            adjust=lambda pkt, x: len(pkt.value) + 2
+            adjust=lambda pkt, x: x + 2
         ),
         _EAPPacketField("value", "", EAP, length_from=lambda p: p.len - 2)
     ]
 
+    def post_dissect(self, s):
+        if not conf.contribs.get("radius", {}).get("auto-defrag", True):
+            return s
+        if isinstance(self.value, conf.raw_layer):
+            # Defragment
+            x = s
+            buf = self.value.load
+            while x and struct.unpack("!B", x[:1])[0] == 79:
+                # Let's carefully avoid the infinite loop
+                length = struct.unpack("!B", x[1:2])[0]
+                if not length:
+                    return s
+                buf, x = buf + x[2:length], x[length:]
+                if length < 254:
+                    self.value = EAP(buf)
+                    return x
+        return s
+
 
 class RadiusAttr_Vendor_Specific(RadiusAttribute):
     """


Test patch:
diff --git a/test/scapy/layers/radius.uts b/test/scapy/layers/radius.uts
--- a/test/scapy/layers/radius.uts
+++ b/test/scapy/layers/radius.uts
@@ -206,6 +206,36 @@ r = raw(RadiusAttr_Framed_IP_Address())
 p = RadiusAttr_Framed_IP_Address(r)
 assert p.type == 8
 
+= Radius - fragmented EAP - GH2832
+
+conf.contribs["radius"] = {}
+
+s = b'\x0b\x1c\x04,%[\xa5\x11\x0b\xdc\x8f\x94\xf2\xe0\x01\x8a\xacNI\x8eO\xff\x01\x97\x00\xff\r\xc0\x00\x1a\x15\xd0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x4f\x03\x00P\x12n\x14\xd1\x9f\xa8\xf3\t\xe4\xc0\x82\xd6\x07AB\xd5\xf5\x18\x12\x19\xd6\x9eX\x05A\x93jo\x9a\t:\xa9g_\xc2'
+
+pkt = Radius(s)
+assert len(pkt.attributes) == 3
+assert pkt.attributes[0].value.tls_data == b'\0' * 244
+assert pkt.attributes[1].type == 80
+assert pkt.attributes[1].len == 18
+assert pkt.attributes[2].type == 24
+assert pkt.attributes[2].len == 18
+
+conf.contribs.setdefault("radius", {})["auto-defrag"] = False
+_od = conf.debug_dissector
+conf.debug_dissector = False
+
+pkt = Radius(s)
+
+conf.debug_dissector = _od
+assert len(pkt.attributes) == 4
+assert pkt.attributes[0].type == 79
+assert pkt.attributes[1].type == 79
+assert pkt.attributes[1].value.load == b'\0'
+assert pkt.attributes[2].type == 80
+assert pkt.attributes[2].len == 18
+assert pkt.attributes[3].type == 24
+assert pkt.attributes[3].len == 18
+
 = RadiusAttr_User_Password
 
 r = b'\x01\x00\x00\x1c0x10x20x30x40x50\x02\x08geheim'