diff --git a/README.md b/README.md index 34082b6..2cf2399 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,17 @@ 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. -Meant to be used in combination with [masscan](https://github.com/robertdavidgraham/masscan) checking **0.0.0.0/0** *(the entire IPv4 range)* for port **6667**. +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. -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. +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. + +## Usage +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. Targets must be a valid IPv4 or IPv6 address & can optionally be suffixed with a port. + +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. ## Order of Operations -First, an attempt to connect using SSL/TLS on port 6697 is made, which will fall back to a standard connection on port 6667 if it fails. The **RPL_ISUPPORT** *(005)* response is checked for the `SSL=` option to try & locate secure ports. +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 standatd & 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. Once connected, server information is gathered from `ADMIN`, `CAP LS`, `MODULES -all`, `VERSION`, `IRCOPS`, `MAP`, `INFO`, `LINKS`, `STATS p`, & `LIST` replies. An attempt to register a nickname is then made by trying to contact NickServ. @@ -21,37 +26,6 @@ Once we have finishing scanning a server, the information found is saved to a JS 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. -## Opt-out -The IRC networks we scanned are PUBLIC networks...any person can freely connect & parse the same information. Send your hate mail to [scan@internetrelaychat.org](mailto://scan@internetrelaychat.org) - -## Config -###### Settings -| Setting | Default Value | Description | -| ------------- | ------------------------------ | ----------------------------------------------------- | -| `errors` | `True` | Show errors in console | -| `errors_conn` | `False` | Show connection errors in console | -| `log_max` | `5000000` | Maximum log size *(in bytes)* before starting another | -| `nickname` | `"IRCP"` | IRC nickname *(`None` = random)* | -| `username` | `"ircp"` | IRC username *(`None` = random)* | -| `realname` | `"internetrelaychat.org"` | IRC realname *(`None` = random)* | -| `ns_mail` | `"scan@internetrelaychat.org"` | NickServ email address *(`None` = random)* | -| `ns_pass` | `"changeme"` | NickServ password *(`None` = random)* | -| `vhost` | `None` | Bind to a specific IP address | - -###### Throttle -| Setting | Default Value | Description | -| ---------- | ------------- | ------------------------------------------------------------- | -| `channels` | `3` | Maximum number of channels to scan at once | -| `delay` | `300` | Delay before registering nick *(if enabled)* & sending `LIST` | -| `join` | `10` | Delay between channel `JOIN` | -| `nick` | `300` | Delay between every random `NICK` change | -| `part` | `10` | Delay before `PART` from channel | -| `seconds` | `300` | Maximum seconds to wait when throttled for `JOIN` | -| `threads` | `100` | Maximum number of threads running | -| `timeout` | `30` | Timeout for all sockets | -| `whois` | `5` | Delay between `WHOIS` requests | -| `ztimeout` | `200` | Timeout for zero data from server | - ## Preview ![](.screens/preview.png) @@ -77,10 +51,12 @@ Mass scanning *default* ports of services is nothing new & though port 6667 is n * Create a seperate log for failed connections * Ability to link multiple IRCP instances running in daemon mode together for balancing * Remote syncing the logs to another server -* Support for handling a target list that contains host:port:ssl for networks on non-standard ports * Give props to [bwall](https://github.com/bwall) for giving me the idea with his [ircsnapshot](https://github.com/bwall/ircsnapshot) repository -* Confirm nick registered *(most likely through MODE +r)* -* Confirm SSL/TLS connections *(most likely through "You are connected using SSL cipher" NOTICE message)* +* Confirm nick registered *(most likely through MODE +r)* *(Log nick & password)* +* Support for hostnames in targets list *(Attempt IPv6 & fallback to IPv4)* + +## Opt-out +The IRC networks we scanned are PUBLIC networks...any person can freely connect & parse the same information. Send your hate mail to [scan@internetrelaychat.org](mailto://scan@internetrelaychat.org) ## Mirrors - [acid.vegas](https://git.acid.vegas/ircp) diff --git a/ircp.py b/ircp.py index 5cf3078..2628e26 100644 --- a/ircp.py +++ b/ircp.py @@ -24,6 +24,7 @@ class settings: class throttle: channels = 3 if not settings.daemon else 2 # Maximum number of channels to scan at once + connect = 15 if not settings.daemon else 60 # Delay between each connection attempt on a diffferent port delay = 300 if not settings.daemon else 600 # Delay before registering nick (if enabled) & sending /LIST join = 10 if not settings.daemon else 30 # Delay between channel JOINs nick = 300 if not settings.daemon else 600 # Delay between every random NICK change @@ -34,44 +35,43 @@ class throttle: whois = 5 if not settings.daemon else 15 # Delay between WHOIS requests ztimeout = 200 if not settings.daemon else 300 # Timeout for zero data from server -donotscan = ( - 'irc.dronebl.org', 'irc.alphachat.net', - '5.9.164.48', '45.32.74.177', '104.238.146.46', '149.248.55.130', - '2001:19f0:6001:1dc::1', '2001:19f0:b001:ce3::1', '2a01:4f8:160:2501:48:164:9:5', '2001:19f0:6401:17c::1' -) - -badchan = { - '403' : 'ERR_NOSUCHCHANNEL', '405' : 'ERR_TOOMANYCHANNELS', - '435' : 'ERR_BANONCHAN', '442' : 'ERR_NOTONCHANNEL', - '448' : 'ERR_FORBIDDENCHANNEL', '470' : 'ERR_LINKCHANNEL', - '471' : 'ERR_CHANNELISFULL', '473' : 'ERR_INVITEONLYCHAN', - '474' : 'ERR_BANNEDFROMCHAN', '475' : 'ERR_BADCHANNELKEY', - '476' : 'ERR_BADCHANMASK', '477' : 'ERR_NEEDREGGEDNICK', - '479' : 'ERR_BADCHANNAME', '480' : 'ERR_THROTTLE', - '485' : 'ERR_CHANBANREASON', '488' : 'ERR_NOSSL', - '489' : 'ERR_SECUREONLYCHAN', '519' : 'ERR_TOOMANYUSERS', - '520' : 'ERR_OPERONLY', '926' : 'ERR_BADCHANNEL' -} - -badserver = { - 'install identd' : 'Identd required', - 'trying to reconnect too fast' : 'Throttled', - 'your host is trying to (re)connect too fast' : 'Throttled', - 'reconnecting too fast' : 'Throttled', - 'access denied' : 'Access denied', - 'not authorized to' : 'Not authorized', - 'not authorised to' : 'Not authorized', - 'password mismatch' : 'Password mismatch', - 'dronebl' : 'DroneBL', - 'dnsbl' : 'DNSBL', - 'g:lined' : 'G:Lined', - 'z:lined' : 'Z:Lined', - 'timeout' : 'Timeout', - 'closing link' : 'Banned', - 'banned' : 'Banned', - 'client exited' : 'QUIT', - 'quit' : 'QUIT' -} +class bad: + donotscan = ( + 'irc.dronebl.org', 'irc.alphachat.net', + '5.9.164.48', '45.32.74.177', '104.238.146.46', '149.248.55.130', + '2001:19f0:6001:1dc::1', '2001:19f0:b001:ce3::1', '2a01:4f8:160:2501:48:164:9:5', '2001:19f0:6401:17c::1' + ) + chan = { + '403' : 'ERR_NOSUCHCHANNEL', '405' : 'ERR_TOOMANYCHANNELS', + '435' : 'ERR_BANONCHAN', '442' : 'ERR_NOTONCHANNEL', + '448' : 'ERR_FORBIDDENCHANNEL', '470' : 'ERR_LINKCHANNEL', + '471' : 'ERR_CHANNELISFULL', '473' : 'ERR_INVITEONLYCHAN', + '474' : 'ERR_BANNEDFROMCHAN', '475' : 'ERR_BADCHANNELKEY', + '476' : 'ERR_BADCHANMASK', '477' : 'ERR_NEEDREGGEDNICK', + '479' : 'ERR_BADCHANNAME', '480' : 'ERR_THROTTLE', + '485' : 'ERR_CHANBANREASON', '488' : 'ERR_NOSSL', + '489' : 'ERR_SECUREONLYCHAN', '519' : 'ERR_TOOMANYUSERS', + '520' : 'ERR_OPERONLY', '926' : 'ERR_BADCHANNEL' + } + error = { + 'install identd' : 'Identd required', + 'trying to reconnect too fast' : 'Throttled', + 'trying to (re)connect too fast' : 'Throttled', + 'reconnecting too fast' : 'Throttled', + 'access denied' : 'Access denied', + 'not authorized to' : 'Not authorized', + 'not authorised to' : 'Not authorized', + 'password mismatch' : 'Password mismatch', + 'dronebl' : 'DroneBL', + 'dnsbl' : 'DNSBL', + 'g:lined' : 'G:Lined', + 'z:lined' : 'Z:Lined', + 'timeout' : 'Timeout', + 'closing link' : 'Banned', + 'banned' : 'Banned', + 'client exited' : 'QUIT', + 'quit' : 'QUIT' + } def backup(name): try: @@ -106,7 +106,8 @@ def ssl_ctx(): class probe: def __init__(self, semaphore, server, port, family=2): self.server = server - self.port = port + self.port = 6697 + self.oport = port self.display = server.ljust(18)+' \033[30m|\033[0m unknown network \033[30m|\033[0m ' self.semaphore = semaphore self.nickname = None @@ -122,15 +123,40 @@ class probe: async def run(self): async with self.semaphore: try: - await self.connect() + await self.connect() # 6697 except Exception as ex: if settings.errors_conn: - error(self.display + '\033[1;31mdisconnected\033[0m - failed to connect using SSL/TLS', ex) - try: - await self.connect(True) - except Exception as ex: - if settings.errors_conn: - error(self.display + '\033[1;31mdisconnected\033[0m - failed to connect', ex) + error(self.display + '\033[1;31mdisconnected\033[0m - failed to connect using SSL/TLS on port ' + str(self.port), ex) + if self.oport not in (6667,6697): + self.port = self.oport + await asyncio.sleep(throttle.connect) + try: + await self.connect() # Non-standard + except Exception as ex: + if settings.errors_conn: + error(self.display + '\033[1;31mdisconnected\033[0m - failed to connect using SSL/TLS on port ' + str(self.port), ex) + self.port = 6667 + await asyncio.sleep(throttle.connect) + try: + await self.connect(True) # 6667 + except Exception as ex: + if settings.errors_conn: + error(self.display + '\033[1;31mdisconnected\033[0m - failed to connect on port ' + str(self.port), ex) + self.port = self.oport + await asyncio.sleep(throttle.connect) + try: + await self.connect(True) # Non-standard + except Exception as ex: + if settings.errors_conn: + error(self.display + '\033[1;31mdisconnected\033[0m - failed to connect on port ' + str(self.port), ex) + else: + self.port = 6667 + await asyncio.sleep(throttle.connect) + try: + await self.connect(True) # 6667 + except Exception as ex: + if settings.errors_conn: + error(self.display + '\033[1;31mdisconnected\033[0m - failed to connect on port ' + str(self.port), ex) async def raw(self, data): self.writer.write(data[:510].encode('utf-8') + b'\r\n') @@ -139,7 +165,7 @@ class probe: async def connect(self, fallback=False): options = { 'host' : self.server, - 'port' : 6667 if fallback else 6697, + 'port' : self.port, 'limit' : 1024, 'ssl' : None if fallback else ssl_ctx(), 'family' : self.family, @@ -152,7 +178,7 @@ class probe: } self.nickname = identity['nick'] self.reader, self.writer = await asyncio.wait_for(asyncio.open_connection(**options), throttle.timeout) - self.snapshot['port'] = option['ports'] + self.snapshot['port'] = option['port'] del options if not fallback: self.snapshot['ssl'] = True @@ -267,17 +293,17 @@ class probe: self.snapshot['RAW'] = self.snapshot['RAW']+[line,] if 'RAW' in self.snapshot else [line,] else: self.snapshot[event] = self.snapshot[event]+[line,] if event in self.snapshot else [line,] - if event in badchan and len(args) >= 4: + if event in bad.chan and len(args) >= 4: chan = args[3] if chan in self.channels['users']: del self.channels['users'][chan] - error(f'{self.display}\033[31merror\033[0m - {chan}', badchan[event]) + error(f'{self.display}\033[31merror\033[0m - {chan}', bad.chan[event]) elif line.startswith('ERROR :'): - check = [check for check in badserver if check in line.lower()] + check = [check for check in bad.error if check in line.lower()] if check: if check[0] in ('dronebl','dnsbl'): self.snapshot['proxy'] = True - raise Exception(badserver[check[0]]) + raise Exception(bad.error[check[0]]) elif args[0] == 'PING': await self.raw('PONG ' + args[1][1:]) elif event == '001': #RPL_WELCOME @@ -350,11 +376,11 @@ class probe: self.jthrottle = throttle.seconds if seconds > throttle.seconds else seconds error(self.display + '\033[31merror\033[0m - delay found', msg) elif event == '465': # ERR_YOUREBANNEDCREEP - check = [check for check in badserver if check in line.lower()] + check = [check for check in bad.error if check in line.lower()] if check: if check[0] in ('dronebl','dnsbl'): self.snapshot['proxy'] = True - raise Exception(badserver[check[0]]) + raise Exception(bad.error[check[0]]) elif event == '464': # ERR_PASSWDMISMATCH raise Exception('Network has a password') elif event == '487': # ERR_MSGSERVICES @@ -418,7 +444,10 @@ async def main(targets): jobs = list() for target in targets: server = ':'.join(target.split(':')[-1:]) - port = ':'.join(target.split(':')[:-1]) + if ':' not in target: # TODO: IPv6 addresses without a port wont get :6667 appeneded to it like this + port = 6697 + else: + port = int(':'.join(target.split(':')[:-1])) try: ipaddress.IPv4Address(server) jobs.append(asyncio.ensure_future(probe(sema, server, port, 2).run())) @@ -427,7 +456,7 @@ async def main(targets): ipaddress.IPv6Address(server) jobs.append(asyncio.ensure_future(probe(sema, server, port, 10).run())) except: - error('failed to scan '+target, 'invalid ip address') + error('invalid ip address', server) await asyncio.gather(*jobs) # Main @@ -449,7 +478,7 @@ else: os.mkdir('logs') except FileExistsError: pass - targets = [line.rstrip() for line in open(targets_file).readlines() if line and line not in donotscan] + targets = [line.rstrip() for line in open(targets_file).readlines() if line and line not in bad.donotscan] found = len(targets) debug(f'loaded {found:,} targets') if settings.daemon: