1
mirror of git://git.acid.vegas/IRCP.git synced 2024-11-26 09:56:42 +00:00
IRCP/ircp.py

383 lines
13 KiB
Python
Raw Normal View History

2023-05-25 20:03:39 +00:00
#!/usr/bin/env python
# internet relay chat probe for https://internetrelaychat.org/ - developed by acidvegas in python (https://git.acid.vegas/ircp)
import asyncio
import copy
import json
import os
import random
import socket
import ssl
import sys
import time
class settings:
errors = False # Show connection errors
nickname = 'IRCP'
username = 'ircp'
realname = 'internetrelaychat.org'
ns_mail = 'ircp@internetrelaychat.org'
ns_pass = 'changeme'
vhost = None # Bind to a specific IP address
class throttle:
channels = 3 # Maximum number of channels to scan at once
delay = 120 # Delay before registering nick (if enabled) & sending /LIST
join = 10 # Delay between channel joins
part = 3 # Delay before leaving a channel
threads = 100 # Maximum number of threads running
timeout = 15 # Timeout for all sockets
whois = 3 # Delay between WHOIS requests
ztimeout = 200 # Timeout for zero data from server
snapshot = {
'server' : None,
'host' : None,
'raw' : [], # All non-classified data is stored in here for analysis
'NOTICE' : None,
'services' : False,
'ssl' : True,
# server information
'001' : None, # RPL_WELCOME
'002' : None, # RPL_YOURHOST
'003' : None, # RPL_CREATED
'004' : None, # RPL_MYINFO
'005' : None, # RPL_ISUPPORT
'006' : None, # RPL_MAP
'018' : None, # RPL_MAPUSERS
'257' : None, # RPL_ADMINLOC1
'258' : None, # RPL_ADMINLOC2
'259' : None, # RPL_ADMINEMAIL
2023-05-26 07:26:45 +00:00
'351' : None, # RPL_VERSION
'364' : None, # RPL_LINKS
'372' : None, # RPL_MOTD
2023-05-25 20:03:39 +00:00
# statistic information (lusers)
'250' : None, # RPL_STATSCONN
'251' : None, # RPL_LUSERCLIENT
'252' : None, # RPL_LUSEROP
'254' : None, # RPL_LUSERCHANNELS
'255' : None, # RPL_LUSERME
'265' : None, # RPL_LOCALUSERS
'266' : None, # RPL_GLOBALUSERS
# channel information
'332' : None, # RPL_TOPIC
'353' : None, # RPL_NAMREPLY
'322' : None, # RPL_LIST
# user information (whois/who)
'311' : None, # RPL_WHOISUSER
'307' : None, # RPL_WHOISREGNICK
'312' : None, # RPL_WHOISSERVER
'671' : None, # RPL_WHOISSECURE
'319' : None, # RPL_WHOISCHANNELS
'320' : None, # RPL_WHOISSPECIAL
'276' : None, # RPL_WHOISCERTFP
'330' : None, # RPL_WHOISACCOUNT
'338' : None, # RPL_WHOISACTUALLY
'352' : None, # RPL_WHOREPLY
2023-05-26 07:26:45 +00:00
# bad channel numerics
2023-05-25 20:03:39 +00:00
'470' : None, # ERR_LINKCHANNEL
'471' : None, # ERR_CHANNELISFULL
'473' : None, # ERR_INVITEONLYCHAN
'474' : None, # ERR_BANNEDFROMCHAN
'475' : None, # ERR_BADCHANNELKEY
'477' : None, # ERR_NEEDREGGEDNICK
'489' : None, # ERR_SECUREONLYCHAN
'519' : None, # ERR_TOOMANYUSERS
'520' : None, # ERR_OPERONLY
2023-05-26 07:26:45 +00:00
# bad server numerics
2023-05-25 20:03:39 +00:00
'464' : None, # ERR_PASSWDMISMATCH
'465' : None, # ERR_YOUREBANNEDCREEP
'466' : None, # ERR_YOUWILLBEBANNED
2023-05-26 07:26:45 +00:00
'421' : None # ERR_UNKNOWNCOMMAND
2023-05-25 20:03:39 +00:00
}
def debug(data):
print('{0} | [~] - {1}'.format(time.strftime('%I:%M:%S'), data))
def error(data, reason=None):
if settings.errors:
print('{0} | [!] - {1} ({2})'.format(time.strftime('%I:%M:%S'), data, str(reason))) if reason else print('{0} | [!] - {1}'.format(time.strftime('%I:%M:%S'), data))
def rndnick():
prefix = random.choice(['st','sn','cr','pl','pr','fr','fl','qu','br','gr','sh','sk','tr','kl','wr','bl']+list('bcdfgklmnprstvwz'))
midfix = random.choice(('aeiou'))+random.choice(('aeiou'))+random.choice(('bcdfgklmnprstvwz'))
suffix = random.choice(['ed','est','er','le','ly','y','ies','iest','ian','ion','est','ing','led','inger']+list('abcdfgklmnprstvwz'))
return prefix+midfix+suffix
def ssl_ctx():
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
return ctx
class probe:
def __init__(self, server, semaphore):
self.server = server
self.semaphore = semaphore
self.snapshot = copy.deepcopy(snapshot) # <--- GET FUCKED PYTHON
self.channels = {'all':list(), 'current':list()}
self.cchannels = dict()
self.nicks = {'all':list(), 'check':list()}
self.loops = {'init':None,'chan':None,'nick':None}
self.reader = None
self.writer = None
async def run(self):
async with self.semaphore:
try:
await self.connect()
except Exception as ex:
error(self.server.ljust(18) + ' | failed to connect using SSL/TLS', ex)
try:
await self.connect(True)
except Exception as ex:
error(self.server.ljust(18) + ' | failed to connect', ex)
async def raw(self, data):
self.writer.write(data[:510].encode('utf-8') + b'\r\n')
await self.writer.drain()
async def connect(self, fallback=False):
options = {
'host' : self.server,
'port' : 6667 if fallback else 6697,
'limit' : 1024,
'ssl' : None if fallback else ssl_ctx(),
'family' : 2, # 2 = IPv4 | 10 = IPv6 (TODO: Check for IPv6 using server DNS)
'local_addr' : settings.vhost
}
identity = {
'nick': settings.nickname if settings.nickname else rndnick(),
'user': settings.username if settings.username else rndnick(),
'real': settings.realname if settings.realname else rndnick()
}
self.reader, self.writer = await asyncio.wait_for(asyncio.open_connection(**options), throttle.timeout)
await self.raw('USER {0} 0 * :{1}'.format(identity['user'], identity['real']))
await self.raw('NICK ' + identity['nick'])
await self.listen()
2023-05-26 07:26:45 +00:00
for item in self.loops:
if self.loops[item]:
self.loops[item].cancel()
2023-05-25 20:03:39 +00:00
for item in [rm for rm in self.snapshot if not self.snapshot[rm]]:
del self.snapshot[item]
with open(f'logs/{self.server.split()[0]}.json', 'w') as fp:
json.dump(self.snapshot, fp)
if '|' in self.server:
debug(self.server + 'finished scanning')
else:
debug(self.server.ljust(18) + ' | finished scanning')
async def loop_initial(self):
try:
await asyncio.sleep(throttle.delay)
login = {
'pass': settings.ns_pass if settings.ns_pass else rndnick(),
'mail': settings.ns_mail if settings.ns_mail else '{rndnick()}@{rndnick()}.'+random.choice(('com','net','org'))
}
for command in ('ADMIN', 'VERSION', 'LINKS', 'MAP', 'PRIVMSG NickServ :REGISTER {0} {1}'.format(login['pass'], login['mail']), 'LIST'):
try:
await self.raw(command)
except:
break
else:
await asyncio.sleep(1.5)
if not self.channels['all']:
error(self.server + 'no channels found')
await self.raw('QUIT')
except asyncio.CancelledError:
pass
async def loop_channels(self):
try:
while self.channels['all']:
while len(self.channels['current']) >= throttle.channels:
await asyncio.sleep(1)
chan = random.choice(self.channels['all'])
self.channels['all'].remove(chan)
try:
await self.raw('JOIN ' + chan)
except:
break
else:
await asyncio.sleep(throttle.join)
del self.cchannels[chan]
while self.nicks['check']:
await asyncio.sleep(1)
self.loops['nick'].cancel()
await self.raw('QUIT')
except asyncio.CancelledError:
pass
async def loop_whois(self):
try:
while True:
if self.nicks['check']:
nick = random.choice(self.nicks['check'])
self.nicks['check'].remove(nick)
2023-05-26 01:22:42 +00:00
debug(self.server + 'WHOIS ' + nick)
2023-05-25 20:03:39 +00:00
try:
await self.raw('WHOIS ' + nick)
except:
break
else:
await asyncio.sleep(throttle.whois)
else:
await asyncio.sleep(1)
except asyncio.CancelledError:
pass
def parsers(self, line):
args = line.split()
numeric = args[1]
2023-05-26 07:26:45 +00:00
data = ' '.join(args[3:])[1:]
2023-05-25 20:03:39 +00:00
if numeric == '001' and len(args) >= 7:
return args[6] if data.lower().startswith('welcome to the ') else line
2023-05-26 07:26:45 +00:00
elif numeric == '002':
return line.split('running version')[1] if 'running version' in line else line
elif numeric == '003':
check = [item for item in ('This server was cobbled together ','This server was created ','This server has been started ','This server was last re(started) on ','This server was last (re)started on ') if data.startswith(item)]
return data.replace(check[0],'') if check else line
elif numeric == '004':
return args[4] if len(args) >= 5 else line
elif numeric == '005':
return data.split(' :')[0]
2023-05-25 20:03:39 +00:00
async def listen(self):
2023-05-26 07:26:45 +00:00
while not self.reader.at_eof():
try:
2023-05-25 20:03:39 +00:00
data = await asyncio.wait_for(self.reader.readuntil(b'\r\n'), throttle.ztimeout)
line = data.decode('utf-8').strip()
#debug(line)
args = line.split()
numeric = args[1]
if line.startswith('ERROR :Closing Link:'):
raise Exception('Connection has closed.')
elif args[0] == 'PING':
await self.raw('PONG ' + args[1][1:])
elif numeric == '001': #RPL_WELCOME
host = args[0][1:]
self.snapshot['server'] = self.server
self.snapshot['host'] = host
if len(host) > 25:
self.server = f'{self.server.ljust(18)} | {host[:25]} | '
else:
self.server = f'{self.server.ljust(18)} | {host.ljust(25)} | '
debug(self.server + 'connected')
self.loops['init'] = asyncio.create_task(self.loop_initial())
elif numeric == '322' and len(args) >= 5: # RPL_LIST
chan = args[3]
users = args[4]
self.channels['all'].append(chan)
self.cchannels[chan] = users
elif numeric == '323': # RPL_LISTEND
if self.channels['all']:
debug(self.server + 'found {0} channel(s)'.format(str(len(self.channels['all']))))
self.loops['chan'] = asyncio.create_task(self.loop_channels())
self.loops['nick'] = asyncio.create_task(self.loop_whois())
elif numeric == '352' and len(args) >= 8: # RPL_WHORPL
nick = args[7]
if nick not in self.nicks['all']:
self.nicks['all'].append(nick)
self.nicks['check'].append(nick)
elif numeric == '366' and len(args) >= 4: # RPL_ENDOFNAMES
chan = args[3]
self.channels['current'].append(chan)
if chan in self.cchannels:
debug(self.server + f'scanning {self.cchannels[chan].ljust(4)} users in {chan}')
else:
debug(self.server + f'scanning users in {chan}')
await self.raw('WHO ' + chan)
await asyncio.sleep(throttle.part)
await self.raw('PART ' + chan)
self.channels['current'].remove(chan)
elif numeric == '421' and len(args) >= 3: # ERR_UNKNOWNCOMMAND
msg = ' '.join(args[2:])
if 'You must be connected for' in msg:
error(self.server + 'delay found', msg)
elif numeric == '433': # ERR_NICKINUSE
if not settings.nickname:
await self.raw('NICK ' + rndnick())
else:
await self.raw('NICK ' + settings.nickname + str(random.randint(1000,9999)))
2023-05-26 01:22:42 +00:00
elif numeric == '465': # ERR_YOUREBANNEDCREEP
if 'dronebl' in line:
error(self.server + 'dronebl detected')
2023-05-25 20:03:39 +00:00
elif numeric == '464': # ERR_PASSWDMISMATCH
error(self.server + 'network has a password')
2023-05-26 01:22:42 +00:00
elif numeric == 'NOTICE' and len(args) >= 4:
2023-05-25 20:03:39 +00:00
nick = args[0].split('!')[1:]
msg = ' '.join(args[3:])[1:]
if nick == 'NickServ':
self.snapshot['services'] = True
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'):
if i in msg:
error(self.server + 'delay found', msg)
2023-05-26 01:22:42 +00:00
elif numeric == 'PRIVMSG' and len(args) >= 4:
2023-05-25 20:03:39 +00:00
nick = args[0].split('!')[0][1:]
2023-05-26 01:22:42 +00:00
msg = ' '.join(args[3:])[1:]
2023-05-25 20:03:39 +00:00
if nick == 'NickServ':
self.snapshot['services'] = True
2023-05-26 01:22:42 +00:00
if msg[:8] == '\001VERSION':
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/')
await self.raw(f'NOTICE {nick} \001VERSION {version}\001')
2023-05-25 20:03:39 +00:00
if numeric in self.snapshot:
if not self.snapshot[numeric]:
self.snapshot[numeric] = line
elif line not in self.snapshot[numeric]:
if type(self.snapshot[numeric]) == list:
self.snapshot[numeric].append(line)
elif type(self.snapshot[numeric]) == str:
self.snapshot[numeric] = [self.snapshot[numeric], line]
else:
self.snapshot['raw'].append(line)
2023-05-26 07:26:45 +00:00
except (UnicodeDecodeError, UnicodeEncodeError):
pass
except Exception as ex:
if '|' in self.server:
error(self.server + 'fatal error occured', ex)
else:
error(self.server.ljust(18) + 'fatal error occured', ex)
break
2023-05-25 20:03:39 +00:00
async def main(targets):
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
jobs = list()
for target in targets:
jobs.append(asyncio.ensure_future(probe(target, sema).run()))
await asyncio.gather(*jobs)
# Main
print('#'*56)
print('#{:^54}#'.format(''))
print('#{:^54}#'.format('Internet Relay Chat Probe (IRCP)'))
print('#{:^54}#'.format('Developed by acidvegas in Python'))
print('#{:^54}#'.format('https://git.acid.vegas/ircp'))
print('#{:^54}#'.format(''))
print('#'*56)
if len(sys.argv) != 2:
raise SystemExit('error: invalid arguments')
else:
targets_file = sys.argv[1]
if not os.path.isfile(targets_file):
raise SystemExit('error: invalid file path')
else:
targets = [line.rstrip() for line in open(targets_file).readlines() if line]
found = len(targets)
debug(f'loaded {found:,} targets')
targets = [target for target in targets if not os.path.isfile(f'logs/{target}.json')] # Do not scan targets we already have logged for
if len(targets) < found:
debug(f'removed {found-len(targets):,} targets we already have logs for already')
random.shuffle(targets)
try:
os.mkdir('logs')
except FileExistsError:
pass
loop = asyncio.get_event_loop()
loop.run_until_complete(main(targets))
debug('IRCP has finished probing!')