diff --git a/pastepwn/actions/__init__.py b/pastepwn/actions/__init__.py index cad74ae..700aba1 100644 --- a/pastepwn/actions/__init__.py +++ b/pastepwn/actions/__init__.py @@ -8,6 +8,7 @@ from .databaseaction import DatabaseAction from .savejsonaction import SaveJSONAction from .twitteraction import TwitterAction +from .discordaction import DiscordAction __all__ = ( "BasicAction", @@ -18,4 +19,5 @@ "DatabaseAction", "SaveJSONAction", "TwitterAction", + "DiscordAction", ) diff --git a/pastepwn/actions/discordaction.py b/pastepwn/actions/discordaction.py new file mode 100644 index 0000000..4037e4c --- /dev/null +++ b/pastepwn/actions/discordaction.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +import asyncio +import json +import logging +import sys +from string import Template + +from pastepwn.util import Request, DictWrapper +from .basicaction import BasicAction + + +class DiscordAction(BasicAction): + """Action to send a Discord message to a certain webhook or channel.""" + name = "DiscordAction" + + def __init__(self, webhook=None, token=None, channel_id=None, template=None): + super().__init__() + self.logger = logging.getLogger(__name__) + self.bot_available = True + + try: + import websockets + except ImportError: + self.logger.warning("Could not import 'websockets' module. So you can only use webhooks for discord.") + self.bot_available = False + + self.webhook = webhook + if webhook is None: + if token is None or channel_id is None: + raise ValueError('Invalid arguments: requires either webhook or token+channel_id arguments') + + if not self.bot_available: + raise NotImplementedError("You can't use bot functionality without the 'websockets' module. Please import it or use webhooks!") + + self.token = token + self.channel_id = channel_id + self.identified = False + + if template is not None: + self.template = Template(template) + else: + self.template = None + + @asyncio.coroutine + def _identify(self, ws_url): + """Connect to the Discord Gateway to identify the bot.""" + # Docs: https://discordapp.com/developers/docs/topics/gateway#connecting-to-the-gateway + # Open connection to the Discord Gateway + socket = yield from websockets.connect(ws_url + '/?v=6&encoding=json') + try: + # Receive Hello + hello_str = yield from socket.recv() + hello = json.loads(hello_str) + if hello.get('op') != 10: + self.logger.warning('[ws] Expected Hello payload but received %s', hello_str) + + # Send heartbeat and receive ACK + yield from socket.send(json.dumps({"op": 1, "d": {}})) + ack_str = yield from socket.recv() + ack = json.loads(ack_str) + if ack.get('op') != 11: + self.logger.warning('[ws] Expected Heartbeat ACK payload but received %s', ack_str) + + # Identify + payload = { + "token": self.token, + "properties": { + "$os": sys.platform, + "$browser": "pastepwn", + "$device": "pastepwn" + } + } + yield from socket.send(json.dumps({"op": 2, "d": payload})) + + # Receive READY event + ready_str = yield from socket.recv() + ready = json.loads(ready_str) + if ready.get('t') != 'READY': + self.logger.warning('[ws] Expected READY event but received %s', ready_str) + finally: + # Close websocket connection + yield from socket.close() + + def initialize_gateway(self): + """Initialize the bot token so Discord identifies it properly.""" + if self.webhook is not None: + raise NotImplementedError('Gateway initialization is only necessary for bot accounts.') + + # Call Get Gateway Bot to get the websocket URL + r = Request() + r.headers = {'Authorization': 'Bot {}'.format(self.token)} + res = json.loads(r.get('https://discordapp.com/api/gateway/bot')) + ws_url = res.get('url') + + # Start websocket client + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(self._identify(ws_url)) + self.identified = True + + def perform(self, paste, analyzer_name=None): + """Send a message via Discord to a specified channel, without checking for errors""" + r = Request() + if self.template is None: + text = "New paste matched by analyzer '{0}' - Link: {1}".format(analyzer_name, paste.full_url) + else: + paste_dict = paste.to_dict() + paste_dict["analyzer_name"] = analyzer_name + text = self.template.safe_substitute(DictWrapper(paste_dict)) + + if self.webhook is not None: + # Send to a webhook (no authentication) + url = self.webhook + else: + # Send through Discord bot API (header-based authentication) + url = 'https://discordapp.com/api/channels/{0}/messages'.format(self.channel_id) + r.headers = {'Authorization': 'Bot {}'.format(self.token)} + + res = r.post(url, {"content": text}) + if res == "": + # If the response is empty, skip further execution + return + + res = json.loads(res) + + if res.get('code') == 40001 and self.bot_available and self.webhook is None and not self.identified: + # Unauthorized access, bot token hasn't been identified to Discord Gateway + self.logger.info('Accessing Discord Gateway to initialize token') + self.initialize_gateway() + # Retry action + self.perform(paste, analyzer_name=analyzer_name) diff --git a/pastepwn/analyzers/__init__.py b/pastepwn/analyzers/__init__.py index e2c5cb6..f33840a 100644 --- a/pastepwn/analyzers/__init__.py +++ b/pastepwn/analyzers/__init__.py @@ -1,18 +1,28 @@ # -*- coding: utf-8 -*- from .alwaystrueanalyzer import AlwaysTrueAnalyzer from .basicanalyzer import BasicAnalyzer +from .battlenetkeyanalyzer import BattleNetKeyAnalyzer from .bcrypthashanalyzer import BcryptHashAnalyzer -from .md5hashanalyzer import MD5HashAnalyzer -from .shahashanalyzer import SHAHashAnalyzer from .creditcardanalyzer import CreditCardAnalyzer +from .databasedumpanalyzer import DatabaseDumpAnalyzer +from .dbconnstringanalyzer import DBConnAnalyzer +from .emailpasswordpairanalyzer import EmailPasswordPairAnalyzer from .genericanalyzer import GenericAnalyzer +from .ibananalyzer import IBANAnalyzer from .mailanalyzer import MailAnalyzer +from .md5hashanalyzer import MD5HashAnalyzer +from .microsoftkeyanalyzer import MicrosoftKeyAnalyzer +from .originkeyanalyzer import OriginKeyAnalyzer from .pastebinurlanalyzer import PastebinURLAnalyzer +from .phonenumberanalyzer import PhoneNumberAnalyzer +from .privatekeyanalyzer import PrivateKeyAnalyzer from .regexanalyzer import RegexAnalyzer +from .shahashanalyzer import SHAHashAnalyzer +from .steamkeyanalyzer import SteamKeyAnalyzer +from .uplaykeyanalyzer import UplayKeyAnalyzer from .urlanalyzer import URLAnalyzer from .wordanalyzer import WordAnalyzer from .ibananalyzer import IBANAnalyzer -from .databasedumpanalyzer import DatabaseDumpAnalyzer from .dbconnstringanalyzer import DBConnAnalyzer from .privatekeyanalyzer import PrivateKeyAnalyzer from .phonenumberanalyzer import PhoneNumberAnalyzer @@ -22,6 +32,7 @@ from .battlenetkeyanalyzer import BattleNetKeyAnalyzer from .microsoftkeyanalyzer import MicrosoftKeyAnalyzer from .adobekeyanalyzer import AdobeKeyAnalyzer +from .emailpasswordpairanalyzer import EmailPasswordPairAnalyzer __all__ = ( 'AlwaysTrueAnalyzer', @@ -47,4 +58,7 @@ 'BattleNetKeyAnalyzer', 'MicrosoftKeyAnalyzer', 'AdobeKeyAnalyzer' + 'DBConnAnalyzer', + 'PrivateKeyAnalyzer', + 'EmailPasswordPairAnalyzer' ) diff --git a/pastepwn/analyzers/emailpasswordpairanalyzer.py b/pastepwn/analyzers/emailpasswordpairanalyzer.py new file mode 100644 index 0000000..fe71e83 --- /dev/null +++ b/pastepwn/analyzers/emailpasswordpairanalyzer.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +from .regexanalyzer import RegexAnalyzer + +_EMAIL_PASSWORD_REGEX = r'[\w\.\+_-]+@[\w\._-]+\.[a-zA-Z]*\:[\w\.\+\!\$\#\^&\*\(\)\{\}\[\]\_\-\@\%\=\§\\\/\'\`\´\?\<\>\;\"\:\|\,\~]+$' + + +class EmailPasswordPairAnalyzer(RegexAnalyzer): + """Analyzer to match username:password pairs""" + name = "EmailPasswordPairAnalyzer" + + def __init__(self, actions): + super().__init__(actions, _EMAIL_PASSWORD_REGEX) diff --git a/pastepwn/analyzers/epickeyanalyzer.py b/pastepwn/analyzers/epickeyanalyzer.py new file mode 100644 index 0000000..1dc37e6 --- /dev/null +++ b/pastepwn/analyzers/epickeyanalyzer.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from .regexanalyzer import RegexAnalyzer + + +class EpicKeyAnalyzer(RegexAnalyzer): + """Analyzer to match Epic Licensing Keys""" + + def __init__(self, action): + """ + Analyzer to match Epic Licensing Keys + :param action: Single action or list of actions to be executed when a paste matches + """ + # Applied general A-Z or 0-9 based on example provided + # Regex can be adjusted if certain characters are not valid + regex = r"(([A-Z0-9]{5}\-){3}([A-Z0-9]{5}))" + + super().__init__(action, regex) diff --git a/pastepwn/analyzers/tests/emailpasswordpairanalyzer_test.py b/pastepwn/analyzers/tests/emailpasswordpairanalyzer_test.py new file mode 100644 index 0000000..226167e --- /dev/null +++ b/pastepwn/analyzers/tests/emailpasswordpairanalyzer_test.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +import unittest +from unittest import mock + +from pastepwn.analyzers.emailpasswordpairanalyzer import EmailPasswordPairAnalyzer + + +class TestWordAnalyzer(unittest.TestCase): + def setUp(self): + self.obj = mock.Mock() + + def test_match(self): + analyzer = EmailPasswordPairAnalyzer(None) + self.obj.body = "This is a Test" + self.assertFalse(analyzer.match(self.obj)) + + analyzer = EmailPasswordPairAnalyzer(None) + self.obj.body = "{a: 'b'}" + self.assertFalse(analyzer.match(self.obj)) + + analyzer = EmailPasswordPairAnalyzer(None) + self.obj.body = "" + self.assertFalse(analyzer.match(self.obj)) + + analyzer = EmailPasswordPairAnalyzer(None) + self.obj.body = "\t\n" + self.assertFalse(analyzer.match(self.obj)) + + analyzer = EmailPasswordPairAnalyzer(None) + self.obj.body = "\n\n" + self.assertFalse(analyzer.match(self.obj)) + + analyzer = EmailPasswordPairAnalyzer(None) + self.obj.body = "estocanam2@gmail.com:Firebird1@" + self.assertTrue(analyzer.match(self.obj)) + + analyzer = EmailPasswordPairAnalyzer(None) + self.obj.body = "test+test@gmail.com:abcd" + self.assertTrue(analyzer.match(self.obj)) + + analyzer = EmailPasswordPairAnalyzer(None) + self.obj.body = "estocanam2@gmail.com:aq12ws" + self.assertTrue(analyzer.match(self.obj)) + + analyzer = EmailPasswordPairAnalyzer(None) + self.obj.body = "estocanam2@apple.com:Fireb§" + self.assertTrue(analyzer.match(self.obj)) + + analyzer = EmailPasswordPairAnalyzer(None) + self.obj.body = "g@bb.com:Firebird1@" + self.assertTrue(analyzer.match(self.obj)) + + +if __name__ == '__main__': + unittest.main() diff --git a/pastepwn/analyzers/tests/epickeyanalyzer_test.py b/pastepwn/analyzers/tests/epickeyanalyzer_test.py new file mode 100644 index 0000000..7808401 --- /dev/null +++ b/pastepwn/analyzers/tests/epickeyanalyzer_test.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +import unittest +from unittest import mock + +from pastepwn.analyzers.epickeyanalyzer import EpicKeyAnalyzer + + +class TestEpicKeyAnalyzer(unittest.TestCase): + def setUp(self): + self.analyzer = EpicKeyAnalyzer(None) + self.paste = mock.Mock() + + def test_match_positive(self): + """Test if positives are recognized""" + # Epic key dump + self.paste.body = "1GZJQ-DW7QX-SB4DC-THDW2" + self.assertTrue(self.analyzer.match(self.paste)) + + # Epic key dump + self.paste.body = "KWETC-MK13P-4DO0N-VHT62" + self.assertTrue(self.analyzer.match(self.paste)) + + # Epic key dump + self.paste.body = "MB9KG-TGXBJ-X8OXE-J7PIF" + self.assertTrue(self.analyzer.match(self.paste)) + + # Epic key dump + self.paste.body = "OMYCF-Q9VYL-4FQEG-8F3XV" + self.assertTrue(self.analyzer.match(self.paste)) + + # part of a sentence + self.paste.body = "Look it's FORTNITE! UGCTH-FH42S-OH98G-QHFZA" + self.assertTrue(self.analyzer.match(self.paste)) + + # Newline seperated Epic keys + self.paste.body = "87C6Y-XIV2I-C3RJZ-B1SVZ\nQ8AQT-APT3F-MO7QU-KPE96" + self.assertTrue(self.analyzer.match(self.paste)) + + def test_match_negative(self): + """Test if negatives are not recognized""" + self.paste.body = "" + self.assertFalse(self.analyzer.match(self.paste)) + + self.paste.body = None + self.assertFalse(self.analyzer.match(self.paste)) + + # Invalid length in section + self.paste.body = "Q8A5QT-APFT3F-MO74QU-KPEL96" + self.assertFalse(self.analyzer.match(self.paste)) + + # No separator + self.paste.body = "OAPMCSEU6N7FFZ72AM5E" + self.assertFalse(self.analyzer.match(self.paste)) + + +if __name__ == '__main__': + unittest.main() diff --git a/requirements.txt b/requirements.txt index dab93a2..c6120db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ pymongo mysql-connector-python requests -python-twitter \ No newline at end of file +python-twitter +websockets>=7.0,<8 \ No newline at end of file