# This file is part of Scapy
# Scapy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# any later version.
#
# Scapy is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Scapy. If not, see .
# Copyright (C) 2017 Francois Contat
# Based on tacacs+ v6 draft https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06 # noqa: E501
# scapy.contrib.description = Terminal Access Controller Access-Control System+
# scapy.contrib.status = loads
import struct
import hashlib
from scapy.packet import Packet, bind_layers
from scapy.fields import ByteEnumField, ByteField, IntField
from scapy.fields import FieldListField
from scapy.fields import FieldLenField, ConditionalField, StrLenField
from scapy.layers.inet import TCP
from scapy.compat import chb, orb
from scapy.config import conf
from scapy.modules.six.moves import range
SECRET = 'test'
def obfuscate(pay, secret, session_id, version, seq):
'''
Obfuscation methodology from section 3.7
https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-3.7
'''
pad = b""
curr_pad = b""
# pad length must equal the payload to obfuscate.
# pad = {MD5_1 [,MD5_2 [ ... ,MD5_n]]}
while len(pad) < len(pay):
msg = hashlib.md5()
msg.update(struct.pack('!I', session_id))
msg.update(secret.encode())
msg.update(struct.pack('!BB', version, seq))
msg.update(curr_pad)
curr_pad = msg.digest()
pad += curr_pad
# Obf/Unobfuscation via XOR operation between plaintext and pad
return b"".join(chb(orb(pad[i]) ^ orb(pay[i])) for i in range(len(pay)))
TACACSPRIVLEVEL = {15: 'Root',
1: 'User',
0: 'Minimum'}
##########################
# Authentication Packets #
##########################
TACACSVERSION = {1: 'Tacacs',
192: 'Tacacs+'}
TACACSTYPE = {1: 'Authentication',
2: 'Authorization',
3: 'Accounting'}
TACACSFLAGS = {1: 'Unencrypted',
4: 'Single Connection'}
TACACSAUTHENACTION = {1: 'Login',
2: 'Change Pass',
4: 'Send Authentication'}
TACACSAUTHENTYPE = {1: 'ASCII',
2: 'PAP',
3: 'CHAP',
4: 'ARAP', # Deprecated
5: 'MSCHAP',
6: 'MSCHAPv2'}
TACACSAUTHENSERVICE = {0: 'None',
1: 'Login',
2: 'Enable',
3: 'PPP',
4: 'ARAP',
5: 'PT',
6: 'RCMD',
7: 'X25',
8: 'NASI',
9: 'FwProxy'}
TACACSREPLYPASS = {1: 'PASS',
2: 'FAIL',
3: 'GETDATA',
4: 'GETUSER',
5: 'GETPASS',
6: 'RESTART',
7: 'ERROR',
21: 'FOLLOW'}
TACACSREPLYFLAGS = {1: 'NOECHO'}
TACACSCONTINUEFLAGS = {1: 'ABORT'}
class TacacsAuthenticationStart(Packet):
'''
Tacacs authentication start body from section 4.1
https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-4.1
'''
name = 'Tacacs Authentication Start Body'
fields_desc = [ByteEnumField('action', 1, TACACSAUTHENACTION),
ByteEnumField('priv_lvl', 1, TACACSPRIVLEVEL),
ByteEnumField('authen_type', 1, TACACSAUTHENTYPE),
ByteEnumField('authen_service', 1, TACACSAUTHENSERVICE),
FieldLenField('user_len', None, fmt='!B', length_of='user'),
FieldLenField('port_len', None, fmt='!B', length_of='port'),
FieldLenField('rem_addr_len', None, fmt='!B', length_of='rem_addr'), # noqa: E501
FieldLenField('data_len', None, fmt='!B', length_of='data'),
ConditionalField(StrLenField('user', '', length_from=lambda x: x.user_len), # noqa: E501
lambda x: x != ''),
StrLenField('port', '', length_from=lambda x: x.port_len),
StrLenField('rem_addr', '', length_from=lambda x: x.rem_addr_len), # noqa: E501
StrLenField('data', '', length_from=lambda x: x.data_len)]
class TacacsAuthenticationReply(Packet):
'''
Tacacs authentication reply body from section 4.2
https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-4.2
'''
name = 'Tacacs Authentication Reply Body'
fields_desc = [ByteEnumField('status', 1, TACACSREPLYPASS),
ByteEnumField('flags', 0, TACACSREPLYFLAGS),
FieldLenField('server_msg_len', None, fmt='!H', length_of='server_msg'), # noqa: E501
FieldLenField('data_len', None, fmt='!H', length_of='data'),
StrLenField('server_msg', '', length_from=lambda x: x.server_msg_len), # noqa: E501
StrLenField('data', '', length_from=lambda x: x.data_len)]
class TacacsAuthenticationContinue(Packet):
'''
Tacacs authentication continue body from section 4.3
https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-4.3
'''
name = 'Tacacs Authentication Continue Body'
fields_desc = [FieldLenField('user_msg_len', None, fmt='!H', length_of='user_msg'), # noqa: E501
FieldLenField('data_len', None, fmt='!H', length_of='data'),
ByteEnumField('flags', 1, TACACSCONTINUEFLAGS),
StrLenField('user_msg', '', length_from=lambda x: x.user_msg_len), # noqa: E501
StrLenField('data', '', length_from=lambda x: x.data_len)]
#########################
# Authorization Packets #
#########################
TACACSAUTHORTYPE = {0: 'Not Set',
1: 'None',
2: 'Kerberos 5',
3: 'Line',
4: 'Enable',
5: 'Local',
6: 'Tacacs+',
8: 'Guest',
16: 'Radius',
17: 'Kerberos 4',
32: 'RCMD'}
TACACSAUTHORSTATUS = {1: 'Pass Add',
2: 'Pass repl',
16: 'Fail',
17: 'Error',
33: 'Follow'}
class TacacsAuthorizationRequest(Packet):
'''
Tacacs authorization request body from section 5.1
https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-5.1
'''
name = 'Tacacs Authorization Request Body'
fields_desc = [ByteEnumField('authen_method', 0, TACACSAUTHORTYPE),
ByteEnumField('priv_lvl', 1, TACACSPRIVLEVEL),
ByteEnumField('authen_type', 1, TACACSAUTHENTYPE),
ByteEnumField('authen_service', 1, TACACSAUTHENSERVICE),
FieldLenField('user_len', None, fmt='!B', length_of='user'),
FieldLenField('port_len', None, fmt='!B', length_of='port'),
FieldLenField('rem_addr_len', None, fmt='!B', length_of='rem_addr'), # noqa: E501
FieldLenField('arg_cnt', None, fmt='!B', count_of='arg_len_list'), # noqa: E501
FieldListField('arg_len_list', [], ByteField('', 0),
length_from=lambda pkt: pkt.arg_cnt),
StrLenField('user', '', length_from=lambda x: x.user_len),
StrLenField('port', '', length_from=lambda x: x.port_len),
StrLenField('rem_addr', '', length_from=lambda x: x.rem_addr_len)] # noqa: E501
def guess_payload_class(self, pay):
if self.arg_cnt > 0:
return TacacsPacketArguments
return conf.padding_layer
class TacacsAuthorizationReply(Packet):
'''
Tacacs authorization reply body from section 5.2
https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-5.2
'''
name = 'Tacacs Authorization Reply Body'
fields_desc = [ByteEnumField('status', 0, TACACSAUTHORSTATUS),
FieldLenField('arg_cnt', None, fmt='!B', count_of='arg_len_list'), # noqa: E501
FieldLenField('server_msg_len', None, fmt='!H', length_of='server_msg'), # noqa: E501
FieldLenField('data_len', None, fmt='!H', length_of='data'),
FieldListField('arg_len_list', [], ByteField('', 0),
length_from=lambda pkt: pkt.arg_cnt),
StrLenField('server_msg', '', length_from=lambda x: x.server_msg_len), # noqa: E501
StrLenField('data', '', length_from=lambda x: x.data_len)]
def guess_payload_class(self, pay):
if self.arg_cnt > 0:
return TacacsPacketArguments
return conf.padding_layer
######################
# Accounting Packets #
######################
TACACSACNTFLAGS = {2: 'Start',
4: 'Stop',
8: 'Watchdog'}
TACACSACNTSTATUS = {1: 'Success',
2: 'Error',
33: 'Follow'}
class TacacsAccountingRequest(Packet):
'''
Tacacs accounting request body from section 6.1
https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-6.1
'''
name = 'Tacacs Accounting Request Body'
fields_desc = [ByteEnumField('flags', 0, TACACSACNTFLAGS),
ByteEnumField('authen_method', 0, TACACSAUTHORTYPE),
ByteEnumField('priv_lvl', 1, TACACSPRIVLEVEL),
ByteEnumField('authen_type', 1, TACACSAUTHENTYPE),
ByteEnumField('authen_service', 1, TACACSAUTHENSERVICE),
FieldLenField('user_len', None, fmt='!B', length_of='user'),
FieldLenField('port_len', None, fmt='!B', length_of='port'),
FieldLenField('rem_addr_len', None, fmt='!B', length_of='rem_addr'), # noqa: E501
FieldLenField('arg_cnt', None, fmt='!B', count_of='arg_len_list'), # noqa: E501
FieldListField('arg_len_list', [], ByteField('', 0),
length_from=lambda pkt: pkt.arg_cnt),
StrLenField('user', '', length_from=lambda x: x.user_len),
StrLenField('port', '', length_from=lambda x: x.port_len),
StrLenField('rem_addr', '', length_from=lambda x: x.rem_addr_len)] # noqa: E501
def guess_payload_class(self, pay):
if self.arg_cnt > 0:
return TacacsPacketArguments
return conf.padding_layer
class TacacsAccountingReply(Packet):
'''
Tacacs accounting reply body from section 6.2
https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-6.2
'''
name = 'Tacacs Accounting Reply Body'
fields_desc = [FieldLenField('server_msg_len', None, fmt='!H', length_of='server_msg'), # noqa: E501
FieldLenField('data_len', None, fmt='!H', length_of='data'),
ByteEnumField('status', None, TACACSACNTSTATUS),
StrLenField('server_msg', '', length_from=lambda x: x.server_msg_len), # noqa: E501
StrLenField('data', '', length_from=lambda x: x.data_len)]
class TacacsPacketArguments(Packet):
'''
Class defined to handle the arguments listed at the end of tacacs+
Authorization and Accounting packets.
'''
__slots__ = ['_len']
name = 'Arguments in Tacacs+ packet'
fields_desc = [StrLenField('data', '', length_from=lambda pkt: pkt._len)]
def pre_dissect(self, s):
cur = self.underlayer
i = 0
# Searching the position in layer in order to get its length
while isinstance(cur, TacacsPacketArguments):
cur = cur.underlayer
i += 1
self._len = cur.arg_len_list[i]
return s
def guess_payload_class(self, pay):
cur = self.underlayer
i = 0
# Guessing if Argument packet. Nothing in encapsulated via tacacs+
while isinstance(cur, TacacsPacketArguments):
cur = cur.underlayer
i += 1
if i + 1 < cur.arg_cnt:
return TacacsPacketArguments
return conf.padding_layer
class TacacsClientPacket(Packet):
'''
Super class for tacacs packet in order to get them unencrypted
Obfuscation methodology from section 3.7
https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-3.7
'''
def post_dissect(self, pay):
if self.flags == 0:
pay = obfuscate(pay, SECRET, self.session_id, self.version, self.seq) # noqa: E501
return pay
class TacacsHeader(TacacsClientPacket):
'''
Tacacs Header packet from section 3.8
https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-3.8
'''
name = 'Tacacs Header'
fields_desc = [ByteEnumField('version', 192, TACACSVERSION),
ByteEnumField('type', 1, TACACSTYPE),
ByteField('seq', 1),
ByteEnumField('flags', 0, TACACSFLAGS),
IntField('session_id', 0),
IntField('length', None)]
def guess_payload_class(self, payload):
# Guessing packet type from type and seq values
# Authentication packet - type 1
if self.type == 1:
if self.seq % 2 == 0:
return TacacsAuthenticationReply
if sum(struct.unpack('bbbb', payload[4:8])) == len(payload[8:]):
return TacacsAuthenticationStart
elif sum(struct.unpack('!hh', payload[:4])) == len(payload[5:]):
return TacacsAuthenticationContinue
# Authorization packet - type 2
if self.type == 2:
if self.seq % 2 == 0:
return TacacsAuthorizationReply
return TacacsAuthorizationRequest
# Accounting packet - type 3
if self.type == 3:
if self.seq % 2 == 0:
return TacacsAccountingReply
return TacacsAccountingRequest
return conf.raw_layer
def post_build(self, p, pay):
# Setting length of packet to obfuscate if not filled by user
if self.length is None and pay:
p = p[:-4] + struct.pack('!I', len(pay))
if self.flags == 0:
pay = obfuscate(pay, SECRET, self.session_id, self.version, self.seq) # noqa: E501
return p + pay
return p
def hashret(self):
return struct.pack('I', self.session_id)
def answers(self, other):
return (isinstance(other, TacacsHeader) and
self.seq == other.seq + 1 and
self.type == other.type and
self.session_id == other.session_id)
bind_layers(TCP, TacacsHeader, dport=49)
bind_layers(TCP, TacacsHeader, sport=49)
bind_layers(TacacsHeader, TacacsAuthenticationStart, type=1, dport=49)
bind_layers(TacacsHeader, TacacsAuthenticationReply, type=1, sport=49)
if __name__ == '__main__':
from scapy.main import interact
interact(mydict=globals(), mybanner='tacacs+')