Initial commit

This commit is contained in:
Dionysus 2023-05-25 16:03:39 -04:00
commit d904c9ab97
Signed by: acidvegas
GPG Key ID: EF4B922DB85DC9DE
4 changed files with 480 additions and 0 deletions

BIN
.screens/ircp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

15
LICENSE Normal file
View File

@ -0,0 +1,15 @@
ISC License
Copyright (c) 2023, acidvegas <acid.vegas@acid.vegas>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

101
README.md Normal file
View File

@ -0,0 +1,101 @@
# Internet Relay Chat Probe (IRCP)
![](.screens/ircp.png)
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**.
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.
## Order of Operations
First, an attempt to connect using SSL/TLS on port 6697 is made, which if it fails, will fall back to a standard connection on port 6667.
Once connected, server information is gathered from `LUSERS`, `VERSION`, `LINKS`, `MAP`, `ADMIN`, `MOTD`, `LIST`, replies.
An attempt to register a nickname is then made by trying to contact NickServ.
Next, every channel is joined with a `WHO` command sent & every new nick found gets a `WHOIS`.
## Collected Information
All of the raw data from a server is logged & stored. The categories below are stored seperately & hilight the key information we are after:
###### Server Information
| Numeric | Title |
| ------- | -------------- |
| 001 | RPL_WELCOME |
| 002 | RPL_YOURHOST |
| 003 | RPL_CREATED |
| 004 | RPL_MYINFO |
| 005 | RPL_ISUPPORT |
| 372 | RPL_MOTD |
| 351 | RPL_VERSION |
| 364 | RPL_LINKS |
| 006 | RPL_MAP |
| 018 | RPL_MAPUSERS |
| 257 | RPL_ADMINLOC1 |
| 258 | RPL_ADMINLOC2 |
| 259 | RPL_ADMINEMAIL |
###### Statistics Information (LUSERS)
| Numeric | Title |
| ------- | ----------------- |
| 250 | RPL_STATSCONN |
| 251 | RPL_LUSERCLIENT |
| 252 | RPL_LUSEROP |
| 254 | RPL_LUSERCHANNELS |
| 255 | RPL_LUSERME |
| 265 | RPL_LOCALUSERS |
| 266 | RPL_GLOBALUSERS |
###### Channel Information
| Numeric | Title |
| ------- | ------------ |
| 332 | RPL_TOPIC |
| 353 | RPL_NAMREPLY |
| 322 | RPL_LIST |
###### User Information (WHOIS/WHO)
| Numeric | Title |
| ------- | ----------------- |
| 311 | RPL_WHOISUSER |
| 307 | RPL_WHOISREGNICK |
| 312 | RPL_WHOISSERVER |
| 671 | RPL_WHOISSECURE |
| 319 | RPL_WHOISCHANNELS |
| 320 | RPL_WHOISSPECIAL |
| 276 | RPL_WHOISCERTFP |
| 330 | RPL_WHOISACCOUNT |
| 338 | RPL_WHOISACTUALLY |
| 352 | RPL_WHOREPLY |
###### Bad Numerics
| Numeric | Title |
| ------- | -------------------- |
| 470 | ERR_LINKCHANNEL |
| 471 | ERR_CHANNELISFULL |
| 473 | ERR_INVITEONLYCHAN |
| 474 | ERR_BANNEDFROMCHAN |
| 475 | ERR_BADCHANNELKEY |
| 477 | ERR_NEEDREGGEDNICK |
| 489 | ERR_SECUREONLYCHAN |
| 519 | ERR_TOOMANYUSERS |
| 520 | ERR_OPERONLY |
| 464 | ERR_PASSWDMISMATCH |
| 465 | ERR_YOUREBANNEDCREEP |
| 466 | ERR_YOUWILLBEBANNED |
| 421 | ERR_UNKNOWNCOMMAND |
## Todo
* Capture `IRCOPS` & `STATS p` command outputs
* Built in identd & CTCP replies
* Checking for IPv6 availability *(Need to find the server DNS, link names are not required to have DNS entries)*
* Random nick changes for stealth on larger networks
* Create a helper script for parsing logs & generating statistics on data
* Parse only certain information for numerics to cut down on log sizes *(Important for scaling)*
## Mirrors
- [acid.vegas](https://git.acid.vegas/ircp)
- [GitHub](https://github.com/acidvegas/ircp)
- [GitLab](https://gitlab.com/acidvegas/ircp)
- [SuperNETs](https://git.supernets.org/acidvegas/ircp)

364
ircp.py Normal file
View File

@ -0,0 +1,364 @@
#!/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
'372' : None, # RPL_MOTD
'351' : None, # RPL_VERSION
'364' : None, # RPL_LINKS
'006' : None, # RPL_MAP
'018' : None, # RPL_MAPUSERS
'257' : None, # RPL_ADMINLOC1
'258' : None, # RPL_ADMINLOC2
'259' : None, # RPL_ADMINEMAIL
# 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
# bad numerics
'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
'464' : None, # ERR_PASSWDMISMATCH
'465' : None, # ERR_YOUREBANNEDCREEP
'466' : None, # ERR_YOUWILLBEBANNED
'421' : None # ERR_UNKNOWNCOMMAND (unreal command throttling)
}
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()
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)
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]
data = args[3:][1:]
if numeric == '001' and len(args) >= 7:
return args[6] if data.lower().startswith('welcome to the ') else line
async def listen(self):
try:
while not self.reader.at_eof():
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)))
elif numeric == '464': # ERR_PASSWDMISMATCH
error(self.server + 'network has a password')
elif numeric == 'NOTICE':
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)
elif numeric == 'PRIVMSG':
nick = args[0].split('!')[0][1:]
if nick == 'NickServ':
self.snapshot['services'] = True
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)
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)
finally:
for item in self.loops:
if self.loops[item]:
self.loops[item].cancel()
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!')