Skip to content

Commit

Permalink
Merge branch 'master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
d-Rickyy-b committed Oct 7, 2019
2 parents fb299f5 + f0af9cb commit 4cf983d
Show file tree
Hide file tree
Showing 8 changed files with 293 additions and 4 deletions.
2 changes: 2 additions & 0 deletions pastepwn/actions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .databaseaction import DatabaseAction
from .savejsonaction import SaveJSONAction
from .twitteraction import TwitterAction
from .discordaction import DiscordAction

__all__ = (
"BasicAction",
Expand All @@ -18,4 +19,5 @@
"DatabaseAction",
"SaveJSONAction",
"TwitterAction",
"DiscordAction",
)
131 changes: 131 additions & 0 deletions pastepwn/actions/discordaction.py
Original file line number Diff line number Diff line change
@@ -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)
20 changes: 17 additions & 3 deletions pastepwn/analyzers/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -22,6 +32,7 @@
from .battlenetkeyanalyzer import BattleNetKeyAnalyzer
from .microsoftkeyanalyzer import MicrosoftKeyAnalyzer
from .adobekeyanalyzer import AdobeKeyAnalyzer
from .emailpasswordpairanalyzer import EmailPasswordPairAnalyzer

__all__ = (
'AlwaysTrueAnalyzer',
Expand All @@ -47,4 +58,7 @@
'BattleNetKeyAnalyzer',
'MicrosoftKeyAnalyzer',
'AdobeKeyAnalyzer'
'DBConnAnalyzer',
'PrivateKeyAnalyzer',
'EmailPasswordPairAnalyzer'
)
12 changes: 12 additions & 0 deletions pastepwn/analyzers/emailpasswordpairanalyzer.py
Original file line number Diff line number Diff line change
@@ -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)
17 changes: 17 additions & 0 deletions pastepwn/analyzers/epickeyanalyzer.py
Original file line number Diff line number Diff line change
@@ -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)
55 changes: 55 additions & 0 deletions pastepwn/analyzers/tests/emailpasswordpairanalyzer_test.py
Original file line number Diff line number Diff line change
@@ -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()
57 changes: 57 additions & 0 deletions pastepwn/analyzers/tests/epickeyanalyzer_test.py
Original file line number Diff line number Diff line change
@@ -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()
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pymongo
mysql-connector-python
requests
python-twitter
python-twitter
websockets>=7.0,<8

0 comments on commit 4cf983d

Please sign in to comment.