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