jupiter

- efnet irc botnet
git clone git://git.acid.vegas/jupiter.git
Log | Files | Refs | Archive | README | LICENSE

commit 601c513aa8befe570bb6b81c8c18a88e83501ded
Author: acidvegas <acid.vegas@acid.vegas>
Date: Thu, 29 Jun 2023 03:20:50 -0400

Initial commit

Diffstat:
A.screens/jupiter.png | 0
ALICENSE | 15+++++++++++++++
AREADME.md | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ajupiter.py | 408+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

4 files changed, 502 insertions(+), 0 deletions(-)

diff --git a/.screens/jupiter.png b/.screens/jupiter.png
Binary files differ.
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,15 @@
+ISC License
+
+Copyright (c) 2021, 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.
diff --git a/README.md b/README.md
@@ -0,0 +1,79 @@
+# Jupiter ♃
+> internet relay chat botnet for efnet
+
+![](.screens/jupiter.png)
+
+*"let the battle of the +oooo -oooo commence"*
+
+## Information
+Jupiter will create a botnet by connecting a defined number of clones to every [EFNet](http://efnet.org) server. A single host could potentially create close to 100 clones without any suspicion. It is meant to monitor/jupe/hold nicks & be controlled to do just about anything.
+
+For example, at the time of writing this, there are 12 active [EFNet](http://efnet.org) servers. With 3 clones per-server on IPv4 connections, plus another 3 clones per-server on IPv6 connections, thats 6 clones per-server, equating to 72 total clones...all from a single machine. Run this bot on multiple machines, you get the point.
+
+Any server with SSL/TLS ports opened, will be connected using SSL/TLS. If using SSL/TLS to connect fails, it will fall back to a standard connection on port 6667 & will try an SSL/TLS again next time. When IPv6 is enabled, Servers with IPv6 support will be connected to with both IPv4 & IPv6 clones. Juping is handled using [MONITOR](https://ircv3.net/specs/extensions/monitor) to watch for nick changes or quits. The bots will also join a backup channel in-case the main channel gets killed & you need to find your bots. The backup channel is suffixed with random numbers & can be searched for from doing /LIST.
+
+The bot is designed to be very minimal, secure, & trustless by nature. This means anyone can run a copy of your script on their server to help build your botnet.
+
+It is highly recommended that you use a [random spoofing ident protocol daemon](https://github.com/acidvegas/random/blob/master/irc/identd.py)
+
+## Commands
+| Command                | Description                                                                                                |
+| ---------------------- | ---------------------------------------------------------------------------------------------------------- |
+| `5000 <chan>`          | Emulates SuperNETs #5000 channel *(Join #5000 on irc.supernets.org for help using this command)*           |
+| `id`                   | Send bot identity                                                                                          |
+| `raw [-d] <data>`      | Send `<data>` to server, optionally delayed with -d argument                                               |
+| `relay <chan>`         | Relay all data from `<chan>` into the bot channel *(Can not use @all & must join channel via `raw` first)* |
+| `relay stop`           | Stop the relay *(Will not turn off from kicks, etc)*                                                       |
+| `monitor list`         | Return MONITOR list                                                                                        |
+| `monitor reset`        | Reset MONITOR list                                                                                         |
+| `monitor <+/-><nicks>` | Add (+) or Remove (-) `<nicks>` from MONITOR list. *(Can be a single nick or comma seperated list)*        |
+| `sync`                 | Sync the bot list *(Handled automatically but good practice to sync occasionally)*                         |
+
+**Note:** All commands must be prefixed with `@all` or the bots nick & will work in a channel or private message.
+
+Raw data must be [IRC RFC](https://www.rfc-editor.org/rfc/rfc2812) compliant data & any nicks in the **MONITOR** list will be juped as soon as they become available.
+
+## EFNet Network Map
+This is an accurate map of the [EFNet](http://efnet.org) IRC network as of *05/19/2023*:
+
+| Host                         | DNS                                                        |
+| ---------------------------- | ---------------------------------------------------------- |
+| 128.39.65.230                | irc.underworld.no                                          |
+| 130.226.213.194              | efnet.deic.eu                                              |
+| 130.243.52.250               | irc.du.se                                                  |
+| 185.100.59.59                | irc.efnet.nl                                               |
+| 188.240.145.90               | irc.swepipe.se                                             |
+| 195.140.202.142              | efnet.port80.se                                            |
+| 195.159.90.90                | irc.homelien.no                                            |
+| 198.47.99.99                 | irc.mzima.net                                              |
+| 198.252.144.2                | irc.colosolutions.net                                      |
+| 209.222.22.22                | irc.choopa.net                                             |
+| 66.225.225.225               | irc.servercentral.net                                      |
+| 67.218.118.62                | irc.Prison.NET                                             |
+| 2001:16d8:aaaa:2::1338       | efnet.port80.se                                            |
+| 2001:668:117::dead:beef:cafe | irc.mzima.net                                              |
+| ~~2001:1838:1007::6667~~     | unknown or offline *(was in the irc.efnet.org roundrobin)* |
+| 2001:19f0::dead:beef:cafe    | irc.choopa.net                                             |
+| 2001:6b0:78::90              | irc.swepipe.se                                             |
+| 2001:67c:12d8::6667          | irc.efnet.nl                                               |
+| 2001:700:3100:1::babe        | irc.underworld.no                                          |
+| 2001:840:0:1000:1::1         | irc.homelien.no                                            |
+| 2001:878:0:e000:82:e2:d5:c2  | efnet.deic.eu                                              |
+| ~~2001:948:7:7::139~~        | unknown or offline *(was in the irc.efnet.org roundrobin)* |
+
+**Note:** Not every host is included in the *irc.efnet.org* roundrobin!
+
+## Todo
+- Ability to set admin/channel on the fly *(requested by delorean)*
+- Built in identd server with randomized spoofing responses
+- Improved protections *(Remove bans placed on bots, retaliate on KICK & +b)*
+- Invite clones to +i channels
+- Takeover attack features
+- Possibly use only one connection per-server & create clones on `multiple` command / destroy clones on `destroy` command. *(No point in having clones when we arent doing anything with them)*
+- Compile a list of common CTCP VERSION replies to improve the random CTCP VERSION responses
+- WHO channel and parse unique hosts to +eI usage
+
+___
+
+###### Mirrors
+[acid.vegas](https://git.acid.vegas/jupiter) • [GitHub](https://github.com/acidvegas/jupiter) • [GitLab](https://gitlab.com/acidvegas/jupiter) • [SourceHut](https://git.sr.ht/~acidvegas/jupiter) • [SuperNETs](https://git.supernets.org/acidvegas/jupiter)
diff --git a/jupiter.py b/jupiter.py
@@ -0,0 +1,407 @@
+#!/usr/bin/env python
+# jupiter: internet relay chat botnet for efnet - developed by acidvegas in python (https://git.acid.vegas/jupiter)
+
+''' A M P L I S S I M U S   M A C H I N A '''
+
+import asyncio
+import copy
+import random
+import re
+import socket
+import ssl
+import time
+
+# Connection
+servers = (
+	{'server':'efnet.deic.eu',         'ssl':6697, 'ipv6': True},
+	{'server':'efnet.port80.se',       'ssl':6697, 'ipv6': True},
+   #{'server':'efnet.portlane.se',     'ssl':6697, 'ipv6': True}, # Removed (efnet.portlane.se is an alias for irc.efnet.org)
+	{'server':'irc.choopa.net',        'ssl':9000, 'ipv6': True},
+	{'server':'irc.colosolutions.net', 'ssl':None, 'ipv6':False}, # error: SSL handshake failed: unsafe legacy renegotiation disabled
+	{'server':'irc.du.se',             'ssl':None, 'ipv6':False}, # error: handshake failed: dh key too small
+   #{'server':'irc.efnet.fr',          'ssl':6697, 'ipv6': True}, # Removed (irc.efnet.fr is an alias for irc.efnet.nl)
+	{'server':'irc.efnet.nl',          'ssl':6697, 'ipv6': True},
+	{'server':'irc.homelien.no',       'ssl':6697, 'ipv6': True},
+	{'server':'irc.mzima.net',         'ssl':6697, 'ipv6': True},
+   #{'server':'irc.nordunet.se',       'ssl':6697, 'ipv6': True}, # Removed (irc.nordunet.se is an alias for irc.swepipe.se)
+	{'server':'irc.prison.net',        'ssl':None, 'ipv6':False},
+	{'server':'irc.swepipe.se',        'ssl':6697, 'ipv6': True},
+	{'server':'irc.underworld.no',     'ssl':6697, 'ipv6': True},
+	{'server':'irc.servercentral.net', 'ssl':9999, 'ipv6':False}
+)
+ipv6     = True # Set to False if your system does not have an IPv6 address
+channel  = '#jupiter'
+backup   = '#jupiter-' + str(random.randint(1000,9999)) # Use /list -re #jupiter-* on weechat to find your bots
+key      = 'xChangeMex'
+
+# Settings
+admin           = 'nick!user@host' # Can use wildcards (Must be in nick!user@host format)
+concurrency     = 3                # Number of clones to load per server
+id              = 'TEST'           # Unique ID so you can tell which bots belong what server
+
+# Formatting Control Characters / Color Codes
+bold        = '\x02'
+reset       = '\x0f'
+green       = '03'
+red         = '04'
+purple      = '06'
+orange      = '07'
+yellow      = '08'
+light_green = '09'
+cyan        = '10'
+light_cyan  = '11'
+light_blue  = '12'
+pink        = '13'
+grey        = '14'
+
+# Globals
+bots     = list()
+callerid = list()
+
+def botcontrol(action, data, ci=False):
+	global bots, callerid
+	if action == '+':
+		if ci:
+			if data not in callerid:
+				callerid.append(data)
+		else:
+			if data not in bots:
+				bots.append(data)
+	elif action == '-':
+		if ci:
+			if data in callerid:
+				callerid.remove(data)
+		else:
+			if data in bots:
+				bots.remove(data)
+
+def color(msg, foreground, background=None):
+	return f'\x03{foreground},{background}{msg}{reset}' if background else f'\x03{foreground}{msg}{reset}'
+
+def debug(data):
+	print('{0} | [~] - {1}'.format(time.strftime('%I:%M:%S'), data))
+
+def error(data, reason=None):
+	print('{0} | [!] - {1} ({2})'.format(time.strftime('%I:%M:%S'), data, str(repr(reason)))) if reason else print('{0} | [!] - {1}'.format(time.strftime('%I:%M:%S'), data))
+
+def is_admin(ident):
+	return re.compile(admin.replace('*','.*')).search(ident)
+
+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
+
+def unicode():
+	msg='\u202e\u0007\x03' + str(random.randint(2,14))
+	for i in range(random.randint(150, 200)):
+		msg += chr(random.randint(0x1000,0x3000))
+	return msg
+
+class clone():
+	def __init__(self, server, use_ipv6=False):
+		self.server     = server
+		self.use_ipv6   = use_ipv6
+		self.ssl_status = True
+		self.nickname   = rndnick()
+		self.host       = self.nickname + '!*@*'
+		self.monlist    = list()
+		self.landmine   = None
+		self.relay      = None
+		self.loop       = None
+		self.reader     = None
+		self.writer     = None
+
+	async def connect(self):
+		await asyncio.sleep(random.randint(300,900))
+		while True:
+			try:
+				options = {
+					'host'       : self.server['server'],
+					'port'       : self.server['ssl'] if self.server['ssl'] and self.ssl_status else 6667,
+					'limit'      : 1024,
+					'ssl'        : ssl_ctx() if self.server['ssl'] and self.ssl_status else None,
+					'family'     : socket.AF_INET6 if self.use_ipv6 else socket.AF_INET
+				}
+				self.reader, self.writer = await asyncio.wait_for(asyncio.open_connection(**options), 15)
+				await self.raw(f'USER {rndnick()} 0 * :{rndnick()}')
+				await self.raw('NICK ' + self.nickname)
+			except Exception as ex:
+				v6 = 'using IPv6 ' if self.use_ipv6 else ''
+				if self.ssl_status and self.server['ssl']:
+					self.ssl_status = False
+					error('Failed to connect to \'{0}\' IRC server {1}on port {2} using SSL/TLS'.format(self.server['server'], v6, str(self.server['ssl'])), ex)
+				else:
+					if not self.ssl_status and self.server['ssl']:
+						self.ssl_status = True
+					error('Failed to connect to \'{0}\' IRC server {1}'.format(self.server['server'], v6), ex)
+			else:
+				await self.listen()
+				self.loop.cancel()
+			finally:
+				await asyncio.sleep(86400+random.randint(1800,3600))
+
+	async def event_message(self, ident, nick, target, msg):
+		if target == self.relay:
+			await asyncio.sleep(0.5)
+			await self.sendmsg(channel, '[{0}] <{1}>: {2}'.format(color(target, cyan), color(nick[:15].ljust(15), purple), msg))
+		if is_admin(ident):
+			args = msg.split()
+			if args[0] in ('@all',self.nickname) and len(args) >= 2:
+				if len(args) == 2:
+					if args[1] == 'id':
+						await self.sendmsg(target, id)
+					elif args[1] == 'sync' and args[0] == self.nickname: # NOTE: Do we need sync? Seems to work without it... (sync adds admin to botlist and wont +o swarm everyone if you +o a bot)
+						await self.raw('WHO ' + channel)
+				elif len(args) == 3:
+					if args[1] == '5000':
+						chan = args[2]
+						if chan == 'stop':
+							self.landmine = None
+							await self.sendmsg(channel, '5000 mode turned off')
+						elif chan[:1] == '#':
+							self.landmine = chan
+							await self.sendmsg(channel, '5000 mode actived on ' + color(chan, cyan))
+					elif args[1] == 'monitor':
+						if args[2] == 'list' and self.monlist:
+							await self.sendmsg(target, '[{0}] {1}'.format(color('Monitor', light_blue), ', '.join(self.monlist)))
+						elif args[2] == 'reset' and self.monlist:
+							await self.monitor('C')
+							self.monlist = list()
+							await self.sendmsg(target, '{0} nick(s) have been {1} from the monitor list.'.format(color(str(len(self.monlist)), cyan), color('removed', red)))
+						elif args[2][:1] == '+':
+							nicks = [mon_nick for mon_nick in set(args[2][1:].split(',')) if mon_nick not in self.monlist]
+							if nicks:
+								await self.monitor('+', nicks)
+								self.monlist += nicks
+								await self.sendmsg(target, '{0} nick(s) have been {1} to the monitor list.'.format(color(str(len(nicks)), cyan), color('added', green)))
+						elif args[2][:1] == '-':
+							nicks = [mon_nick for mon_nick in set(args[2][1:].split(',')) if mon_nick in self.monlist]
+							if nicks:
+								await self.monitor('-', nicks)
+								for mon_nick in nicks:
+									self.monlist.remove(mon_nick)
+								await self.sendmsg(target, '{0} nick(s) have been {1} from the monitor list.'.format(color(str(len(nicks)), cyan), color('removed', red)))
+					elif args[1] == 'relay' and args[0] == self.nickname:
+						chan = args[2]
+						if chan == 'stop':
+							self.relay = None
+							await self.sendmsg(channel, 'Relay turned off')
+						elif chan[:1] == '#':
+							self.relay = chan
+							await self.sendmsg(channel, 'Monitoring ' + color(chan, cyan))
+				elif len(args) >= 4 and args[1] == 'raw':
+					if args[2] == '-d':
+						data = ' '.join(args[3:])
+						self.loops = asyncio.create_task(self.raw(data,True))
+					else:
+						data = ' '.join(args[2:])
+						await self.raw(data)
+		elif target == self.nickname:
+			if msg.startswith('\x01ACTION'):
+				await self.sendmsg(channel, '[{0}] {1}{2}{3} * {4}'.format(color('PM', red), color('<', grey), color(nick, yellow), color('>', grey), msg[8:][:-1]))
+			else:
+				await self.sendmsg(channel, '[{0}] {1}{2}{3} {4}'.format(color('PM', red), color('<', grey), color(nick, yellow), color('>', grey), msg))
+
+	async def event_mode(self, nick, chan, modes):
+		if chan == backup and modes == '+nt' and key:
+			await self.mode(backup, '+mk' + key)
+		elif ('e' in modes or 'I' in modes) and self.host in modes:
+			if nick not in bots:
+				await self.mode(chan, f'+eI *!*@{self.host} *!*@{self.host}') # Quick and dirty +eI recovery
+		else:
+			nicks = modes.split()[1:]
+			modes = modes.split()[0]
+			if 'o' in modes:
+				state = None
+				op = False
+				lostop = list()
+				for item in modes:
+					if item in ('+-'):
+						state = item
+					else:
+						if nicks:
+							current = nicks.pop(0)
+							if current == self.nickname and item == 'o':
+								op = True if state == '+' else False
+							elif current in bots and item == 'o' and state == '-':
+								lostop.append(current)
+				if op:
+					if nick not in bots:
+						_bots = copy.deepcopy(bots)
+						random.shuffle(_bots)
+						_bots = [_bots[i:i+4] for i in range(0, len(_bots), 4)]
+						for clones in _bots:
+							await self.mode(chan, '+oooo ' + ' '.join(clones))
+					await self.mode(chan, f'+eI *!*@{self.host} *!*@{self.host}')
+					await self.mode(chan, f'+eI {unicode()[:10]}!{unicode()[:10]}@{unicode()[:10]} {unicode()[:10]}!{unicode()[:10]}@{unicode()[:10]}')
+				elif lostop:
+					await self.mode(chan, '+' + 'o'*len(lostop) + ' ' + ' '.join(lostop))
+					await self.raw(f'KICK {chan} {nick} {unicode()}')
+					await self.mode(chan, f'+b {nick}!*@*')
+					await self.sendmsg(chan, f'{unicode()} oh god what is happening {unicode()}')
+			#await self.mode(chan, '+eeee ')                     # Set +b exemption on bots
+			#await self.mode(chan, '+IIII ')                     # Set +I exemption on bots
+			#await self.mode(chan, '+imk ' + random.randint(1000,9999)
+			#await self.raw('KICK {chan} {nick} {unicode()}')    # Kick everyone using unifuck as the kick reason
+			#await self.mode(chan, '+bbbb ')                     # Ban every user
+			#await self.mode(chan, '+bbb *!*@* *!*@*.* *!*@*:*') # Ban everyone
+
+	async def listen(self):
+		while not self.reader.at_eof():
+			try:
+				data = await asyncio.wait_for(self.reader.readuntil(b'\r\n'), 600)
+				line = data.decode('utf-8').strip()
+				args = line.split()
+				if line.startswith('ERROR :Closing Link:'):
+					raise Exception('Banned')
+				elif line.startswith('ERROR :Reconnecting too fast'):
+					raise Exception('Throttled')
+				elif args[0] == 'PING':
+					await self.raw('PONG ' + args[1][1:])
+				elif args[1] == '001': # RPL_WELCOME
+					if self.monlist:
+						await self.monitor('+', self.monlist)
+					await self.raw(f'JOIN {channel} {key}') if key else await self.raw('JOIN ' + channel)
+					await self.raw(f'JOIN {backup}  {key}') if key else await self.raw('JOIN ' +  backup)
+				elif args[1] == '315': # RPL_ENDOFWHO
+					await self.sendmsg(channel, 'Sync complete')
+				elif args[1] == '352' and len(args) >= 8: # RPL_WHOREPLY
+					nick = args[7]
+					botcontrol('+',nick)
+				elif args[1] == '433' and len(args) >= 4: # ERR_NICKNAMEINUSE
+					nick = args[2]
+					target_nick = args[3]
+					if nick == '*':
+						self.nickname = random_nick()
+						self.nick(self.nickname)
+				elif args[1] == '465': # ERR_YOUREBANNEDCREEP
+					error('K-Lined', self.server)
+				elif args[1] in ('716','717'): # RPL_TARGNOTIFY
+					nick = args[2] #TODO: verify this is the correct arguement
+					botcontrol(nick, '+', True)
+				elif args[1] == '731' and len(args) >= 4: # RPL_MONOFFLINE
+					nick = args[3][1:]
+					await self.nick(nick)
+				elif args[1] == 'JOIN' and len(args) == 3:
+					nick = args[0].split('!')[0][1:]
+					host = args[0].split('@')[1]
+					chan = args[2][1:]
+					if chan == self.landmine:
+						await self.sendmsg(chan, f'{unicode()} oh god {nick} what is happening {unicode()}')
+						await self.sendmsg(nick, f'{unicode()} oh god {nick} what is happening {unicode()}')
+					elif chan == channel:
+						botcontrol('+', nick)
+						if nick == self.nickname:
+							self.host = host
+				elif args[1] == 'KICK' and len(args) >= 4:
+					chan = args[2]
+					nick = args[3]
+					if nick == self.nickname:
+						await asyncio.sleep(3)
+						await self.raw('JOIN ' + chan)
+				elif args[1] == 'MODE' and len(args) >= 4:
+					nick  = args[0].split('!')[0][1:]
+					chan  = args[2]
+					modes = ' '.join(args[3:])
+					await self.event_mode(nick, chan, modes)
+				elif args[1] == 'NICK' and len(args) == 3:
+					nick = args[0].split('!')[0][1:]
+					new_nick = args[2][1:]
+					if nick == self.nickname:
+						botcontrol('-', nick)
+						botcontrol('+', new_nick)
+						self.nickname = new_nick
+						if self.nickname in self.monlist:
+							await self.monitor('C')
+							self.monlist = list()
+					elif nick in self.monlist:
+						self.nick(nick)
+					elif nick in bots:
+						botcontrol('-', nick)
+						botcontrol('+', new_nick)
+				elif args[1] == 'NOTICE':
+					nick   = args[0].split('!')[0][1:]
+					target = args[2]
+					msg    = ' '.join(args[3:])[1:]
+					if target == self.nickname:
+						if '!' not in args[0] and 'Blacklisted Proxy found' in line:
+							error('Blacklisted IP', line)
+						elif 'You are now being scanned for open proxies' in line:
+							pass # We can ignore these & not relay them into the channel
+						else:
+							await self.sendmsg(channel, '[{0}] {1}{2}{3} {4}'.format(color('NOTICE', purple), color('<', grey), color(nick, yellow), color('>', grey), msg))
+				elif args[1] == 'PRIVMSG' and len(args) >= 4:
+					ident  = args[0][1:]
+					nick   = args[0].split('!')[0][1:]
+					target = args[2]
+					msg    = ' '.join(args[3:])[1:]
+					if msg[:1] == '\001':
+						msg = msg[1:-1]
+						if target == self.nickname:
+							if msg == 'VERSION':
+								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')
+							else:
+								await self.sendmsg(channel, '[{0}] {1}{2}{3} {4}'.format(color('CTCP', green), color('<', grey), color(nick, yellow), color('>', grey), msg))
+					else:
+						await self.event_message(ident, nick, target, msg)
+				elif args[1] == 'QUIT':
+					nick = args[0].split('!')[0][1:]
+					if nick in self.monlist:
+						await self.nick(nick)
+					elif nick in bots:
+						botcontrol('-', nick)
+			except (UnicodeDecodeError,UnicodeEncodeError):
+				pass
+			except Exception as ex:
+				error('Unexpected error occured on \'{0}\' server.'.format(self.server['server']), ex)
+				break
+
+	async def mode(self, target, mode):
+		await self.raw(f'MODE {target} {mode}')
+
+	async def monitor(self, action, nicks=list()):
+		await self.raw(f'MONITOR {action} ' + ','.join(nicks))
+
+	async def nick(self, nick):
+		await self.raw('NICK ' + nick)
+
+	async def raw(self, data, delay=False):
+		try:
+			if delay:
+				await asyncio.sleep(random.randint(60,300))
+			self.writer.write(data[:510].encode('utf-8') + b'\r\n')
+			await self.writer.drain()
+		except asyncio.CancelledError:
+			pass
+
+	async def sendmsg(self, target, msg):
+		await self.raw(f'PRIVMSG {target} :{msg}')
+
+async def main():
+	jobs = list()
+	for i in range(concurrency):
+		for server in servers:
+			jobs.append(asyncio.ensure_future(clone(server).connect()))
+			if ipv6 and server['ipv6']:
+				jobs.append(asyncio.ensure_future(clone(server, True).connect()))
+	await asyncio.gather(*jobs)
+
+# Main
+print('#'*56)
+print('#{:^54}#'.format(''))
+print('#{:^54}#'.format('Jupiter IRC botnet for EFnet'))
+print('#{:^54}#'.format('Developed by acidvegas in Python'))
+print('#{:^54}#'.format('https://git.acid.vegas/jupiter'))
+print('#{:^54}#'.format(''))
+print('#'*56)
+loop = asyncio.get_event_loop()
+loop.run_until_complete(main())
+\ No newline at end of file