├── GTScan.py ├── LICENSE ├── README.md └── requirements.txt /GTScan.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright 2017 Loay Abdelrazek (@sigploiter) 3 | # 4 | 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are 7 | # met: 8 | # 9 | # * Redistributions of source code must retain the above copyright 10 | # notice, this list of conditions and the following disclaimer. 11 | # * Redistributions in binary form must reproduce the above 12 | # copyright notice, this list of conditions and the following disclaimer 13 | # in the documentation and/or other materials provided with the 14 | # distribution. 15 | # * Neither the name of the nor the names of its 16 | # contributors may be used to endorse or promote products derived from 17 | # this software without specific prior written permission. 18 | # 19 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | 32 | import socket 33 | import sys 34 | from argparse import ArgumentParser 35 | import os 36 | from struct import * 37 | from threading import * 38 | from binascii import * 39 | from colorama import init 40 | from termcolor import cprint 41 | from pyfiglet import figlet_format 42 | from prettytable import PrettyTable 43 | 44 | 45 | def initM3UA(): 46 | # M3UA Header 47 | m3ua_version = 1 # 1Byte 48 | m3ua_reserved = 0 # 1Byte 49 | 50 | # M3UA Message Classes, 1Byte 51 | m3ua_msg_class = {'ASPStateMaint': 3, 52 | 'ASPTrafficMaint': 4, 53 | 'TransferMessages': 1, 54 | 'SSNM': 2} 55 | # M3UA Message Types 56 | m3ua_msg_type = { 57 | 'ASPUP': 1, 58 | 'ASPC': 1, 59 | 'ASPUP_Ack': 4, 60 | 'ASPAC_ACK': 3, 61 | 'Payload': 1, 62 | 'DAUD': 3} 63 | 64 | m3ua_param_tags = { 65 | 'TrafficMode': 11, 66 | 'RoutingContext': 6, 67 | 'ASPIdentifier': 17, 68 | 'ProtocolData': 528} # 2Bytes 69 | 70 | # 4Bytes, total length of message to be the sum of all messages lenghts of the high layers (sccp,tcap) and be replaced with calcsize() 71 | msg_length = 0 72 | 73 | # M3UA ASP Identifier Parameter 74 | m3ua_asp_identifier = 3 # 4Bytes 75 | aspid_param_len = 8 # 2Bytes 76 | 77 | # M3UA Traffic Mode type Parameters 78 | m3ua_traffic_mode = { 79 | 'Loadshare': 2, 80 | 'Broadcast': 3, 81 | 'Override': 1} # 4Bytes 82 | 83 | traffic_mode_param_len = 8 # 2Bytes 84 | 85 | # M3UA Routing Context Parameters 86 | m3ua_rc = 100 # 4Bytes 87 | rc_param_len = 8 88 | 89 | # M3UA Protocol Data Parameters 90 | protocol_data_param_len = 49 91 | 92 | opc = client_pc # 4Bytes 93 | dpc = peer_pc # 4Bytes 94 | 95 | # service indicator, SCCP 96 | SI = 3 # 1Byte 97 | 98 | NI = 0 # 1Byte, International, adjust to 2 for national 99 | sls = 1 # 1Byte 100 | mp = 0 # 1Byte 101 | 102 | try: 103 | m3ua_header_aspup = pack('!BBBBiHHi', m3ua_version, m3ua_reserved, m3ua_msg_class['ASPStateMaint'], m3ua_msg_type['ASPUP'], msg_length, 104 | m3ua_param_tags['ASPIdentifier'], aspid_param_len, m3ua_asp_identifier) 105 | sk.sendall(m3ua_header_aspup) 106 | reply_1 = sk.recv(4096) 107 | 108 | m3ua_recv = unpack('!BBBBiHHi', reply_1) 109 | #print(m3ua_recv) 110 | #if (1 and 0 and 3 and 4) in m3ua_recv: 111 | # print('True') 112 | 113 | # ASPUP_ACK Received 114 | if m3ua_recv[3] == 4: 115 | m3ua_header_aspc = pack('!BBBBi', m3ua_version, m3ua_reserved,m3ua_msg_class['ASPTrafficMaint'], m3ua_msg_type['ASPC'], calcsize('BBBBi')) 116 | sk.sendall(m3ua_header_aspc) 117 | else: 118 | print('\033[31m[-]\033[0m M3UA ASP is Down..Probably not a Sigtran Node') 119 | 120 | except Exception as e: 121 | print("\033[31m[-]\033[0mError M3UA Stack Failed to Initialize: %s" %str(e)) 122 | 123 | else: 124 | print("[+]M3UA Stack Initialized...\n") 125 | reply_2 = sk.recv(4096) 126 | #m3ua_reply = unpack('!BBBBiHHiHHi',reply_2) 127 | 128 | msg_length = 69 129 | 130 | # return m3ua header set to send data protocol class to be used by other stack layers 131 | m3ua_header_data = pack('!BBBBiHHiiBBBB', m3ua_version, m3ua_reserved, m3ua_msg_class['TransferMessages'], m3ua_msg_type['Payload'], 132 | msg_length, m3ua_param_tags['ProtocolData'], protocol_data_param_len, 133 | opc, dpc, SI, NI, sls, mp) 134 | 135 | return m3ua_header_data 136 | 137 | 138 | def initSCCP(source_GT, destination_GT, destination_ssn): 139 | # Mandatory Fixed Parameters 140 | sccp_msg_type = 9 # 1Byte 141 | 142 | # SCCP data transfer message class, in-sequence delievery 143 | sccp_msg = b'\x81' 144 | # sccp_msg_handling = 8 #1Byte 145 | sccp_pointer_var1 = 3 # 1Byte 146 | sccp_pointer_var2 = 14 # 1Byte 147 | sccp_pointer_var3 = 24 # 1Byte 148 | 149 | # Mandatory Variable Parameters 150 | # Called Party Address 151 | # Address Indicator 152 | # Address indicator is 1 byte in length its value determined based on the bits that are included\ 153 | # in our case a value of 18(0x12) indicated that routing based on GT, GT format is 0100, SSN is included, no PC is included 154 | called_addr_length = (5 + dGT_len) # 1Byte 155 | called_addr_indicator = b'\x12' # 0x12 in hex, 1Byte 156 | called_ssn = destination_ssn # 1Byte 157 | 158 | # Global title 159 | called_translation_type = b'\x00' # 1Byte 160 | called_numbering_plan = b'\x12' # 1Byte 161 | # International Number 162 | called_nature_of_address_indicator = b'\x04' # 1Byte 163 | called_GT = destination_GT # 6-8 Bytes 164 | 165 | # Calling Party Address 166 | calling_addr_length = (5 + sGT_len) # 1Byte 167 | calling_addr_indicator = b'\x12' # 0x12 in hex, 1Byte 168 | calling_ssn = source_ssn # 1Byte 169 | 170 | # Global title 171 | calling_translation_type = b'\x00' # 1Byte 172 | calling_numbering_plan = b'\x12' # 1Byte 173 | 174 | # International Number 175 | 176 | calling_nature_of_address_indicator = b'\x04' # 1Byte 177 | calling_GT = source_GT # 6-8 Bytes 178 | 179 | 180 | # try: 181 | sccp_header = pack('!B1sBBBB1sB1s1s1s6sB1sB1s1s1s6s', sccp_msg_type, sccp_msg, 182 | sccp_pointer_var1, sccp_pointer_var2, sccp_pointer_var3, called_addr_length, called_addr_indicator, 183 | called_ssn, called_translation_type, called_numbering_plan, called_nature_of_address_indicator, 184 | called_GT, calling_addr_length, calling_addr_indicator, calling_ssn, calling_translation_type, calling_numbering_plan, 185 | calling_nature_of_address_indicator, calling_GT) 186 | 187 | return sccp_header 188 | 189 | 190 | def initTCAP(): 191 | 192 | # Transaction 193 | # TCAP Message Type(Empty TCAP) 194 | # Incase that the destination GT listens on the scanned SSN, it will return with a TCAP Abort message, else it will respond with UDTS error message of 195 | # No translation on this specific address, as internally in core network GT is translated to pc+ssn 196 | 197 | message_type = 96 # 1Byte, 0x60 as hex 198 | tcap_length = 0 # 1byte, 0x04 as hex 199 | 200 | # Component Tags - Mandatory 201 | component_tag = b'\xa1' # 1Byte 202 | component_length = b'\x1d' # 1Byte 203 | 204 | try: 205 | tcap_header = pack('!BB1s1s', message_type, calcsize('BB1s1s'), component_tag, component_length) 206 | return tcap_header 207 | except Exception as e: 208 | print('\033[31m[-]\033[0mError in TCAP Layer: %s ' %str(e)) 209 | sys.exit(2) 210 | 211 | 212 | if __name__ == '__main__': 213 | 214 | global client_pc 215 | global peer_pc 216 | global sGT_len 217 | global dGT_len 218 | 219 | init(strip=not sys.stdout.isatty()) 220 | banner = "GTScan" 221 | cprint(figlet_format(banner, font="standard"), "blue") 222 | print ("\033[33m[+]\033[0m \tGlobalTitle Scanner \033[33m[+]\033[0m") 223 | print ("\033[33m[+]\033[0m \t Version 1 \033[33m[+]\033[0m") 224 | print ("\033[33m[+]\033[0m\t Author: LoayAbdelrazek \033[33m[+]\033[0m") 225 | print ("\033[33m[+]\033[0m \t (@SigPloiter) \033[33m[+]\033[0m\n") 226 | 227 | 228 | 229 | table = PrettyTable() 230 | table.field_names=['Global Title', 'Subsytem Number', 'Node'] 231 | 232 | 233 | 234 | parser = ArgumentParser() 235 | 236 | parser.add_argument("-l", dest="client_ip", 237 | default=False, action="store", 238 | help="\tSpecify local IP listening for sctp") 239 | 240 | parser.add_argument("-p", dest="client_port", 241 | default=2905, type=int, action="store", 242 | help="\tSpecify local sctp port,default 2905") 243 | 244 | parser.add_argument("-r", dest="peer_ip", 245 | default=False, action="store", 246 | help="\tSpecify Remote STP IP") 247 | 248 | parser.add_argument("-P", dest="peer_port", 249 | default=False, type=int, action="store", 250 | help="\tSpecify Remote SCTP port") 251 | 252 | parser.add_argument("-c", dest="client_pc", 253 | default=False, type=int, action="store", 254 | help="\tSpecify local point code") 255 | 256 | parser.add_argument("-C", dest="peer_pc", 257 | default=False, type=int, action="store", 258 | help="\tSpecify STP point code") 259 | 260 | parser.add_argument("-g", dest="sGT", 261 | default=False, action="store", 262 | help="\tSpecify local global title") 263 | 264 | parser.add_argument("-G", dest="dGT", 265 | default=False, action="store", 266 | help="\tSpecify global title(s) to scan,comma-sperated list or range (-) sperated") 267 | 268 | parser.add_argument("-s", dest="source_ssn", 269 | default=7, type=int, action="store", 270 | help="\tSpecify SSN to use, default is 7") 271 | 272 | args = parser.parse_args() 273 | 274 | if (args.client_ip is False) or (args.client_port is False) or (args.sGT is False) or (args.dGT is False) or (args.client_pc is False) or (args.peer_pc is False): 275 | print("not enought number of arguments\n\nExample: ./GTScan.py -G 201500000000,201500000002 -g 965123456780 -c 1 -C 2 -p 2905 -P 2906 -l 192.168.56.1 -r 192.168.56.102\n") 276 | sys.exit(1) 277 | 278 | client_ip = args.client_ip 279 | client_port = args.client_port 280 | peer_ip = args.peer_ip 281 | peer_port = args.peer_port 282 | client_pc = args.client_pc 283 | peer_pc = args.peer_pc 284 | sGT = args.sGT 285 | dGT = args.dGT 286 | source_ssn = args.source_ssn 287 | 288 | destination_ssn = { 289 | 6:'HLR', 290 | 7:'VLR', 291 | 8:'MSC', 292 | 9:'EIR', 293 | 10:'AuC', 294 | 142:'RANAP', 295 | 143:'RNSAP', 296 | 145:'GMLC', 297 | 146:'gsmSCF_CAP', 298 | 147:'gsmSCF_MAP', 299 | 148:'SIWF', 300 | 149:'SGSN', 301 | 150:'GGSN', 302 | 249:'PCAP', 303 | 250:'BCS', 304 | 251:'MCS_BSSAP-LE', 305 | 252:'SMLC', 306 | 253:'BSS_O&M', 307 | 254:'A_Interface'} 308 | 309 | if len(sGT) % 2 == 0: 310 | source_GT = unhexlify(''.join([sGT[x:x + 2][::-1] for x in range(0, len(sGT), 2)])) 311 | sGT_len = len(source_GT) 312 | 313 | 314 | # Initializing SCTP 315 | try: 316 | sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_SCTP) 317 | sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 318 | sk.bind((client_ip, client_port)) 319 | sk.connect((peer_ip, peer_port)) 320 | sk.settimeout(1) 321 | 322 | 323 | except socket.error as msg: 324 | print('\033[31m[-]\033[0mError Socket could not be created: %s' %str(msg)) 325 | sys.exit(1) 326 | else: 327 | print('[+]SCTP Stack Initialized...') 328 | m3ua_header_data = initM3UA() 329 | 330 | 331 | if "," in dGT: 332 | dGT = dGT.split(',') 333 | 334 | if len(dGT[0]) % 2 == 0: 335 | dGT_len = len(unhexlify(''.join([dGT[0][x:x + 2][::-1] for x in range(0, len(dGT[0]), 2)]))) 336 | else: 337 | dGT_temp = dGT[0] + '0' 338 | dGT_len = len(unhexlify(''.join([dGT_temp[x:x + 2][::-1] for x in range(0, len(dGT_temp), 2)]))) 339 | 340 | for gt in dGT: 341 | if len(gt) % 2 == 0: 342 | destination_GT = unhexlify(''.join([gt[x:x + 2][::-1] for x in range(0, len(gt), 2)])) 343 | else: 344 | gt_temp = gt + '0' 345 | destination_GT = len(unhexlify(''.join([gt_temp[x:x + 2][::-1] for x in range(0, len(gt_temp), 2)]))) 346 | 347 | for ssn in destination_ssn: 348 | try: 349 | sccp_header = initSCCP(source_GT, destination_GT, ssn) 350 | tcap_header = initTCAP() 351 | sk.sendall(m3ua_header_data + sccp_header + tcap_header) 352 | print('\033[34m[*]\033[0m Scanning +{} on SSN: {}'.format(gt, ssn)) 353 | 354 | tcap_reply = sk.recv(4096) 355 | tcap_recv = unpack_from('!BBB', tcap_reply, offset=66) 356 | if tcap_recv == (73,0,74): 357 | print('\033[32m[+] {} Detected on GT:+{} ,SSN:{}\033[0m '.format(destination_ssn[ssn], gt, ssn)) 358 | table.add_row([gt, ssn, destination_ssn[ssn]]) 359 | except socket.timeout: 360 | continue 361 | 362 | 363 | elif "-" in dGT: 364 | dGT = dGT.split('-') 365 | 366 | if len(dGT[0]) % 2 == 0: 367 | dGT_len = len(unhexlify(''.join([dGT[0][x:x + 2][::-1] for x in range(0, len(dGT[0]), 2)]))) 368 | else: 369 | #dGT[0] = dGT[0] + '0' 370 | dGT_len = len(unhexlify(''.join([dGT[0][x:x + 2][::-1] for x in range(0, len(dGT[0]), 2)]))) 371 | dGT[0] = dGT[0][:-1] 372 | 373 | dGT1 = int(dGT[0]) 374 | dGT2 = int(dGT[1]) 375 | 376 | 377 | for gt in range(dGT1, dGT2 + 1): 378 | if len(str(gt)) % 2 == 0: 379 | destination_GT = unhexlify(''.join([str(gt)[x:x + 2][::-1] for x in range(0, len(str(gt)), 2)])) 380 | else: 381 | gt = int(str(gt) + '0') 382 | destination_GT = unhexlify(''.join([str(gt)[x:x + 2][::-1] for x in range(0, len(str(gt)), 2)])) 383 | 384 | for ssn in destination_ssn: 385 | try: 386 | sccp_header = initSCCP(source_GT, destination_GT, ssn) 387 | tcap_header = initTCAP() 388 | sk.sendall(m3ua_header_data + sccp_header + tcap_header) 389 | print('\033[34m[*]\033[0mScanning +{} on SSN: {}'.format(gt, ssn)) 390 | 391 | tcap_reply = sk.recv(4096) 392 | 393 | if len(tcap_reply) > 0: 394 | tcap_recv = unpack_from('!BBB', tcap_reply, offset=66) 395 | if tcap_recv == (73,0,74): 396 | print('\033[32m[+] {} Detected on GT:+{} ,SSN:{}\033[0m '.format(destination_ssn[ssn], str(gt), ssn)) 397 | table.add_row([str(gt), ssn, destination_ssn[ssn]]) 398 | except socket.timeout: 399 | continue 400 | 401 | else: 402 | if len(dGT) % 2 == 0: 403 | dGT_len = len(unhexlify(''.join([dGT[x:x + 2][::-1] for x in range(0, len(dGT), 2)]))) 404 | destination_GT = unhexlify(''.join([dGT[x:x + 2][::-1] for x in range(0, len(dGT), 2)])) 405 | 406 | else: 407 | dGT_len = len(dGT) 408 | dGT = dGT + '0' 409 | #dGT_len = len(unhexlify(''.join([dGT[x:x + 2][::-1] for x in range(0, len(dGT), 2)]))) & 0xf 410 | #print(len(unhexlify(''.join([dGT[x:x + 2][::-1] for x in range(0, len(dGT), 2)])))) 411 | destination_GT = unhexlify(''.join([dGT[x:x + 2][::-1] for x in range(0, len(dGT), 2)])) 412 | 413 | 414 | for ssn in destination_ssn: 415 | try: 416 | sccp_header = initSCCP(source_GT, destination_GT, ssn) 417 | tcap_header = initTCAP() 418 | sk.sendall(m3ua_header_data + sccp_header + tcap_header) 419 | print('\033[34m[*]\033[0m Scanning +{} on SSN: {}'.format(dGT, ssn)) 420 | tcap_reply = sk.recv(4096) 421 | 422 | if len(tcap_reply) > 0: 423 | tcap_recv = unpack_from('!BBB', tcap_reply, offset=66) 424 | if tcap_recv == (73,0,74): 425 | print('\033[32m[+] {} Detected on GT:+{} ,SSN:{}\033[0m '.format(destination_ssn[ssn], dGT, ssn)) 426 | table.add_row([dGT, ssn, destination_ssn[ssn]]) 427 | except socket.timeout: 428 | continue 429 | 430 | print() 431 | print('\033[32m*** Detected GT ***\033[0m') 432 | print(table) 433 | 434 | sk.close() 435 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Loay Abdelrazek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GTScan 2 | The Nmap Scanner for Telco. With the current focus on telecom security, there used tools in day to day IT side penetration testing should 3 | be extended to telecom as well. From here came the motivation for an nmap-like scanner but for telco 4 | 5 | The current security interconnect security controls might fail against reconnaissance , although mobile operators might implement 6 | SMS firewalls/proxies, Interconnect firewalls, some of those leak information that could be used for further information gathering 7 | process. 8 | 9 | The motivation behind this project, first adding a new toolking into the arsenal of telecom penetration testers. Second give the 10 | mobile operators a way to test their controls to a primitive methodology such as information gathering and reconnaissance. 11 | 12 | # How does it work 13 | GTScan relies on using empty TCAP layers as probes to detect listening subsystem numbers (i.e application port numbers like 80 for 14 | http, 443 for https but for telecom nodes) on the respective global titles. With this way will be able to map the network 15 | and use the results to conduct targeted direct attacks to the respective nodes. 16 | 17 | GTScan includes Message handling: Return message on error in the SCCP layer to determine from the response what is the scanned node. 18 | If a TCAP abort message is returned with an error p-abortCause: unrecognizedMessageType (0) thus the destination nodes is listening 19 | on the SSN that was scanned, else then the scanner continues scanning on other SSNs 20 | 21 | You can provide GTscan a range of global titles to be scanned, a comma-separated or a single GT to be scanned, along with other 22 | parameters 23 | 24 | # Requirements 25 | python3 26 | 27 | pip3 install -r requirements.txt 28 | 29 | And of course an SS7/Sigtran access :) 30 | 31 | # Usage 32 | 33 | Example: ./GTScan.py -G 201500000000,201500000002 -g 965123456780 -c 1 -C 2 -p 2905 -P 2906 -l 192.168.56.1 -r 192.168.56.102 34 | 35 | 36 | 37 | All contributions are mostly welcomed 38 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyfiglet==0.7.5 2 | termcolor==1.1.0 3 | colorama==0.3.9 4 | --------------------------------------------------------------------------------