mirror of
git://git.acid.vegas/IRCP.git
synced 2024-11-21 15:36:41 +00:00
Initial commit
This commit is contained in:
commit
d904c9ab97
BIN
.screens/ircp.png
Normal file
BIN
.screens/ircp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 225 KiB |
15
LICENSE
Normal file
15
LICENSE
Normal 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
101
README.md
Normal 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
364
ircp.py
Normal 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!')
|
Loading…
Reference in New Issue
Block a user