Home | History | Annotate | Download | only in contrib
      1 # This file is part of Scapy
      2 # Scapy is free software: you can redistribute it and/or modify
      3 # it under the terms of the GNU General Public License as published by
      4 # the Free Software Foundation, either version 2 of the License, or
      5 # any later version.
      6 #
      7 # Scapy is distributed in the hope that it will be useful,
      8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
      9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
     10 # GNU General Public License for more details.
     11 #
     12 # You should have received a copy of the GNU General Public License
     13 # along with Scapy. If not, see <http://www.gnu.org/licenses/>.
     14 
     15 # Copyright (C) 2017 Francois Contat <francois.contat (at] ssi.gouv.fr>
     16 
     17 # Based on tacacs+ v6 draft https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06
     18 
     19 # scapy.contrib.description = TACACS+ Protocol
     20 # scapy.contrib.status = loads
     21 
     22 import struct
     23 import hashlib
     24 
     25 from scapy.packet import Packet, bind_layers
     26 from scapy.fields import ByteEnumField, ByteField, IntField
     27 from scapy.fields import FieldListField
     28 from scapy.fields import FieldLenField, ConditionalField, StrLenField
     29 from scapy.layers.inet import TCP
     30 from scapy.compat import chb, orb
     31 from scapy.config import conf
     32 from scapy.modules.six.moves import range
     33 
     34 SECRET = 'test'
     35 
     36 def obfuscate(pay, secret, session_id, version, seq):
     37 
     38     '''
     39 
     40     Obfuscation methodology from section 3.7
     41     https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-3.7
     42 
     43     '''
     44 
     45     pad = b""
     46     curr_pad = b""
     47 
     48     # pad length must equal the payload to obfuscate.
     49     # pad = {MD5_1 [,MD5_2 [ ... ,MD5_n]]}
     50 
     51     while len(pad) < len(pay):
     52 
     53         msg = hashlib.md5()
     54         msg.update(struct.pack('!I', session_id))
     55         msg.update(secret.encode())
     56         msg.update(struct.pack('!BB', version, seq))
     57         msg.update(curr_pad)
     58         curr_pad = msg.digest()
     59         pad += curr_pad
     60 
     61     # Obf/Unobfuscation via XOR operation between plaintext and pad
     62 
     63     return b"".join(chb(orb(pad[i]) ^ orb(pay[i])) for i in range(len(pay)))
     64 
     65 TACACSPRIVLEVEL = {15:'Root',
     66                    1:'User',
     67                    0:'Minimum'}
     68 
     69 ##########################
     70 # Authentication Packets #
     71 ##########################
     72 
     73 TACACSVERSION = {1:'Tacacs',
     74                  192:'Tacacs+'}
     75 
     76 TACACSTYPE = {1:'Authentication',
     77               2:'Authorization',
     78               3:'Accounting'}
     79 
     80 TACACSFLAGS = {1:'Unencrypted',
     81                4:'Single Connection'}
     82 
     83 TACACSAUTHENACTION = {1:'Login',
     84                       2:'Change Pass',
     85                       4:'Send Authentication'}
     86 
     87 TACACSAUTHENTYPE = {1:'ASCII',
     88                     2:'PAP',
     89                     3:'CHAP',
     90                     4:'ARAP', #Deprecated
     91                     5:'MSCHAP',
     92                     6:'MSCHAPv2'}
     93 
     94 TACACSAUTHENSERVICE = {0:'None',
     95                        1:'Login',
     96                        2:'Enable',
     97                        3:'PPP',
     98                        4:'ARAP',
     99                        5:'PT',
    100                        6:'RCMD',
    101                        7:'X25',
    102                        8:'NASI',
    103                        9:'FwProxy'}
    104 
    105 TACACSREPLYPASS = {1:'PASS',
    106                    2:'FAIL',
    107                    3:'GETDATA',
    108                    4:'GETUSER',
    109                    5:'GETPASS',
    110                    6:'RESTART',
    111                    7:'ERROR',
    112                    21:'FOLLOW'}
    113 
    114 TACACSREPLYFLAGS = {1:'NOECHO'}
    115 
    116 TACACSCONTINUEFLAGS = {1:'ABORT'}
    117 
    118 
    119 class TacacsAuthenticationStart(Packet):
    120 
    121     '''
    122 
    123     Tacacs authentication start body from section 4.1
    124     https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-4.1
    125 
    126     '''
    127 
    128     name = 'Tacacs Authentication Start Body'
    129     fields_desc = [ByteEnumField('action', 1, TACACSAUTHENACTION),
    130                    ByteEnumField('priv_lvl', 1, TACACSPRIVLEVEL),
    131                    ByteEnumField('authen_type', 1, TACACSAUTHENTYPE),
    132                    ByteEnumField('authen_service', 1, TACACSAUTHENSERVICE),
    133                    FieldLenField('user_len', None, fmt='!B', length_of='user'),
    134                    FieldLenField('port_len', None, fmt='!B', length_of='port'),
    135                    FieldLenField('rem_addr_len', None, fmt='!B', length_of='rem_addr'),
    136                    FieldLenField('data_len', None, fmt='!B', length_of='data'),
    137                    ConditionalField(StrLenField('user', '', length_from=lambda x: x.user_len),
    138                                     lambda x: x != ''),
    139                    StrLenField('port', '', length_from=lambda x: x.port_len),
    140                    StrLenField('rem_addr', '', length_from=lambda x: x.rem_addr_len),
    141                    StrLenField('data', '', length_from=lambda x: x.data_len)]
    142 
    143 class TacacsAuthenticationReply(Packet):
    144 
    145     '''
    146 
    147     Tacacs authentication reply body from section 4.2
    148     https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-4.2
    149 
    150     '''
    151 
    152     name = 'Tacacs Authentication Reply Body'
    153     fields_desc = [ByteEnumField('status', 1, TACACSREPLYPASS),
    154                    ByteEnumField('flags', 0, TACACSREPLYFLAGS),
    155                    FieldLenField('server_msg_len', None, fmt='!H', length_of='server_msg'),
    156                    FieldLenField('data_len', None, fmt='!H', length_of='data'),
    157                    StrLenField('server_msg', '', length_from=lambda x: x.server_msg_len),
    158                    StrLenField('data', '', length_from=lambda x: x.data_len)]
    159 
    160 class TacacsAuthenticationContinue(Packet):
    161 
    162     '''
    163 
    164     Tacacs authentication continue body from section 4.3
    165     https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-4.3
    166 
    167     '''
    168 
    169     name = 'Tacacs Authentication Continue Body'
    170     fields_desc = [FieldLenField('user_msg_len', None, fmt='!H', length_of='user_msg'),
    171                    FieldLenField('data_len', None, fmt='!H', length_of='data'),
    172                    ByteEnumField('flags', 1, TACACSCONTINUEFLAGS),
    173                    StrLenField('user_msg', '', length_from=lambda x: x.user_msg_len),
    174                    StrLenField('data', '', length_from=lambda x: x.data_len)]
    175 
    176 #########################
    177 # Authorization Packets #
    178 #########################
    179 
    180 TACACSAUTHORTYPE = {0:'Not Set',
    181                     1:'None',
    182                     2:'Kerberos 5',
    183                     3:'Line',
    184                     4:'Enable',
    185                     5:'Local',
    186                     6:'Tacacs+',
    187                     8:'Guest',
    188                     16:'Radius',
    189                     17:'Kerberos 4',
    190                     32:'RCMD'}
    191 
    192 TACACSAUTHORSTATUS = {1:'Pass Add',
    193                       2:'Pass repl',
    194                       16:'Fail',
    195                       17:'Error',
    196                       33:'Follow'}
    197 
    198 class TacacsAuthorizationRequest(Packet):
    199 
    200     '''
    201 
    202     Tacacs authorization request body from section 5.1
    203     https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-5.1
    204 
    205     '''
    206 
    207     name = 'Tacacs Authorization Request Body'
    208     fields_desc = [ByteEnumField('authen_method', 0, TACACSAUTHORTYPE),
    209                    ByteEnumField('priv_lvl', 1, TACACSPRIVLEVEL),
    210                    ByteEnumField('authen_type', 1, TACACSAUTHENTYPE),
    211                    ByteEnumField('authen_service', 1, TACACSAUTHENSERVICE),
    212                    FieldLenField('user_len', None, fmt='!B', length_of='user'),
    213                    FieldLenField('port_len', None, fmt='!B', length_of='port'),
    214                    FieldLenField('rem_addr_len', None, fmt='!B', length_of='rem_addr'),
    215                    FieldLenField('arg_cnt', None, fmt='!B', count_of='arg_len_list'),
    216                    FieldListField('arg_len_list', [], ByteField('', 0),
    217                                   length_from=lambda pkt: pkt.arg_cnt),
    218                    StrLenField('user', '', length_from=lambda x: x.user_len),
    219                    StrLenField('port', '', length_from=lambda x: x.port_len),
    220                    StrLenField('rem_addr', '', length_from=lambda x: x.rem_addr_len)]
    221 
    222     def guess_payload_class(self, pay):
    223         if self.arg_cnt > 0:
    224             return TacacsPacketArguments
    225         return conf.padding_layer
    226 
    227 class TacacsAuthorizationReply(Packet):
    228 
    229     '''
    230 
    231     Tacacs authorization reply body from section 5.2
    232     https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-5.2
    233 
    234     '''
    235 
    236     name = 'Tacacs Authorization Reply Body'
    237     fields_desc = [ByteEnumField('status', 0, TACACSAUTHORSTATUS),
    238                    FieldLenField('arg_cnt', None, fmt='!B', count_of='arg_len_list'),
    239                    FieldLenField('server_msg_len', None, fmt='!H', length_of='server_msg'),
    240                    FieldLenField('data_len', None, fmt='!H', length_of='data'),
    241                    FieldListField('arg_len_list', [], ByteField('', 0),
    242                                   length_from=lambda pkt: pkt.arg_cnt),
    243                    StrLenField('server_msg', '', length_from=lambda x: x.server_msg_len),
    244                    StrLenField('data', '', length_from=lambda x: x.data_len)]
    245 
    246     def guess_payload_class(self, pay):
    247         if self.arg_cnt > 0:
    248             return TacacsPacketArguments
    249         return conf.padding_layer
    250 
    251 
    252 ######################
    253 # Accounting Packets #
    254 ######################
    255 
    256 TACACSACNTFLAGS = {2:'Start',
    257                    4:'Stop',
    258                    8:'Watchdog'}
    259 
    260 TACACSACNTSTATUS = {1:'Success',
    261                     2:'Error',
    262                     33:'Follow'}
    263 
    264 class TacacsAccountingRequest(Packet):
    265 
    266     '''
    267 
    268     Tacacs accounting request body from section 6.1
    269     https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-6.1
    270 
    271     '''
    272 
    273     name = 'Tacacs Accounting Request Body'
    274     fields_desc = [ByteEnumField('flags', 0, TACACSACNTFLAGS),
    275                    ByteEnumField('authen_method', 0, TACACSAUTHORTYPE),
    276                    ByteEnumField('priv_lvl', 1, TACACSPRIVLEVEL),
    277                    ByteEnumField('authen_type', 1, TACACSAUTHENTYPE),
    278                    ByteEnumField('authen_service', 1, TACACSAUTHENSERVICE),
    279                    FieldLenField('user_len', None, fmt='!B', length_of='user'),
    280                    FieldLenField('port_len', None, fmt='!B', length_of='port'),
    281                    FieldLenField('rem_addr_len', None, fmt='!B', length_of='rem_addr'),
    282                    FieldLenField('arg_cnt', None, fmt='!B', count_of='arg_len_list'),
    283                    FieldListField('arg_len_list', [], ByteField('', 0),
    284                                   length_from=lambda pkt: pkt.arg_cnt),
    285                    StrLenField('user', '', length_from=lambda x: x.user_len),
    286                    StrLenField('port', '', length_from=lambda x: x.port_len),
    287                    StrLenField('rem_addr', '', length_from=lambda x: x.rem_addr_len)]
    288 
    289     def guess_payload_class(self, pay):
    290         if self.arg_cnt > 0:
    291             return TacacsPacketArguments
    292         return conf.padding_layer
    293 
    294 class TacacsAccountingReply(Packet):
    295 
    296     '''
    297 
    298     Tacacs accounting reply body from section 6.2
    299     https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-6.2
    300 
    301     '''
    302 
    303     name = 'Tacacs Accounting Reply Body'
    304     fields_desc = [FieldLenField('server_msg_len', None, fmt='!H', length_of='server_msg'),
    305                    FieldLenField('data_len', None, fmt='!H', length_of='data'),
    306                    ByteEnumField('status', None, TACACSACNTSTATUS),
    307                    StrLenField('server_msg', '', length_from=lambda x: x.server_msg_len),
    308                    StrLenField('data', '', length_from=lambda x: x.data_len)]
    309 
    310 class TacacsPacketArguments(Packet):
    311 
    312     '''
    313 
    314     Class defined to handle the arguments listed at the end of tacacs+
    315     Authorization and Accounting packets.
    316 
    317     '''
    318 
    319     __slots__ = ['_len']
    320     name = 'Arguments in Tacacs+ packet'
    321     fields_desc = [StrLenField('data', '', length_from=lambda pkt: pkt._len)]
    322 
    323     def pre_dissect(self, s):
    324         cur = self.underlayer
    325         i = 0
    326 
    327         # Searching the position in layer in order to get its length
    328 
    329         while isinstance(cur, TacacsPacketArguments):
    330             cur = cur.underlayer
    331             i += 1
    332         self._len = cur.arg_len_list[i]
    333         return s
    334 
    335     def guess_payload_class(self, pay):
    336         cur = self.underlayer
    337         i = 0
    338 
    339         # Guessing if Argument packet. Nothing in encapsulated via tacacs+
    340 
    341         while isinstance(cur, TacacsPacketArguments):
    342             cur = cur.underlayer
    343             i += 1
    344         if i+1 < cur.arg_cnt:
    345             return TacacsPacketArguments
    346         return conf.padding_layer
    347 
    348 
    349 
    350 class TacacsClientPacket(Packet):
    351 
    352     '''
    353 
    354     Super class for tacacs packet in order to get them uncrypted
    355     Obfuscation methodology from section 3.7
    356     https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-3.7
    357 
    358     '''
    359     def post_dissect(self, pay):
    360 
    361         if self.flags == 0:
    362             pay = obfuscate(pay, SECRET, self.session_id, self.version, self.seq)
    363             return pay
    364 
    365 class TacacsHeader(TacacsClientPacket):
    366 
    367     '''
    368 
    369     Tacacs Header packet from section 3.8
    370     https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-3.8
    371 
    372     '''
    373 
    374     name = 'Tacacs Header'
    375     fields_desc = [ByteEnumField('version', 192, TACACSVERSION),
    376                    ByteEnumField('type', 1, TACACSTYPE),
    377                    ByteField('seq', 1),
    378                    ByteEnumField('flags', 0, TACACSFLAGS),
    379                    IntField('session_id', 0),
    380                    IntField('length', None)]
    381 
    382     def guess_payload_class(self, payload):
    383 
    384         # Guessing packet type from type and seq values
    385 
    386         # Authentication packet - type 1
    387 
    388         if self.type == 1:
    389             if self.seq % 2 == 0:
    390                 return TacacsAuthenticationReply
    391             if sum(struct.unpack('bbbb', payload[4:8])) == len(payload[8:]):
    392                 return TacacsAuthenticationStart
    393             elif sum(struct.unpack('!hh', payload[:4])) == len(payload[5:]):
    394                 return TacacsAuthenticationContinue
    395 
    396         # Authorization packet - type 2
    397 
    398         if self.type == 2:
    399             if self.seq % 2 == 0:
    400                 return TacacsAuthorizationReply
    401             return TacacsAuthorizationRequest
    402 
    403         # Accounting packet - type 3
    404 
    405         if self.type == 3:
    406             if self.seq % 2 == 0:
    407                 return TacacsAccountingReply
    408             return TacacsAccountingRequest
    409 
    410         return conf.raw_layer
    411 
    412     def post_build(self, p, pay):
    413 
    414         # Setting length of packet to obfuscate if not filled by user
    415 
    416         if self.length is None and pay:
    417             p = p[:-4] + struct.pack('!I', len(pay))
    418 
    419 
    420         if self.flags == 0:
    421 
    422             pay = obfuscate(pay, SECRET, self.session_id, self.version, self.seq)
    423             return p + pay
    424 
    425         return p
    426 
    427     def hashret(self):
    428         return struct.pack('I', self.session_id)
    429 
    430     def answers(self, other):
    431         return (isinstance(other, TacacsHeader) and
    432                 self.seq == other.seq + 1 and
    433                 self.type == other.type and
    434                 self.session_id == other.session_id)
    435 
    436 
    437 bind_layers(TCP, TacacsHeader, dport=49)
    438 bind_layers(TCP, TacacsHeader, sport=49)
    439 bind_layers(TacacsHeader, TacacsAuthenticationStart, type=1, dport=49)
    440 bind_layers(TacacsHeader, TacacsAuthenticationReply, type=1, sport=49)
    441 
    442 if __name__ == '__main__':
    443     from scapy.main import interact
    444     interact(mydict=globals(), mybanner='tacacs+')
    445