├── .gitignore ├── .screens ├── base.png ├── ircp.png └── preview.png ├── LICENSE ├── parser.py ├── README.md └── ircp.py /.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | backup/ 3 | -------------------------------------------------------------------------------- /.screens/base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/internet-relay-chat/IRCP/HEAD/.screens/base.png -------------------------------------------------------------------------------- /.screens/ircp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/internet-relay-chat/IRCP/HEAD/.screens/ircp.png -------------------------------------------------------------------------------- /.screens/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/internet-relay-chat/IRCP/HEAD/.screens/preview.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2023, acidvegas 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /parser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # ircp logs parser - developed by acidvegas in python (https://git.acid.vegas/ircp) 3 | 4 | import json 5 | import os 6 | import sys 7 | 8 | def parse(option, data, raw=True): 9 | if not raw: 10 | data = ' '.join(data.split()[3:]) 11 | if data[:1] == ':': 12 | data = data[1:] 13 | if type(data) == bool: 14 | data = str(data) 15 | print(data.replace(option, f'\033[31m{option}\033[0m')) 16 | return data 17 | 18 | # Main 19 | if len(sys.argv) >= 2: 20 | option = sys.argv[1] 21 | raw = True 22 | search = False 23 | if len(sys.argv) == 3: 24 | if sys.argv[2] == 'clean': 25 | raw = False 26 | elif sys.argv[2] == 'search': 27 | search = True 28 | logs = os.listdir('logs') 29 | found = list() 30 | for log in logs: 31 | with open('logs/'+log) as logfile: 32 | try: 33 | data = json.loads(logfile.read()) 34 | except: 35 | print('error: failed to load ' + log) 36 | break 37 | if option in data: 38 | data = data[option] 39 | if type(data) == str: 40 | found.append(parse(option, data, raw)) 41 | elif type(data) == list: 42 | for item in data: 43 | found.append(parse(option, item, raw)) 44 | elif search: 45 | for item in data: 46 | _data = data[item] 47 | if type(_data) == str and option in _data: 48 | found.append(parse(option, item, raw)) 49 | elif type(_data) == list: 50 | for _item in _data: 51 | if option in _item: 52 | found.append(parse(option, _item, raw)) 53 | if found: 54 | print(f'\nfound {len(found)} results in {len(logs)} logs') 55 | else: 56 | print('usage: python parser.py [clean]\n') 57 | print(' may be any item in the snapshots (001, NOTICE, 464, etc) or a string to search') 58 | print(' [clean] may be optionally used to display a cleaner output') 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Internet Relay Chat Probe (IRCP) 2 | 3 | ![](.screens/ircp.png) 4 | 5 | *TRIPLE 6 SEVEN OCULOUS* 6 | 7 | A robust information gathering tool for large scale reconnaissance on [Internet Relay Chat](https://en.wikipedia.org/wiki/Internet_Relay_Chat) servers, made for future usage with [internetrelaychat.org](https://internetrelaychat.org) for public statistics on the protocol. 8 | 9 | Meant to be used in combination with [masscan](https://github.com/robertdavidgraham/masscan) checking **0.0.0.0/0** *(the entire IPv4 range)* for ports **6660-6669**, **6697**, **7000**, & other common IRC ports. 10 | 11 | The idea is to create a *proof-of-concept* documenting how large-scale information gathering on the IRC protocol can be malicious & invasive to privacy, while also yielding deep-dive look at the IRC protocol & it's internal statistics & commonalities. 12 | 13 | ## Usage 14 | The only required arguement to pass is a direct path to the targets list, which should be a text file containing a new-line seperated list of targets. 15 | 16 | Targets must be a valid IPv4 or IPv6 address & can optionally be suffixed with a port. 17 | 18 | Edit [ircp.py](https://github.com/internet-relay-chat/IRCP/blob/master/ircp.py) & tweak the settings to your favor, though they rest with sane defaults. 19 | 20 | ## Order of Operations 21 | First, an attempt to connect using SSL/TLS is made, which will fall back to a standard connection if it fails. If a non-standard port was given, both standard & secure connection attempts are made on the port as-well. The **RPL_ISUPPORT** *(005)* response is checked for the `SSL=` option to try & locate secure ports. 22 | 23 | Once connected, server information is gathered from `ADMIN`, `CAP LS`, `COMMANDS`, `HELP`, `MODULES -all`, `VERSION`, `IRCOPS`, `MAP`, `INFO`, `LINKS`, `SERVLIST`, `STATS p`, & `LIST` replies. An attempt to register a nickname is then made by trying to contact NickServ. 24 | 25 | Lastly, every channel is joined with a `WHO` command sent & every new nick found gets a `WHOIS` sent. Registered channels & nicks are issued a NickServ/ChanServ `INFO` command. CTCP requests are sent to channels & nicks aswell. 26 | 27 | Once we have finishing scanning a server, the information found is saved to a JSON file. The data in the logs are stored in categories based on [numerics](https://raw.githubusercontent.com/internet-relay-chat/random/master/numerics.txt) *(001 is RPL_WELCOME, 322 is RPL_LIST, etc)* & events *(JOIN, MODE, KILL, etc)*. 28 | 29 | Everything is done in a *carefully* throttled manner for stealth to avoid detection. An extensive amount research on IRC daemons, services, & common practices used by network administrators was done & has fine tuned this project to be able to evade common triggers that thwart what we are doing. 30 | 31 | ## Preview 32 | ![](.screens/preview.png) 33 | 34 | ## Threat Scope 35 | While IRC is an generally unfavored chat protocol as of 2023 *(roughly 7,000 networks)*, it still has a beating heart *(over 300,000 users & channels)* with potential for user growth & active development being done on [IRCv3](https://ircv3.net/) protocol implementations. 36 | 37 | Point is..it's is not going anywhere. With that being said, every network being on the same port leads way for a lot of potential threats: 38 | 39 | * A new RCE is found for a very common IRC bot 40 | * A new 0day is found for a certain IRCd version 41 | * Old IRC daemons running versions with known CVE's 42 | * Tracing users network/channel whereabouts 43 | * Mass spamming attacks on every network 44 | 45 | Mass scanning *default* ports of services is nothing new & though port 6667 is not a common target, running an IRCd on a **non-standard** port should be the **standard**. If we have learned anything in the last 10 years, using standard ports for *anything* is almost always smells like a bad idea. 46 | 47 | ![](.screens/base.png) 48 | 49 | ## Todo 50 | * Built in identd 51 | * Checking for IPv6 availability *(SSL= in 005 responses may help verify IPv6)* 52 | * Support for IRC servers using old versions of SSL 53 | * Support for hostnames in targets list *(Attempt IPv6 & fallback to IPv4)* 54 | * Support for multiple vhost 55 | * How do we handle the possibility of connecting to multiple servers linked to same network? 56 | * Seperate lists for failed & banned networks. 57 | * Learn network target-change throttles from 439 **ERR_TARGETTOOFAST** replies *(Research IRCd defaults)* 58 | * Store last command execute to detect triggers 59 | 60 | ## Opt-out 61 | You can request to opt out of our scans by sending an email to [scan@internetrelaychat.org](mailto://scan@internetrelaychat.org) 62 | 63 | ___ 64 | 65 | ###### Mirrors 66 | [acid.vegas](https://git.acid.vegas/IRCP) • [GitHub](https://github.com/internet-relay-chat/IRCP) • [GitLab](https://gitlab.com/internetrelaychat/IRCP) • [SuperNETs](https://git.supernets.org/internetrelaychat/IRCP) 67 | -------------------------------------------------------------------------------- /ircp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # internet relay chat probe for https://internetrelaychat.org/ - developed by acidvegas in python (https://git.acid.vegas/ircp) 3 | 4 | import asyncio 5 | import ipaddress 6 | import json 7 | import os 8 | import random 9 | import ssl 10 | import sys 11 | import tarfile 12 | import time 13 | 14 | class settings: 15 | daemon = False # Run in daemon mode (24/7 throttled scanning) 16 | errors = True # Show errors in console 17 | errors_conn = False # Show connection errors in console 18 | log_max = 5000000 # 5mb # Maximum log size (in bytes) before starting another 19 | nickname = 'IRCP' # None = random 20 | username = 'ircp' # None = random 21 | realname = 'scan@internetrelaychat.org' # None = random 22 | ns_mail = 'scan@internetrelaychat.org' # None = random@random.[com|net|org] 23 | ns_pass = None # None = random 24 | vhost = None # Bind to a specific IP address 25 | 26 | class throttle: 27 | channels = 5 if not settings.daemon else 3 # Maximum number of channels to scan at once 28 | commands = 1.5 if not settings.daemon else 3 # Delay bewteen multiple commands send to the same target 29 | connect = 15 if not settings.daemon else 60 # Delay between each connection attempt on a diffferent port 30 | delay = 300 if not settings.daemon else 600 # Delay before registering nick (if enabled) & sending /LIST 31 | join = 10 if not settings.daemon else 30 # Delay between channel JOINs 32 | nick = 900 if not settings.daemon else 1200 # Delay between every random NICK change 33 | part = 10 if not settings.daemon else 30 # Delay before PARTing a channel 34 | seconds = 300 if not settings.daemon else 600 # Maximum seconds to wait when throttled for JOIN or WHOIS 35 | threads = 500 if not settings.daemon else 300 # Maximum number of threads running 36 | timeout = 30 if not settings.daemon else 60 # Timeout for all sockets 37 | whois = 15 if not settings.daemon else 30 # Delay between WHOIS requests 38 | ztimeout = 600 if not settings.daemon else 900 # Timeout for zero data from server 39 | 40 | class bad: 41 | donotscan = ( 42 | 'irc.dronebl.org', 'irc.alphachat.net', 43 | '5.9.164.48', '45.32.74.177', '104.238.146.46', '149.248.55.130', 44 | '2001:19f0:6001:1dc::1', '2001:19f0:b001:ce3::1', '2a01:4f8:160:2501:48:164:9:5', '2001:19f0:6401:17c::1' 45 | ) 46 | chan = { 47 | '403' : 'ERR_NOSUCHCHANNEL', '405' : 'ERR_TOOMANYCHANNELS', 48 | '435' : 'ERR_BANONCHAN', '442' : 'ERR_NOTONCHANNEL', 49 | '448' : 'ERR_FORBIDDENCHANNEL', '470' : 'ERR_LINKCHANNEL', 50 | '471' : 'ERR_CHANNELISFULL', '473' : 'ERR_INVITEONLYCHAN', 51 | '474' : 'ERR_BANNEDFROMCHAN', '475' : 'ERR_BADCHANNELKEY', 52 | '476' : 'ERR_BADCHANMASK', '477' : 'ERR_NEEDREGGEDNICK', 53 | '479' : 'ERR_BADCHANNAME', '480' : 'ERR_THROTTLE', 54 | '485' : 'ERR_CHANBANREASON', '488' : 'ERR_NOSSL', 55 | '489' : 'ERR_SECUREONLYCHAN', '519' : 'ERR_TOOMANYUSERS', 56 | '520' : 'ERR_OPERONLY', '926' : 'ERR_BADCHANNEL' 57 | } 58 | error = { 59 | 'install identd' : 'Identd required', 60 | 'trying to reconnect too fast' : 'Throttled', 61 | 'trying to (re)connect too fast' : 'Throttled', 62 | 'reconnecting too fast' : 'Throttled', 63 | 'access denied' : 'Access denied', 64 | 'not authorized to' : 'Not authorized', 65 | 'not authorised to' : 'Not authorized', 66 | 'password mismatch' : 'Password mismatch', 67 | 'dronebl' : 'DroneBL', 68 | 'dnsbl' : 'DNSBL', 69 | 'g:lined' : 'G:Lined', 70 | 'z:lined' : 'Z:Lined', 71 | 'timeout' : 'Timeout', 72 | 'closing link' : 'Banned', 73 | 'banned' : 'Banned', 74 | 'client exited' : 'QUIT', 75 | 'quit' : 'QUIT' 76 | } 77 | 78 | def backup(name): 79 | try: 80 | with tarfile.open(f'backup/{name}.tar.gz', 'w:gz') as tar: 81 | for log in os.listdir('logs'): 82 | tar.add('logs/' + log) 83 | debug('\033[1;32mBACKUP COMPLETE\033[0m') 84 | for log in os.listdir('logs'): 85 | os.remove('logs/' + log) 86 | except Exception as ex: 87 | error('\033[1;31mBACKUP FAILED\033[0m', ex) 88 | 89 | def debug(data): 90 | print('{0} \033[1;30m|\033[0m [\033[35m~\033[0m] {1}'.format(time.strftime('%I:%M:%S'), data)) 91 | 92 | def error(data, reason=None): 93 | if settings.errors: 94 | print('{0} \033[1;30m|\033[0m [\033[31m!\033[0m] {1} \033[1;30m({2})\033[0m'.format(time.strftime('%I:%M:%S'), data, str(reason))) if reason else print('{0} \033[1;30m|\033[0m [\033[31m!\033[0m] {1}'.format(time.strftime('%I:%M:%S'), data)) 95 | 96 | def rndnick(): 97 | prefix = random.choice(['st','sn','cr','pl','pr','fr','fl','qu','br','gr','sh','sk','tr','kl','wr','bl']+list('bcdfgklmnprstvwz')) 98 | midfix = random.choice(('aeiou'))+random.choice(('aeiou'))+random.choice(('bcdfgklmnprstvwz')) 99 | suffix = random.choice(['ed','est','er','le','ly','y','ies','iest','ian','ion','est','ing','led','inger']+list('abcdfgklmnprstvwz')) 100 | return prefix+midfix+suffix 101 | 102 | def ssl_ctx(): 103 | ctx = ssl.create_default_context() 104 | ctx.check_hostname = False 105 | ctx.verify_mode = ssl.CERT_NONE 106 | return ctx 107 | 108 | class probe: 109 | def __init__(self, semaphore, server, port, family=2): 110 | self.semaphore = semaphore 111 | self.server = server 112 | self.port = 6697 113 | self.oport = port 114 | self.family = family 115 | self.display = server.ljust(18)+' \033[1;30m|\033[0m unknown network \033[1;30m|\033[0m ' 116 | self.nickname = None 117 | self.multi = '' 118 | self.snapshot = dict() 119 | self.channels = {'all':list(), 'current':list(), 'users':dict()} 120 | self.nicks = {'all':list(), 'check':list()} 121 | self.loops = {'init':None, 'chan':None, 'nick':None, 'whois':None} 122 | self.login = {'pass': settings.ns_pass if settings.ns_pass else rndnick(), 'mail': settings.ns_mail if settings.ns_mail else f'{rndnick()}@{rndnick()}.'+random.choice(('com','net','org'))} 123 | self.services = {'chanserv':True, 'nickserv':True} 124 | self.jthrottle = throttle.join 125 | self.nthrottle = throttle.whois 126 | self.reader = None 127 | self.write = None 128 | 129 | async def sendmsg(self, target, msg): 130 | await self.raw(f'PRIVMSG {target} :{msg}') 131 | 132 | async def run(self): 133 | async with self.semaphore: 134 | try: 135 | await self.connect() # 6697 136 | except Exception as ex: 137 | if settings.errors_conn: 138 | error(self.display + '\033[1;31mdisconnected\033[0m - failed to connect using SSL/TLS on port ' + str(self.port), ex) 139 | if self.oport not in (6667,6697): 140 | self.port = self.oport 141 | await asyncio.sleep(throttle.connect) 142 | try: 143 | await self.connect() # Non-standard 144 | except Exception as ex: 145 | if settings.errors_conn: 146 | error(self.display + '\033[1;31mdisconnected\033[0m - failed to connect using SSL/TLS on port ' + str(self.port), ex) 147 | self.port = 6667 148 | await asyncio.sleep(throttle.connect) 149 | try: 150 | await self.connect(True) # 6667 151 | except Exception as ex: 152 | if settings.errors_conn: 153 | error(self.display + '\033[1;31mdisconnected\033[0m - failed to connect on port ' + str(self.port), ex) 154 | self.port = self.oport 155 | await asyncio.sleep(throttle.connect) 156 | try: 157 | await self.connect(True) # Non-standard 158 | except Exception as ex: 159 | if settings.errors_conn: 160 | error(self.display + '\033[1;31mdisconnected\033[0m - failed to connect on port ' + str(self.port), ex) 161 | else: 162 | self.port = 6667 163 | await asyncio.sleep(throttle.connect) 164 | try: 165 | await self.connect(True) # 6667 166 | except Exception as ex: 167 | if settings.errors_conn: 168 | error(self.display + '\033[1;31mdisconnected\033[0m - failed to connect on port ' + str(self.port), ex) 169 | 170 | async def raw(self, data): 171 | self.writer.write(data[:510].encode('utf-8') + b'\r\n') 172 | await self.writer.drain() 173 | 174 | async def connect(self, fallback=False): 175 | options = { 176 | 'host' : self.server, 177 | 'port' : self.port, 178 | 'limit' : 1024, 179 | 'ssl' : None if fallback else ssl_ctx(), 180 | 'family' : self.family, 181 | 'local_addr' : (settings.vhost, random.randint(5000,65000)) if settings.vhost else None 182 | } 183 | identity = { 184 | 'nick': settings.nickname if settings.nickname else rndnick(), 185 | 'user': settings.username if settings.username else rndnick(), 186 | 'real': settings.realname if settings.realname else rndnick() 187 | } 188 | self.nickname = identity['nick'] 189 | self.reader, self.writer = await asyncio.wait_for(asyncio.open_connection(**options), throttle.timeout) 190 | self.snapshot['port'] = options['port'] 191 | del options 192 | if not fallback: 193 | self.snapshot['ssl'] = True 194 | await self.raw('USER {0} 0 * :{1}'.format(identity['user'], identity['real'])) 195 | await self.raw('NICK ' + identity['nick']) 196 | del identity 197 | await self.listen() 198 | for item in self.loops: 199 | if self.loops[item]: 200 | self.loops[item].cancel() 201 | with open(f'logs/{self.server}.json{self.multi}', 'w') as fp: 202 | json.dump(self.snapshot, fp) 203 | debug(self.display + 'finished scanning') 204 | 205 | async def loop_initial(self): 206 | try: 207 | await asyncio.sleep(throttle.delay) 208 | cmds = ['ADMIN', 'CAP LS', 'COMMANDS', 'HELP', 'INFO', 'IRCOPS', 'LINKS', 'MAP', 'MODULES -all', 'SERVLIST', 'STATS p', 'VERSION'] 209 | random.shuffle(cmds) 210 | cmds += ['PRIVMSG NickServ :REGISTER {0} {1}'.format(self.login['pass'], self.login['mail']), 'PRIVMSG ChanServ :LIST *', 'PRIVMSG NickServ :LIST *', 'LIST'] 211 | for command in cmds: 212 | try: 213 | await self.raw(command) 214 | except: 215 | break 216 | else: 217 | await asyncio.sleep(throttle.commands) 218 | if not self.channels['all']: 219 | error(self.display + '\033[31merror\033[0m - no channels found') 220 | await self.raw('QUIT') 221 | except asyncio.CancelledError: 222 | pass 223 | except Exception as ex: 224 | error(self.display + '\033[31merror\033[0m - loop_initial', ex) 225 | 226 | async def loop_channels(self): 227 | try: 228 | while self.channels['all']: 229 | while len(self.channels['current']) >= throttle.channels: 230 | await asyncio.sleep(1) 231 | await asyncio.sleep(self.jthrottle) 232 | chan = random.choice(self.channels['all']) 233 | self.channels['all'].remove(chan) 234 | try: 235 | if self.services['chanserv']: 236 | await self.sendmsg('ChanServ', 'INFO ' + chan) 237 | await asyncio.sleep(throttle.commands) 238 | await self.raw('JOIN ' + chan) 239 | except: 240 | break 241 | self.loops['nick'].cancel() 242 | while self.nicks['check']: 243 | await asyncio.sleep(1) 244 | self.loops['whois'].cancel() 245 | self.loops['nick'].cancel() 246 | await self.raw('QUIT') 247 | except asyncio.CancelledError: 248 | pass 249 | except Exception as ex: 250 | error(self.display + '\033[31merror\033[0m - loop_channels', ex) 251 | 252 | async def loop_nick(self): 253 | try: 254 | while True: 255 | await asyncio.sleep(throttle.nick+random.randint(60,90)) 256 | self.nickname = rndnick() 257 | await self.raw('NICK ' + self.nickname) 258 | debug(self.display + '\033[0;35mNICK\033[0m - new identity') 259 | except asyncio.CancelledError: 260 | pass 261 | except Exception as ex: 262 | error(self.display + '\033[31merror\033[0m - loop_nick', ex) 263 | 264 | async def loop_whois(self): 265 | try: 266 | while True: 267 | if self.nicks['check']: 268 | nick = random.choice(self.nicks['check']) 269 | self.nicks['check'].remove(nick) 270 | try: 271 | await self.raw('WHOIS ' + nick) 272 | await asyncio.sleep(throttle.commands) 273 | if self.services['nickserv']: 274 | await self.sendmsg('NickServ', 'INFO ' + nick) 275 | await asyncio.sleep(throttle.commands) 276 | await self.raw(f'NOTICE {nick} \001VERSION\001') # TODO: check the database if we already have this information to speed things up 277 | await asyncio.sleep(throttle.commands) 278 | await self.raw(f'NOTICE {nick} \001TIME\001') 279 | await asyncio.sleep(throttle.commands) 280 | await self.raw(f'NOTICE {nick} \001CLIENTINFO\001') 281 | await asyncio.sleep(throttle.commands) 282 | await self.raw(f'NOTICE {nick} \001SOURCE\001') 283 | except: 284 | break 285 | else: 286 | del nick 287 | await asyncio.sleep(throttle.whois) 288 | else: 289 | await asyncio.sleep(1) 290 | except asyncio.CancelledError: 291 | pass 292 | except Exception as ex: 293 | error(self.display + '\033[31merror\033[0m - loop_whois', ex) 294 | 295 | async def db(self, event, data): 296 | if event in self.snapshot: 297 | if data not in self.snapshot[event]: 298 | self.snapshot[event].append(data) 299 | else: 300 | self.snapshot[event] = [data,] 301 | 302 | async def listen(self): 303 | while True: 304 | try: 305 | if self.reader.at_eof(): 306 | break 307 | data = await asyncio.wait_for(self.reader.readuntil(b'\r\n'), throttle.ztimeout) 308 | line = data.decode('utf-8').strip() 309 | args = line.split() 310 | event = args[1].upper() 311 | if sys.getsizeof(self.snapshot) >= settings.log_max: 312 | with open(f'logs/{self.server}.json{self.multi}', 'w') as fp: 313 | json.dump(self.snapshot, fp) 314 | self.snapshot = dict() 315 | self.multi = '.1' if not self.multi else '.' + str(int(self.multi[1:])+1) 316 | if args[0].upper() == 'ERROR': 317 | await self.db('ERROR', line) 318 | elif not event.isdigit() and event not in ('CAP','INVITE','JOIN','KICK','KILL','MODE','NICK','NOTICE','PART','PRIVMSG','QUIT','TOPIC','WHO'): 319 | await self.db('RAW', line) 320 | elif event != '401': 321 | await self.db(event, line) 322 | if event in bad.chan and len(args) >= 4: 323 | chan = args[3] 324 | if chan in self.channels['users']: 325 | del self.channels['users'][chan] 326 | error(f'{self.display}\033[31merror\033[0m - {chan}', bad.chan[event]) 327 | elif line.startswith('ERROR :'): 328 | check = [check for check in bad.error if check in line.lower()] 329 | if check: 330 | if check[0] in ('dronebl','dnsbl'): 331 | self.snapshot['proxy'] = True 332 | raise Exception(bad.error[check[0]]) 333 | elif args[0] == 'PING': 334 | await self.raw('PONG ' + args[1][1:]) 335 | elif event == 'KICK' and len(args) >= 4: 336 | chan = args[2] 337 | kicked = args[3] 338 | if kicked == self.nickname: 339 | if chan in self.channels['current']: 340 | self.channels['current'].remove(chan) 341 | elif event == 'MODE' and len(args) == 4: 342 | nick = args[2] 343 | if nick == self.nickname: 344 | mode = args[3][1:] 345 | if mode == '+r': 346 | self.snapshot['registered'] = self.login 347 | elif event == '001': #RPL_WELCOME 348 | host = args[0][1:] 349 | self.snapshot['server'] = self.server 350 | self.snapshot['host'] = host 351 | if len(host) > 25: 352 | self.display = f'{self.server.ljust(18)} \033[1;30m|\033[0m {host[:22]}... \033[1;30m|\033[0m ' 353 | else: 354 | self.display = f'{self.server.ljust(18)} \033[1;30m|\033[0m {host.ljust(25)} \033[1;30m|\033[0m ' 355 | debug(self.display + f'\033[1;32mconnected\033[0m \033[1;30m(port {self.port})\033[0m') 356 | self.loops['init'] = asyncio.create_task(self.loop_initial()) 357 | elif event == '005': 358 | for item in args: 359 | if item.startswith('SSL=') and item[4:]: 360 | if not self.snapshot['ssl']: 361 | self.snapshot['ssl'] = item[4:] 362 | break 363 | elif event == '311' and len(args) >= 4: # RPL_WHOISUSER 364 | nick = args[3] 365 | if 'open proxy' in line.lower() or 'proxy monitor' in line.lower(): 366 | self.snapshot['proxy'] = True 367 | error(self.display + '\033[93mProxy Monitor detected\033[0m', nick) 368 | else: 369 | debug(f'{self.display}\033[34mWHOIS\033[0m {nick}') 370 | elif event == 315 and len(args) >= 3: # RPL_ENDOFWHO 371 | chan = args[3] 372 | await self.raw(f'MODE {chan} +b') 373 | await asyncio.sleep(throttle.commands) 374 | await self.raw(f'MODE {chan} +e') 375 | await asyncio.sleep(throttle.commands) 376 | await self.raw(f'MODE {chan} +I') 377 | await asyncio.sleep(throttle.commands) 378 | await self.raw(f'NOTICE {chan} \001VERSION\001') 379 | await asyncio.sleep(throttle.commands) 380 | await self.raw(f'NOTICE {chan} \001TIME\001') 381 | await asyncio.sleep(throttle.commands) 382 | await self.raw(f'NOTICE {chan} \001CLIENTINFO\001') 383 | await asyncio.sleep(throttle.commands) 384 | await self.raw(f'NOTICE {chan} \001SOURCE\001') 385 | await asyncio.sleep(throttle.part) 386 | await self.raw('PART ' + chan) 387 | self.channels['current'].remove(chan) 388 | elif event == '322' and len(args) >= 4: # RPL_LIST 389 | chan = args[3] 390 | users = args[4] 391 | if users != '0': # no need to JOIN empty channels... 392 | self.channels['all'].append(chan) 393 | self.channels['users'][chan] = users 394 | elif event == '323': # RPL_LISTEND 395 | if self.channels['all']: 396 | debug(self.display + '\033[36mLIST\033[0m found \033[93m{0}\033[0m channel(s)'.format(str(len(self.channels['all'])))) 397 | self.loops['chan'] = asyncio.create_task(self.loop_channels()) 398 | self.loops['nick'] = asyncio.create_task(self.loop_nick()) 399 | self.loops['whois'] = asyncio.create_task(self.loop_whois()) 400 | elif event == '352' and len(args) >= 8: # RPL_WHORPL 401 | nick = args[7] 402 | if nick not in self.nicks['all']+[self.nickname,]: 403 | self.nicks['all'].append(nick) 404 | self.nicks['check'].append(nick) 405 | elif event == '366' and len(args) >= 4: # RPL_ENDOFNAMES 406 | chan = args[3] 407 | self.channels['current'].append(chan) 408 | if chan in self.channels['users']: 409 | debug('{0}\033[32mJOIN\033[0m {1} \033[1;30m(found \033[93m{2}\033[1;30m users)\033[0m'.format(self.display, chan, self.channels['users'][chan])) 410 | del self.channels['users'][chan] 411 | await self.raw('WHO ' + chan) 412 | elif event == '401' and len(args) >= 4: # ERR_NOSUCHNICK 413 | nick = args[3] 414 | if nick == 'ChanServ': 415 | self.services['chanserv'] = False 416 | elif nick == 'NickServ': 417 | self.services['nickserv'] = False 418 | else: 419 | await self.raw('WHOWAS ' + nick) 420 | elif event == '421' and len(args) >= 3: # ERR_UNKNOWNCOMMAND 421 | msg = ' '.join(args[2:]) 422 | if 'You must be connected for' in msg: 423 | error(self.display + '\033[31merror\033[0m - delay found', msg) 424 | elif event == '433': # ERR_NICKINUSE 425 | self.nickname = rndnick() 426 | await self.raw('NICK ' + self.nickname) 427 | elif event == '439' and len(args) >= 11: # ERR_TARGETTOOFAST 428 | target = args[3] 429 | msg = ' '.join(args[4:])[1:] 430 | seconds = args[10] 431 | if target[:1] in ('#','&'): 432 | self.channels['all'].append(target) 433 | if seconds.isdigit(): 434 | self.jthrottle = throttle.seconds if int(seconds) > throttle.seconds else int(seconds) 435 | else: 436 | self.nicks['check'].append(target) 437 | if seconds.isdigit(): 438 | self.nthrottle = throttle.seconds if int(seconds) > throttle.seconds else int(seconds) 439 | error(self.display + '\033[31merror\033[0m - delay found for ' + target, msg) 440 | elif event == '465' and len(args) >= 5: # ERR_YOUREBANNEDCREEP 441 | check = [check for check in bad.error if check in line.lower()] 442 | if check: 443 | if check[0] in ('dronebl','dnsbl'): 444 | self.snapshot['proxy'] = True 445 | raise Exception(bad.error[check[0]]) 446 | elif event == '464': # ERR_PASSWDMISMATCH 447 | raise Exception('Network has a password') 448 | elif event == '487': # ERR_MSGSERVICES 449 | if '"/msg NickServ" is no longer supported' in line: 450 | await self.raw('/NickServ REGISTER {0} {1}'.format(self.login['pass'], self.login['mail'])) 451 | elif event == 'KILL': 452 | nick = args[2] 453 | if nick == self.nickname: 454 | raise Exception('KILL') 455 | elif event in ('NOTICE','PRIVMSG') and len(args) >= 4: 456 | nick = args[0].split('!')[1:] 457 | target = args[2] 458 | msg = ' '.join(args[3:])[1:] 459 | if target == self.nickname: 460 | for i in ('proxy','proxys','proxies'): 461 | if i in msg.lower(): 462 | self.snapshot['proxy'] = True 463 | check = [x for x in ('bopm','hopm') if x in line] 464 | if check: 465 | error(f'{self.display}\033[93m{check[0].upper()} detected\033[0m') 466 | else: 467 | error(self.display + '\033[93mProxy Monitor detected\033[0m') 468 | for i in ('You must have been using this nick for','You must be connected for','not connected long enough','Please wait', 'You cannot list within the first'): 469 | if i in msg: 470 | error(self.display + '\033[31merror\033[0m - delay found', msg) 471 | break 472 | if msg[:8] == '\001VERSION': 473 | version = random.choice(('http://www.mibbit.com ajax IRC Client','mIRC v6.35 Khaled Mardam-Bey','xchat 0.24.1 Linux 2.6.27-8-eeepc i686','rZNC Version 1.0 [02/01/11] - Built from ZNC','thelounge v3.0.0 -- https://thelounge.chat/')) 474 | await self.raw(f'NOTICE {nick} \001VERSION {version}\001') 475 | elif ('You are connected' in line or 'Connected securely via' in line) and ('SSL' in line or 'TLS' in line): 476 | cipher = line.split()[-1:][0].replace('\'','').replace('"','') 477 | self.snapshot['ssl_cipher'] = cipher 478 | elif nick in ('ChanServ','NickServ'): 479 | self.snapshot['services'] = True 480 | if 'is now registered' in msg or f'Nickname {self.nickname} registered' in msg: 481 | debug(self.display + '\033[35mNickServ\033[0m registered') 482 | self.snapshot['registered'] = self.login 483 | elif '!' not in args[0]: 484 | if 'dronebl.org/lookup' in msg: 485 | self.snapshot['proxy'] = True 486 | error(self.display + '\033[93mDroneBL detected\033[0m') 487 | raise Exception('DroneBL') 488 | else: 489 | if [i for i in ('You\'re banned','You are permanently banned','You are banned','You are not welcome','Temporary K-line') if i in msg]: 490 | raise Exception('K-Lined') 491 | except (UnicodeDecodeError, UnicodeEncodeError): 492 | pass 493 | except Exception as ex: 494 | error(self.display + '\033[1;31mdisconnected\033[0m', ex) 495 | break 496 | 497 | async def main(targets): 498 | sema = asyncio.BoundedSemaphore(throttle.threads) # B O U N D E D S E M A P H O R E G A N G 499 | jobs = list() 500 | for target in targets: 501 | server = ':'.join(target.split(':')[-1:]) 502 | if ':' not in target: # TODO: IPv6 addresses without a port wont get :6667 appeneded to it like this 503 | port = 6697 504 | else: 505 | port = int(':'.join(target.split(':')[:-1])) 506 | try: 507 | ipaddress.IPv4Address(server) 508 | jobs.append(asyncio.ensure_future(probe(sema, server, port, 2).run())) 509 | except: 510 | try: 511 | ipaddress.IPv6Address(server) 512 | jobs.append(asyncio.ensure_future(probe(sema, server, port, 10).run())) 513 | except: 514 | error('invalid ip address', server) 515 | await asyncio.gather(*jobs) 516 | 517 | # Main 518 | print('#'*56) 519 | print('#{:^54}#'.format('')) 520 | print('#{:^54}#'.format('Internet Relay Chat Probe (IRCP)')) 521 | print('#{:^54}#'.format('Developed by acidvegas in Python')) 522 | print('#{:^54}#'.format('https://git.acid.vegas/ircp')) 523 | print('#{:^54}#'.format('')) 524 | print('#'*56) 525 | if len(sys.argv) != 2: 526 | raise SystemExit('error: invalid arguments') 527 | else: 528 | targets_file = sys.argv[1] 529 | if not os.path.isfile(targets_file): 530 | raise SystemExit('error: invalid file path') 531 | else: 532 | try: 533 | os.mkdir('logs') 534 | except FileExistsError: 535 | pass 536 | targets = [line.rstrip() for line in open(targets_file).readlines() if line and line not in bad.donotscan] 537 | found = len(targets) 538 | debug(f'loaded {found:,} targets') 539 | if settings.daemon: 540 | try: 541 | os.mkdir('backup') 542 | except FileExistsError: 543 | pass 544 | else: 545 | targets = [target for target in targets if not os.path.isfile(f'logs/{target}.json')] # Do not scan targets we already have logged for 546 | if len(targets) < found: 547 | debug(f'removed {found-len(targets):,} targets we already have logs for already') 548 | del found, targets_file 549 | while True: 550 | random.shuffle(targets) 551 | asyncio.run(main(targets)) 552 | debug('IRCP has finished probing!') 553 | if settings.daemon: 554 | backup(time.strftime('%y%m%d-%H%M%S')) 555 | else: 556 | break 557 | --------------------------------------------------------------------------------