diff --git a/.env.example b/.env.example index 576efd7..566dd3b 100644 --- a/.env.example +++ b/.env.example @@ -4,4 +4,9 @@ OVISBOT_HTB_CREDS_EMAIL=htb@randomdomain.com OVISBOT_HTB_CREDS_PASS=myhtbpassword OVISBOT_HTB_TEAM_ID=1000 OVISBOT_CTFTIME_TEAM_ID=999 -OVISBOT_ADMIN_ROLE=moderator \ No newline at end of file +OVISBOT_ADMIN_ROLE=moderator +OVISBOT_DB_URL=mongodb://localhost/ovisdb +OVISBOT_THIRD_PARTY_COGS_INSTALL_DIR=~/cogs +OVISBOT_CTFSOLVES_GITHUB_TOKEN= +OVISBOT_CTFSOLVES_GITHUB_USER= +OVISBOT_CTFSOLVES_GITHUB_REPO= \ No newline at end of file diff --git a/ovisbot/config.py b/ovisbot/config.py index 7b7d9c6..a8e7da9 100644 --- a/ovisbot/config.py +++ b/ovisbot/config.py @@ -149,6 +149,9 @@ class Config(AbstractConfig): DB_URL = environ.get("OVISBOT_DB_URL", "mongodb://mongo/ovisdb") COMMAND_PREFIX = environ.get("OVISBOT_COMMAND_PREFIX", "!") DISCORD_BOT_TOKEN = environ.get("OVISBOT_DISCORD_TOKEN") + GITHUB_CTFSOLVES_TOKEN = environ.get("OVISBOT_CTFSOLVES_GITHUB_TOKEN") + GITHUB_CTFSOLVES_USER = environ.get("OVISBOT_CTFSOLVES_GITHUB_USER") + GITHUB_CTFSOLVES_REPO = environ.get("OVISBOT_CTFSOLVES_GITHUB_REPO") THIRD_PARTY_COGS_INSTALL_DIR = environ.get( "OVISBOT_THIRD_PARTY_COGS_INSTALL_DIR", "/usr/local/share/ovisbot/cogs" diff --git a/ovisbot/exceptions.py b/ovisbot/exceptions.py index ddb0ad7..4213ed3 100644 --- a/ovisbot/exceptions.py +++ b/ovisbot/exceptions.py @@ -9,9 +9,11 @@ class ChallengeExistsException(Exception): class ChallengeInvalidCategory(Exception): pass + class ChallengeInvalidDifficulty(Exception): pass + class ChallengeAlreadySolvedException(Exception): def __init__(self, solved_by, *args, **kwargs): self.solved_by = solved_by diff --git a/ovisbot/extensions/ctf/ctf.py b/ovisbot/extensions/ctf/ctf.py index 9f20ade..886a596 100644 --- a/ovisbot/extensions/ctf/ctf.py +++ b/ovisbot/extensions/ctf/ctf.py @@ -1,4 +1,7 @@ +import asyncio +import base64 import copy +import os import discord import datetime import logging @@ -8,6 +11,8 @@ import dateutil.parser import pytz import random +import json +import time import ovisbot.locale as i18n @@ -61,12 +66,7 @@ "htb", ] -CHALLENGE_DIFFICULTIES = [ - "none", - "easy", - "medium", - "hard" -] +CHALLENGE_DIFFICULTIES = ["none", "easy", "medium", "hard"] # difficulty -> (:emoji:, text) DIFFICULTY_REWARDS = { @@ -76,6 +76,7 @@ "hard": (":icecream:", "παωτόν"), } + class Ctf(commands.Cog): def __init__(self, bot): self.bot = bot @@ -117,6 +118,7 @@ async def status_error(self, ctx, error): @ctf.command() async def archive(self, ctx, ctf_name): + start = time.time() """ Arcive a CTF to the DB and remove it from discord """ @@ -127,10 +129,18 @@ async def archive(self, ctx, ctf_name): if ctfrole is not None: await ctfrole.delete() + for c in category.channels: + challenge = next((ch for ch in ctf.challenges if ch.name == c.name), None) + if challenge is not None: + await harvest_pins(c) + asyncio.ensure_future(self.push_to_github(ctf_name, c.name)) + await c.delete() await category.delete() + + print(f"Took {(time.time() - start)} seconds to archive") ctf.name = "__ARCHIVED__" + ctf.name # bug fix (==) ctf.save() @@ -366,7 +376,7 @@ async def finish_error(self, ctx, error): @ctf.command() async def solve(self, ctx): """ - Marks the current challenge as solved by you. + Marks the current challenge as solved by you. Addition of team mates that helped to solve is optional """ chall_name = ctx.channel.name @@ -730,7 +740,7 @@ async def showcreds(self, ctx): raise CTFSharedCredentialsNotSet emb = discord.Embed(description=ctf.credentials(), colour=4387968) await ctx.channel.send(embed=emb) - + @showcreds.error async def showcreds_error(self, ctx, error): if isinstance(error, CTF.DoesNotExist): @@ -955,6 +965,58 @@ async def check_reminders(self): except CTF.DoesNotExist: continue + async def push_to_github(self, ctf_name, challenge_name): + pins_dir = f"{os.path.abspath(os.getcwd())}/pins/{challenge_name}/" + for filename in os.listdir(pins_dir): + user = self.bot.config_cls.GITHUB_CTFSOLVES_USER + repo = self.bot.config_cls.GITHUB_CTFSOLVES_REPO + url = f"https://api.github.com/repos/{user}/{repo}/contents/{ctf_name}/{challenge_name}/{filename}" + file = base64.b64encode(open(pins_dir + filename, "rb").read()) + token = self.bot.config_cls.GITHUB_CTFSOLVES_TOKEN + message = json.dumps( + { + "message": f"store pins of challenge {challenge_name} from {ctf_name} ctf", + "branch": "master", + "content": file.decode("utf-8"), + } + ) + resp = requests.put( + url, + data=message, + headers={ + "Accept": "application/vnd.github.v3+json", + "Authorization": "token " + token, + }, + ) + os.remove(pins_dir + filename) + os.rmdir(pins_dir) + + +# Gathers all pinned messages and files in the /pins folder +async def harvest_pins(channel): + pins_dir = f"{os.path.abspath(os.getcwd())}/pins/{channel.name}" + os.makedirs(pins_dir) + complete_path = os.path.join(pins_dir, f"{channel.name}.md") + f = open(complete_path, "x") + + pinned_messages = await channel.pins() + + # reversed as to write first pinned message, first. + # otherwise, the last pinned message is the first to be written. + for pinned in reversed(pinned_messages): + f.write(pinned.content + os.linesep) + + attachs = pinned.attachments + if len(attachs) != 0: + for a in attachs: + await a.save(os.path.join(pins_dir, a.filename)) + file = f"[{a.filename}](./{a.filename})" + f.write(os.linesep + file + os.linesep) + + f.write(os.linesep + "---" + os.linesep) + + f.close() + def setup(bot): bot.add_cog(Ctf(bot)) diff --git a/ovisbot/extensions/ctftime/ctftime.py b/ovisbot/extensions/ctftime/ctftime.py index 2676cb5..b4c1bcc 100644 --- a/ovisbot/extensions/ctftime/ctftime.py +++ b/ovisbot/extensions/ctftime/ctftime.py @@ -102,26 +102,25 @@ def rgb2hex(r, g, b): ) await ctx.channel.send(embed=embed) - @ctftime.command( aliases = ["w"] ) + @ctftime.command(aliases=["w"]) async def writeups(self, ctx, name): """ Returns the submitted writeups for a given CTF !ctftime writeups !ctftime w """ - event = ctfh.Event( e_name = name ) + event = ctfh.Event(e_name=name) try: ctf_name, ctf_writeups = event.find_event_writeups() except ValueError: - await ctx.channel.send( "Could not find such event" ) + await ctx.channel.send("Could not find such event") return - - writeups = '\n'.join(ctf_writeups) + + writeups = "\n".join(ctf_writeups) writeups = chunkify(writeups, 1700) for chunk in writeups: await ctx.channel.send(chunk) - @writeups.error async def writeups_error(self, ctx, error): if isinstance(error.original, ValueError): diff --git a/ovisbot/extensions/ctftime/ctftime_helpers.py b/ovisbot/extensions/ctftime/ctftime_helpers.py index 5177144..d1f81a6 100644 --- a/ovisbot/extensions/ctftime/ctftime_helpers.py +++ b/ovisbot/extensions/ctftime/ctftime_helpers.py @@ -15,106 +15,106 @@ from typing import List, Sequence, Dict # write machinery to find `eventID` according to the CTF -URL = 'https://ctftime.org/event/{EVENT_ID}/tasks/' -HEADERS = { - 'User-Agent':'Mozilla/5.0' -} +URL = "https://ctftime.org/event/{EVENT_ID}/tasks/" +HEADERS = {"User-Agent": "Mozilla/5.0"} # ----- [ Class Event ] ----- # BEGIN # -class Event(): - ''' +class Event: + """ Event class - name : str + name : str id : int ctf_id : int ctftime_url : str e_url : str e_title : str - ''' + """ s = requests.Session() - def __init__(self, - e_name : str = '', - e_id : int = 0, - e_ctf_id : int = 0, - e_ctftime_url : str = '', - e_url : str = '', - e_title : str = '' - ): + def __init__( + self, + e_name: str = "", + e_id: int = 0, + e_ctf_id: int = 0, + e_ctftime_url: str = "", + e_url: str = "", + e_title: str = "", + ): self.e_name = e_name - ''' @raises ValueError ''' - self.e_id = int( e_id ) - self.e_ctf_id = int( e_ctf_id ) - + """ @raises ValueError """ + self.e_id = int(e_id) + self.e_ctf_id = int(e_ctf_id) + self.e_ctftime_url = e_ctftime_url self.e_url = e_url self.e_title = e_title self.writeups = self.find_event_writeups - def __dict__(self) -> Dict: - e_dict = dict ({ - "name" : self.e_name, - "id" : str(self.e_id), - "ctf_id" : str(self.e_ctf_id), - "ctftime_url" : str(self.e_ctftime_url), - "url" : str(self.e_url), - "title" : str(self.e_title) - }) + e_dict = dict( + { + "name": self.e_name, + "id": str(self.e_id), + "ctf_id": str(self.e_ctf_id), + "ctftime_url": str(self.e_ctftime_url), + "url": str(self.e_url), + "title": str(self.e_title), + } + ) return e_dict def find_event_by_name(self): - ''' + """ Given @name finds @event_id and fills needed fields to be used by @find_event_by_id Raises @ValueError - ''' - - url = 'https://ctftime.org/event/list/past' + """ + + url = "https://ctftime.org/event/list/past" r = (self.s).get(url=url, headers=HEADERS) - soup = BeautifulSoup(r.text, 'html.parser') + soup = BeautifulSoup(r.text, "html.parser") # find Event by name found = False try: - all_tr = soup.body.table.find_all('tr')[1::] + all_tr = soup.body.table.find_all("tr")[1::] except AttributeError: print("Error: Unable to get list") for i in all_tr: name = (i.td).text - e_name = self.e_name.split('.')[0] + e_name = self.e_name.split(".")[0] if e_name.upper() in name.upper(): found = True - href = (i.a).get('href') - start_ix, end_ix = re.search(r'\d+', href).span() - self.e_id = int( href[start_ix:end_ix] ) + href = (i.a).get("href") + start_ix, end_ix = re.search(r"\d+", href).span() + self.e_id = int(href[start_ix:end_ix]) self.e_name = name break if not found: - raise ValueError('Event not found') - + raise ValueError("Event not found") + def find_event_by_id(self): - ''' + """ Using the @event_id finds and collects available writeups into a list - ''' - - url = f'https://ctftime.org/event/{ str(self.e_id) }/tasks/' + """ + + url = f"https://ctftime.org/event/{ str(self.e_id) }/tasks/" r = (self.s).get(url=url, headers=HEADERS) - soup = BeautifulSoup(r.text, 'html.parser') + soup = BeautifulSoup(r.text, "html.parser") writeups = [] - all_tr = soup.find_all('tr') + all_tr = soup.find_all("tr") for i in all_tr[1:]: writeup = Writeup() writeup.url = (i.a).get("href") writeup.name = (i.a).get_text() try: - writeup.tags = list(map(lambda x: x.get_text(), i.find_all('span'))) - all_td = i.find_all('td') + writeup.tags = list(map(lambda x: x.get_text(), i.find_all("span"))) + all_td = i.find_all("td") if len(all_td) > 1: writeup.points = all_td[1].get_text() writeup.no_writeups = int(all_td[-2].get_text()) @@ -123,7 +123,7 @@ def find_event_by_id(self): except IndexError: pass - writeups.append( writeup ) + writeups.append(writeup) self.writeups = writeups return self.e_name, self.writeups @@ -132,48 +132,51 @@ def find_event_writeups(self): self.find_event_by_name() return self.find_event_by_id() + @dataclass -class Writeup(): - ''' +class Writeup: + """ Writeup Class name : str points : int tags : list no_writeups : int url : str - ''' - name : str = '' - points : int = 0 - tags : Sequence[str] = None - no_writeups : int = 0 - url : str = '' + """ + + name: str = "" + points: int = 0 + tags: Sequence[str] = None + no_writeups: int = 0 + url: str = "" def __str__(self) -> str: - ''' + """ String representation of class Writeup - ''' - return f'Name: {self.name} ({self.points} pts)\n' +\ - f'Tags: {self.tags}\n' +\ - f'#Writeups: {self.no_writeups}\n' +\ - f'Writeup URL: https://ctftime.org{self.url}\n' - -if __name__ == '__main__': - - if len(sys.argv) == 2: - e_name = str( sys.argv[1] ) - event = Event ( - e_name = e_name + """ + return ( + f"Name: {self.name} ({self.points} pts)\n" + + f"Tags: {self.tags}\n" + + f"#Writeups: {self.no_writeups}\n" + + f"Writeup URL: https://ctftime.org{self.url}\n" ) + + +if __name__ == "__main__": + + if len(sys.argv) == 2: + e_name = str(sys.argv[1]) + event = Event(e_name=e_name) try: ctf_name, writeups = event.find_event_writeups() except ValueError as e: - print( 'Could not find such event' ) - sys.exit( 1 ) + print("Could not find such event") + sys.exit(1) n = len(ctf_name) - print('=' * n, ctf_name, '=' * n, sep='\n') - print( '\n'.join( map( str, writeups ))) + print("=" * n, ctf_name, "=" * n, sep="\n") + print("\n".join(map(str, writeups))) sys.exit(0) else: - print(f'Usage: {sys.argv[0]} ') + print(f"Usage: {sys.argv[0]} ") sys.exit(1) diff --git a/ovisbot/extensions/utils/utils.py b/ovisbot/extensions/utils/utils.py index 0e84ab2..e8ab0c0 100644 --- a/ovisbot/extensions/utils/utils.py +++ b/ovisbot/extensions/utils/utils.py @@ -9,12 +9,18 @@ logger = logging.getLogger(__name__) + def rotn_helper(offset, text): - shifted = string.ascii_lowercase[offset:] + string.ascii_lowercase[:offset] +\ - string.ascii_uppercase[offset:] + string.ascii_uppercase[:offset] + shifted = ( + string.ascii_lowercase[offset:] + + string.ascii_lowercase[:offset] + + string.ascii_uppercase[offset:] + + string.ascii_uppercase[:offset] + ) shifted_tab = str.maketrans(string.ascii_letters, shifted) return text.translate(shifted_tab) + class Utils(commands.Cog): def __init__(self, bot): self.bot = bot @@ -47,9 +53,7 @@ async def str2hex(self, ctx, *params): Converts string to hex """ joined_params = " ".join(params) - await ctx.send( - "`{0}`".format(joined_params.encode("latin-1").hex()) - ) + await ctx.send("`{0}`".format(joined_params.encode("latin-1").hex())) @utils.command() async def hex2str(self, ctx, param): @@ -60,28 +64,28 @@ async def hex2str(self, ctx, param): @utils.command() async def rotn(self, ctx, shift, *params): - ''' + """ Returns the ROT-n encoding of a message. - ''' - msg = ' '.join(params) - out = 'Original message:\n' + msg + """ + msg = " ".join(params) + out = "Original message:\n" + msg if shift == "*": for s in range(1, 14): - out += f'\n=[ ROT({s}) ]=\n' + out += f"\n=[ ROT({s}) ]=\n" out += rotn_helper(s, msg) else: shift = int(shift) shifted_str = rotn_helper(shift, msg) - out += f'\n=[ ROT({shift}) ]=\n' - out += 'Encoded message:\n' + shifted_str - + out += f"\n=[ ROT({shift}) ]=\n" + out += "Encoded message:\n" + shifted_str + for chunk in chunkify(out, 1700): await ctx.send("".join(["```", chunk, "```"])) @utils.command() - async def genshadow(self, ctx, cleartext, method = None): - ''' + async def genshadow(self, ctx, cleartext, method=None): + """ genshadow, generates a UNIX password hash and a corresponding /etc/shadow entry and is intended for usage in boot2root environments @@ -90,29 +94,25 @@ async def genshadow(self, ctx, cleartext, method = None): + Blowfish + SHA-256 + SHA-512 - ''' + """ __methods = { "1": crypt.METHOD_MD5, "MD5": crypt.METHOD_MD5, - "2": crypt.METHOD_BLOWFISH, "BLOWFISH": crypt.METHOD_BLOWFISH, - "5": crypt.METHOD_SHA256, "SHA256": crypt.METHOD_SHA256, - "6": crypt.METHOD_SHA512, - "SHA512": crypt.METHOD_SHA512 + "SHA512": crypt.METHOD_SHA512, } if method and not method.isnumeric(): method = method.upper() method = __methods.get(method, None) - + unix_passwd = crypt.crypt(cleartext, method) shadow = f"root:{unix_passwd}:0:0:99999:7::" - await ctx.send(f"{cleartext}:\n" +\ - f"=> {unix_passwd}\n" +\ - f"=> {shadow}") + await ctx.send(f"{cleartext}:\n" + f"=> {unix_passwd}\n" + f"=> {shadow}") + def setup(bot): bot.add_cog(Utils(bot)) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b0471b7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta:__legacy__" \ No newline at end of file