├── adminbot
├── __init__.py
├── REQUIREMENTS-base.txt
├── REQUIREMENTS.txt
├── Dockerfile
└── tasks.py
├── pwnedapi
├── routes
│ └── __init__.py
├── static
│ └── swaggerui
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── index.css
│ │ ├── swagger-initializer.js
│ │ ├── index.html
│ │ └── oauth2-redirect.html
├── wsgi.py
├── extensions.py
├── validators.py
├── REQUIREMENTS-base.txt
├── Dockerfile
├── constants.py
├── REQUIREMENTS.txt
├── fixtures
│ └── base
│ │ ├── rooms.json
│ │ ├── tools.json
│ │ ├── users.json
│ │ └── messages.json
├── tasks.py
├── config.py
├── decorators.py
└── __init__.py
├── pwnedhub
├── routes
│ ├── __init__.py
│ └── errors.py
├── static
│ ├── favicon.ico
│ └── js
│ │ └── pwnedhub.js
├── wsgi.py
├── extensions.py
├── REQUIREMENTS-base.txt
├── templates
│ ├── 404.html
│ ├── 500.html
│ ├── profile_view.html
│ ├── reset_init.html
│ ├── reset_question.html
│ ├── macros.html
│ ├── reset_password.html
│ ├── mail_reply.html
│ ├── about.html
│ ├── mail_compose.html
│ ├── index.html
│ ├── mail_inbox.html
│ ├── mail_view.html
│ ├── admin_tools.html
│ ├── register.html
│ ├── mobile.html
│ ├── login.html
│ ├── admin_users.html
│ ├── profile.html
│ ├── artifacts.html
│ ├── notes.html
│ ├── diagnostics.html
│ └── tools.html
├── Dockerfile
├── REQUIREMENTS.txt
├── constants.py
├── fixtures
│ └── base
│ │ ├── tools.json
│ │ ├── mail.json
│ │ ├── messages.json
│ │ └── users.json
├── validators.py
├── utils.py
├── config.py
└── decorators.py
├── pwnedsso
├── routes
│ ├── __init__.py
│ └── sso.py
├── extensions.py
├── REQUIREMENTS-base.txt
├── wsgi.py
├── REQUIREMENTS.txt
├── Dockerfile
├── __init__.py
├── config.py
├── utils.py
└── models.py
├── pwnedspa
├── REQUIREMENTS-base.txt
├── static
│ ├── favicon.ico
│ └── vue
│ │ ├── helpers
│ │ ├── socket.js
│ │ └── fetch-wrapper.js
│ │ ├── modals
│ │ └── scans-modal.js
│ │ ├── main.js
│ │ ├── views
│ │ ├── reset.css
│ │ ├── passwordless.css
│ │ ├── users.css
│ │ ├── signup.css
│ │ ├── tools.css
│ │ ├── account.css
│ │ ├── login.css
│ │ ├── profile.css
│ │ ├── activate.js
│ │ ├── profile.js
│ │ ├── notes.css
│ │ ├── passwordless.js
│ │ ├── scans.css
│ │ ├── login.js
│ │ ├── notes.js
│ │ ├── account.js
│ │ ├── signup.js
│ │ └── messages.css
│ │ ├── components
│ │ ├── link-preview.css
│ │ ├── background.css
│ │ ├── toasts.js
│ │ ├── background.js
│ │ ├── toasts.css
│ │ ├── password-field.js
│ │ ├── modal.js
│ │ ├── modal.css
│ │ ├── google-login.js
│ │ ├── link-preview.js
│ │ ├── navigation.css
│ │ └── navigation.js
│ │ ├── app.js
│ │ ├── style.css
│ │ ├── stores
│ │ └── app-store.js
│ │ ├── services
│ │ └── api.js
│ │ ├── libs
│ │ ├── vue3-infinite-loading.js
│ │ └── vue-demi.js
│ │ └── router.js
├── wsgi.py
├── REQUIREMENTS.txt
├── routes
│ └── core.py
├── Dockerfile
├── __init__.py
├── config.py
└── templates
│ └── spa.html
├── .gitignore
├── pwnedadmin
├── extensions.py
├── REQUIREMENTS-base.txt
├── wsgi.py
├── utils.py
├── REQUIREMENTS.txt
├── constants.py
├── Dockerfile
├── routes
│ ├── email.py
│ └── config.py
├── templates
│ ├── layout.html
│ ├── config.html
│ └── emails.html
├── config.py
├── models.py
├── fixtures
│ └── base
│ │ └── configs.json
└── __init__.py
├── common
└── static
│ ├── images
│ ├── logo.png
│ ├── app-store.png
│ ├── avatars
│ │ ├── wolf.jpg
│ │ ├── admin.png
│ │ ├── bacon.png
│ │ ├── c-man.png
│ │ ├── kitty.jpg
│ │ └── default.png
│ ├── background.jpg
│ ├── logo-filled.png
│ ├── play-store.png
│ ├── google_signin.png
│ ├── background-dark.jpg
│ ├── get_flash_player.gif
│ └── hub.svg
│ ├── webfonts
│ ├── fa-solid-900.eot
│ ├── fa-solid-900.ttf
│ ├── fa-brands-400.eot
│ ├── fa-brands-400.ttf
│ ├── fa-brands-400.woff
│ ├── fa-brands-400.woff2
│ ├── fa-regular-400.eot
│ ├── fa-regular-400.ttf
│ ├── fa-regular-400.woff
│ ├── fa-solid-900.woff
│ ├── fa-solid-900.woff2
│ └── fa-regular-400.woff2
│ ├── fonts
│ ├── Open-Sans-300
│ │ ├── Open-Sans-300.eot
│ │ ├── Open-Sans-300.ttf
│ │ ├── Open-Sans-300.woff
│ │ └── Open-Sans-300.woff2
│ ├── Open-Sans-600
│ │ ├── Open-Sans-600.eot
│ │ ├── Open-Sans-600.ttf
│ │ ├── Open-Sans-600.woff
│ │ └── Open-Sans-600.woff2
│ └── Open-Sans-regular
│ │ ├── Open-Sans-regular.eot
│ │ ├── Open-Sans-regular.ttf
│ │ ├── Open-Sans-regular.woff
│ │ └── Open-Sans-regular.woff2
│ └── css
│ ├── custom-utility.css
│ └── custom-flex.css
├── proxy
├── proxy_params
└── nginx.conf
├── LICENSE.txt
├── database
├── cs
│ ├── 01-init.sql
│ └── 04-pwnedhub-admin.sql
├── ctf
│ ├── 01-init.sql
│ └── 04-pwnedhub-admin.sql
└── init
│ ├── 01-init.sql
│ └── 04-pwnedhub-admin.sql
├── .dockerignore
└── README.md
/adminbot/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pwnedapi/routes/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pwnedhub/routes/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pwnedsso/routes/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/adminbot/REQUIREMENTS-base.txt:
--------------------------------------------------------------------------------
1 | selenium
2 | rq
3 |
--------------------------------------------------------------------------------
/pwnedspa/REQUIREMENTS-base.txt:
--------------------------------------------------------------------------------
1 | flask
2 | gunicorn
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.pyc
3 | *.db
4 | *sublime*
5 | venv/
6 | unused/
7 |
--------------------------------------------------------------------------------
/pwnedsso/extensions.py:
--------------------------------------------------------------------------------
1 | from flask_sqlalchemy import SQLAlchemy
2 |
3 | db = SQLAlchemy()
4 |
--------------------------------------------------------------------------------
/pwnedadmin/extensions.py:
--------------------------------------------------------------------------------
1 | from flask_sqlalchemy import SQLAlchemy
2 |
3 | db = SQLAlchemy()
4 |
--------------------------------------------------------------------------------
/pwnedhub/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/pwnedhub/static/favicon.ico
--------------------------------------------------------------------------------
/pwnedspa/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/pwnedspa/static/favicon.ico
--------------------------------------------------------------------------------
/common/static/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/images/logo.png
--------------------------------------------------------------------------------
/pwnedadmin/REQUIREMENTS-base.txt:
--------------------------------------------------------------------------------
1 | flask
2 | flask-mysqldb
3 | flask-sqlalchemy
4 | gunicorn
5 | mysqlclient
6 |
--------------------------------------------------------------------------------
/pwnedsso/REQUIREMENTS-base.txt:
--------------------------------------------------------------------------------
1 | flask
2 | flask-mysqldb
3 | flask-sqlalchemy
4 | gunicorn
5 | mysqlclient
6 | pyjwt
7 |
--------------------------------------------------------------------------------
/common/static/images/app-store.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/images/app-store.png
--------------------------------------------------------------------------------
/common/static/images/avatars/wolf.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/images/avatars/wolf.jpg
--------------------------------------------------------------------------------
/common/static/images/background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/images/background.jpg
--------------------------------------------------------------------------------
/common/static/images/logo-filled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/images/logo-filled.png
--------------------------------------------------------------------------------
/common/static/images/play-store.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/images/play-store.png
--------------------------------------------------------------------------------
/common/static/images/avatars/admin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/images/avatars/admin.png
--------------------------------------------------------------------------------
/common/static/images/avatars/bacon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/images/avatars/bacon.png
--------------------------------------------------------------------------------
/common/static/images/avatars/c-man.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/images/avatars/c-man.png
--------------------------------------------------------------------------------
/common/static/images/avatars/kitty.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/images/avatars/kitty.jpg
--------------------------------------------------------------------------------
/common/static/images/google_signin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/images/google_signin.png
--------------------------------------------------------------------------------
/common/static/webfonts/fa-solid-900.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/webfonts/fa-solid-900.eot
--------------------------------------------------------------------------------
/common/static/webfonts/fa-solid-900.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/webfonts/fa-solid-900.ttf
--------------------------------------------------------------------------------
/pwnedadmin/wsgi.py:
--------------------------------------------------------------------------------
1 | from pwnedadmin import create_app
2 |
3 | app = create_app()
4 | if __name__ == '__main__':
5 | app.run()
6 |
--------------------------------------------------------------------------------
/pwnedhub/wsgi.py:
--------------------------------------------------------------------------------
1 | from pwnedhub import create_app
2 |
3 | app = create_app()
4 | if __name__ == '__main__':
5 | app.run()
6 |
--------------------------------------------------------------------------------
/pwnedspa/wsgi.py:
--------------------------------------------------------------------------------
1 | from pwnedspa import create_app
2 |
3 | app = create_app()
4 | if __name__ == '__main__':
5 | app.run()
6 |
--------------------------------------------------------------------------------
/pwnedsso/wsgi.py:
--------------------------------------------------------------------------------
1 | from pwnedsso import create_app
2 |
3 | app = create_app()
4 | if __name__ == '__main__':
5 | app.run()
6 |
--------------------------------------------------------------------------------
/common/static/images/avatars/default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/images/avatars/default.png
--------------------------------------------------------------------------------
/common/static/images/background-dark.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/images/background-dark.jpg
--------------------------------------------------------------------------------
/common/static/images/get_flash_player.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/images/get_flash_player.gif
--------------------------------------------------------------------------------
/common/static/webfonts/fa-brands-400.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/webfonts/fa-brands-400.eot
--------------------------------------------------------------------------------
/common/static/webfonts/fa-brands-400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/webfonts/fa-brands-400.ttf
--------------------------------------------------------------------------------
/common/static/webfonts/fa-brands-400.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/webfonts/fa-brands-400.woff
--------------------------------------------------------------------------------
/common/static/webfonts/fa-brands-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/webfonts/fa-brands-400.woff2
--------------------------------------------------------------------------------
/common/static/webfonts/fa-regular-400.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/webfonts/fa-regular-400.eot
--------------------------------------------------------------------------------
/common/static/webfonts/fa-regular-400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/webfonts/fa-regular-400.ttf
--------------------------------------------------------------------------------
/common/static/webfonts/fa-regular-400.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/webfonts/fa-regular-400.woff
--------------------------------------------------------------------------------
/common/static/webfonts/fa-solid-900.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/webfonts/fa-solid-900.woff
--------------------------------------------------------------------------------
/common/static/webfonts/fa-solid-900.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/webfonts/fa-solid-900.woff2
--------------------------------------------------------------------------------
/common/static/webfonts/fa-regular-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/webfonts/fa-regular-400.woff2
--------------------------------------------------------------------------------
/pwnedapi/static/swaggerui/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/pwnedapi/static/swaggerui/favicon-16x16.png
--------------------------------------------------------------------------------
/pwnedapi/static/swaggerui/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/pwnedapi/static/swaggerui/favicon-32x32.png
--------------------------------------------------------------------------------
/pwnedapi/wsgi.py:
--------------------------------------------------------------------------------
1 | from pwnedapi import create_app
2 |
3 | app, socketio = create_app()
4 | if __name__ == '__main__':
5 | app.run()
6 |
--------------------------------------------------------------------------------
/pwnedhub/extensions.py:
--------------------------------------------------------------------------------
1 | from flask_sqlalchemy import SQLAlchemy
2 | from flask_session import Session
3 |
4 | db = SQLAlchemy()
5 | sess = Session()
6 |
--------------------------------------------------------------------------------
/common/static/fonts/Open-Sans-300/Open-Sans-300.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/fonts/Open-Sans-300/Open-Sans-300.eot
--------------------------------------------------------------------------------
/common/static/fonts/Open-Sans-300/Open-Sans-300.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/fonts/Open-Sans-300/Open-Sans-300.ttf
--------------------------------------------------------------------------------
/common/static/fonts/Open-Sans-300/Open-Sans-300.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/fonts/Open-Sans-300/Open-Sans-300.woff
--------------------------------------------------------------------------------
/common/static/fonts/Open-Sans-600/Open-Sans-600.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/fonts/Open-Sans-600/Open-Sans-600.eot
--------------------------------------------------------------------------------
/common/static/fonts/Open-Sans-600/Open-Sans-600.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/fonts/Open-Sans-600/Open-Sans-600.ttf
--------------------------------------------------------------------------------
/common/static/fonts/Open-Sans-600/Open-Sans-600.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/fonts/Open-Sans-600/Open-Sans-600.woff
--------------------------------------------------------------------------------
/common/static/fonts/Open-Sans-300/Open-Sans-300.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/fonts/Open-Sans-300/Open-Sans-300.woff2
--------------------------------------------------------------------------------
/common/static/fonts/Open-Sans-600/Open-Sans-600.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/fonts/Open-Sans-600/Open-Sans-600.woff2
--------------------------------------------------------------------------------
/common/static/fonts/Open-Sans-regular/Open-Sans-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/fonts/Open-Sans-regular/Open-Sans-regular.eot
--------------------------------------------------------------------------------
/common/static/fonts/Open-Sans-regular/Open-Sans-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/fonts/Open-Sans-regular/Open-Sans-regular.ttf
--------------------------------------------------------------------------------
/common/static/fonts/Open-Sans-regular/Open-Sans-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/fonts/Open-Sans-regular/Open-Sans-regular.woff
--------------------------------------------------------------------------------
/common/static/fonts/Open-Sans-regular/Open-Sans-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practisec/pwnedhub/HEAD/common/static/fonts/Open-Sans-regular/Open-Sans-regular.woff2
--------------------------------------------------------------------------------
/pwnedspa/REQUIREMENTS.txt:
--------------------------------------------------------------------------------
1 | blinker==1.6.2
2 | click==8.1.4
3 | Flask==2.3.2
4 | gunicorn==20.1.0
5 | itsdangerous==2.1.2
6 | Jinja2==3.1.2
7 | MarkupSafe==2.1.3
8 | Werkzeug==2.3.6
9 |
--------------------------------------------------------------------------------
/pwnedhub/REQUIREMENTS-base.txt:
--------------------------------------------------------------------------------
1 | flask
2 | flask-mysqldb
3 | flask-session
4 | flask-sqlalchemy
5 | gunicorn
6 | pyjwt
7 | lxml
8 | markdown
9 | mysqlclient
10 | requests
11 | redis
12 | rq
13 |
--------------------------------------------------------------------------------
/pwnedspa/routes/core.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, render_template
2 |
3 | blp = Blueprint('core', __name__)
4 |
5 | @blp.route('/')
6 | def index():
7 | return render_template('spa.html')
8 |
--------------------------------------------------------------------------------
/pwnedapi/extensions.py:
--------------------------------------------------------------------------------
1 | from flask_sqlalchemy import SQLAlchemy
2 | from flask_cors import CORS
3 | from flask_socketio import SocketIO
4 |
5 | db = SQLAlchemy()
6 | cors = CORS()
7 | socketio = SocketIO()
8 |
--------------------------------------------------------------------------------
/proxy/proxy_params:
--------------------------------------------------------------------------------
1 | proxy_set_header Host $http_host;
2 | proxy_set_header X-Real-IP $remote_addr;
3 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
4 | proxy_set_header X-Forwarded-Proto $scheme;
5 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright 2015-2024 Practical Security Services LLC
2 |
3 | This software is intended for private use only. This license prohibits the distibution, modification, sublicensing and/or commercial use of this software.
4 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/helpers/socket.js:
--------------------------------------------------------------------------------
1 | import { io } from '../libs/socket.io.js'; // esm build
2 |
3 | export const socket = io(API_BASE_URL, {
4 | autoConnect: false,
5 | transports: ['websocket'],
6 | query: {},
7 | });
8 |
--------------------------------------------------------------------------------
/pwnedhub/templates/404.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
4 |
Oops! That page doesn't exist.
5 | {{ message|safe }}
6 |
7 | {% endblock %}
8 |
--------------------------------------------------------------------------------
/pwnedadmin/utils.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timezone
2 |
3 | def get_current_utc_time():
4 | return datetime.now(tz=timezone.utc)
5 |
6 | def get_local_from_utc(dtg):
7 | return dtg.replace(tzinfo=timezone.utc).astimezone(tz=None)
8 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/modals/scans-modal.js:
--------------------------------------------------------------------------------
1 | const template = `
2 |
5 | `;
6 |
7 | export default {
8 | name: 'ScansModal',
9 | template,
10 | props: {
11 | results: String,
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/pwnedhub/templates/500.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
4 |
Oops! Somethin' broke.
5 |
8 |
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/pwnedapi/static/swaggerui/index.css:
--------------------------------------------------------------------------------
1 | html {
2 | box-sizing: border-box;
3 | overflow: -moz-scrollbars-vertical;
4 | overflow-y: scroll;
5 | }
6 |
7 | *,
8 | *:before,
9 | *:after {
10 | box-sizing: inherit;
11 | }
12 |
13 | body {
14 | margin: 0;
15 | background: #fafafa;
16 | }
17 |
--------------------------------------------------------------------------------
/pwnedapi/validators.py:
--------------------------------------------------------------------------------
1 | from pwnedapi.models import Config
2 | import re
3 |
4 | def is_valid_command(cmd):
5 | pattern = r'[;&|]'
6 | if Config.get_value('OSCI_PROTECT'):
7 | pattern = r'[;&|<>`$(){}]'
8 | if re.search(pattern, cmd):
9 | return False
10 | return True
11 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/main.js:
--------------------------------------------------------------------------------
1 | import App from './app.js';
2 | import { router } from './router.js';
3 |
4 | const { createApp } = Vue;
5 | const { createPinia } = Pinia;
6 | const pinia = createPinia();
7 | const app = createApp(App);
8 |
9 | app.use(router);
10 | app.use(pinia);
11 | app.mount('#app');
12 |
--------------------------------------------------------------------------------
/pwnedadmin/REQUIREMENTS.txt:
--------------------------------------------------------------------------------
1 | blinker==1.6.2
2 | click==8.1.4
3 | Flask==2.3.2
4 | Flask-MySQLdb==1.0.1
5 | Flask-SQLAlchemy==3.0.5
6 | greenlet==2.0.2
7 | gunicorn==20.1.0
8 | itsdangerous==2.1.2
9 | Jinja2==3.1.2
10 | MarkupSafe==2.1.3
11 | mysqlclient==2.2.0
12 | SQLAlchemy==2.0.18
13 | typing_extensions==4.7.1
14 | Werkzeug==2.3.6
15 |
--------------------------------------------------------------------------------
/pwnedsso/REQUIREMENTS.txt:
--------------------------------------------------------------------------------
1 | blinker==1.6.2
2 | click==8.1.4
3 | Flask==2.3.2
4 | Flask-MySQLdb==1.0.1
5 | Flask-SQLAlchemy==3.0.5
6 | greenlet==2.0.2
7 | gunicorn==20.1.0
8 | itsdangerous==2.1.2
9 | Jinja2==3.1.2
10 | MarkupSafe==2.1.3
11 | mysqlclient==2.2.0
12 | PyJWT==2.7.0
13 | SQLAlchemy==2.0.18
14 | typing_extensions==4.7.1
15 | Werkzeug==2.3.6
16 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/views/reset.css:
--------------------------------------------------------------------------------
1 | .reset {
2 | color: white;
3 | padding: 1rem;
4 | }
5 |
6 | .reset .form {
7 | padding: 1rem 2rem;
8 | background-color: rgba(0,0,0,0.6);
9 | }
10 |
11 | /* desktop reset */
12 | @media all and (min-width: 960px) {
13 |
14 | .reset .form {
15 | margin: 0 auto;
16 | width: 30rem;
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/adminbot/REQUIREMENTS.txt:
--------------------------------------------------------------------------------
1 | async-timeout==4.0.2
2 | attrs==23.1.0
3 | certifi==2023.5.7
4 | click==8.1.4
5 | exceptiongroup==1.1.2
6 | h11==0.14.0
7 | idna==3.4
8 | outcome==1.2.0
9 | PySocks==1.7.1
10 | redis==4.6.0
11 | rq==1.15.1
12 | selenium==4.10.0
13 | sniffio==1.3.0
14 | sortedcontainers==2.4.0
15 | trio==0.22.1
16 | trio-websocket==0.10.3
17 | urllib3==2.0.3
18 | wsproto==1.2.0
19 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/components/link-preview.css:
--------------------------------------------------------------------------------
1 | .messages .message-container .message .link-preview a {
2 | color: inherit;
3 | text-decoration: inherit;
4 | font-weight: inherit;
5 | }
6 |
7 | .messages .message-container .message .link-preview p {
8 | font-size: 1.25rem;
9 | border-left: 2px solid red;
10 | padding-left: .5rem;
11 | margin-bottom: .5rem;
12 | }
13 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/views/passwordless.css:
--------------------------------------------------------------------------------
1 | .passwordless {
2 | color: white;
3 | padding: 1rem;
4 | }
5 |
6 | .passwordless .form {
7 | padding: 1rem 2rem;
8 | background-color: rgba(0,0,0,0.6);
9 | }
10 |
11 | /* desktop reset */
12 | @media all and (min-width: 960px) {
13 |
14 | .passwordless .form {
15 | margin: 0 auto;
16 | width: 30rem;
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/database/cs/01-init.sql:
--------------------------------------------------------------------------------
1 | -- Setup databases and users
2 | CREATE DATABASE IF NOT EXISTS `pwnedhub`;
3 | CREATE DATABASE IF NOT EXISTS `pwnedhub-test`;
4 | CREATE DATABASE IF NOT EXISTS `pwnedhub-admin`;
5 | CREATE USER 'pwnedhub'@'%' IDENTIFIED BY 'dbconnectpass';
6 | GRANT ALL PRIVILEGES ON `pwnedhub` . * TO 'pwnedhub'@'%';
7 | GRANT ALL PRIVILEGES ON `pwnedhub-test` . * TO 'pwnedhub'@'%';
8 | GRANT ALL PRIVILEGES ON `pwnedhub-admin` . * TO 'pwnedhub'@'%';
9 |
--------------------------------------------------------------------------------
/database/ctf/01-init.sql:
--------------------------------------------------------------------------------
1 | -- Setup databases and users
2 | CREATE DATABASE IF NOT EXISTS `pwnedhub`;
3 | CREATE DATABASE IF NOT EXISTS `pwnedhub-test`;
4 | CREATE DATABASE IF NOT EXISTS `pwnedhub-admin`;
5 | CREATE USER 'pwnedhub'@'%' IDENTIFIED BY 'dbconnectpass';
6 | GRANT ALL PRIVILEGES ON `pwnedhub` . * TO 'pwnedhub'@'%';
7 | GRANT ALL PRIVILEGES ON `pwnedhub-test` . * TO 'pwnedhub'@'%';
8 | GRANT ALL PRIVILEGES ON `pwnedhub-admin` . * TO 'pwnedhub'@'%';
9 |
--------------------------------------------------------------------------------
/database/init/01-init.sql:
--------------------------------------------------------------------------------
1 | -- Setup databases and users
2 | CREATE DATABASE IF NOT EXISTS `pwnedhub`;
3 | CREATE DATABASE IF NOT EXISTS `pwnedhub-test`;
4 | CREATE DATABASE IF NOT EXISTS `pwnedhub-admin`;
5 | CREATE USER 'pwnedhub'@'%' IDENTIFIED BY 'dbconnectpass';
6 | GRANT ALL PRIVILEGES ON `pwnedhub` . * TO 'pwnedhub'@'%';
7 | GRANT ALL PRIVILEGES ON `pwnedhub-test` . * TO 'pwnedhub'@'%';
8 | GRANT ALL PRIVILEGES ON `pwnedhub-admin` . * TO 'pwnedhub'@'%';
9 |
--------------------------------------------------------------------------------
/pwnedadmin/constants.py:
--------------------------------------------------------------------------------
1 | RESTRICTED_USERS = [
2 | 'admin@pwnedhub.com',
3 | 'cooper@pwnedhub.com',
4 | 'taylor@pwnedhub.com',
5 | 'tanner@pwnedhub.com',
6 | 'emilee@pwnedhub.com',
7 | ]
8 |
9 | class ConfigTypes:
10 | CONTROL = 'security control'
11 | FEATURE = 'feature'
12 |
13 | def __init__(self):
14 | pass
15 |
16 | @property
17 | def serialized(self):
18 | return [self.CONTROL, self.FEATURE]
19 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/views/users.css:
--------------------------------------------------------------------------------
1 | .users {
2 | padding: 1rem;
3 | }
4 |
5 | .users .avatar {
6 | margin-right: 1rem;
7 | }
8 |
9 | .users .avatar img {
10 | display: block;
11 | object-fit: cover;
12 | width: 4rem;
13 | height: 4rem;
14 | }
15 |
16 | /* desktop users */
17 | @media all and (min-width: 960px) {
18 |
19 | .users .users-table .users-table-row .actions-cell {
20 | width: 100%;
21 | text-align: center;
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/pwnedapi/REQUIREMENTS-base.txt:
--------------------------------------------------------------------------------
1 | eventlet
2 | flask
3 | flask-cors
4 | flask-mysqldb
5 | flask-restful
6 | flask-socketio
7 | flask-sqlalchemy
8 | # https://github.com/benoitc/gunicorn/issues/2828
9 | # https://www.pythonfixing.com/2022/03/fixed-gunicorn-importerror-cannot.html
10 | # https://github.com/benoitc/gunicorn/archive/refs/heads/master.zip#egg=gunicorn==20.1.0
11 | gunicorn
12 | jsonpickle
13 | lxml
14 | mysqlclient
15 | pyjwt
16 | redis
17 | rq
18 | requests
19 | cryptography
20 | jwcrypto
21 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/__pycache__
2 | **/.venv
3 | **/.classpath
4 | **/.dockerignore
5 | **/.env
6 | **/.git
7 | **/.gitignore
8 | **/.project
9 | **/.settings
10 | **/.toolstarget
11 | **/.vs
12 | **/.vscode
13 | **/*.*proj.user
14 | **/*.dbmdl
15 | **/*.jfm
16 | **/bin
17 | **/charts
18 | **/docker-compose*
19 | **/compose*
20 | **/Dockerfile*
21 | **/node_modules
22 | **/npm-debug.log
23 | **/obj
24 | **/secrets.dev.yaml
25 | **/values.dev.yaml
26 | LICENSE.txt
27 | README.md
28 |
--------------------------------------------------------------------------------
/pwnedhub/templates/profile_view.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
4 |
5 |
{{ user.name }}
6 |
Member since: {{ user.created_as_string[:-9] }}
7 |
8 |
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/adminbot/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.10-alpine
2 |
3 | ENV BUILD_DEPS=""
4 | ENV RUNTIME_DEPS="firefox"
5 |
6 | ENV PYTHONDONTWRITEBYTECODE=1
7 | ENV PYTHONUNBUFFERED=1
8 |
9 | RUN mkdir -p /src
10 |
11 | WORKDIR /src
12 |
13 | ADD ./REQUIREMENTS.txt /src/REQUIREMENTS.txt
14 |
15 | RUN apk update &&\
16 | apk add --no-cache $BUILD_DEPS $RUNTIME_DEPS &&\
17 | pip install --no-cache-dir --upgrade pip &&\
18 | pip install --no-cache-dir -r REQUIREMENTS.txt &&\
19 | apk del $BUILD_DEPS &&\
20 | rm -rf /var/cache/apk/*
21 |
--------------------------------------------------------------------------------
/pwnedhub/templates/reset_init.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
4 |
10 |
11 | {% endblock %}
12 |
--------------------------------------------------------------------------------
/pwnedspa/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.10-alpine
2 |
3 | ENV BUILD_DEPS="build-base gcc libc-dev"
4 | ENV RUNTIME_DEPS=""
5 |
6 | ENV PYTHONDONTWRITEBYTECODE=1
7 | ENV PYTHONUNBUFFERED=1
8 |
9 | RUN mkdir -p /src
10 |
11 | WORKDIR /src
12 |
13 | ADD ./REQUIREMENTS.txt /src/REQUIREMENTS.txt
14 |
15 | RUN apk update &&\
16 | apk add --no-cache $BUILD_DEPS $RUNTIME_DEPS &&\
17 | pip install --no-cache-dir --upgrade pip &&\
18 | pip install --no-cache-dir -r REQUIREMENTS.txt &&\
19 | apk del $BUILD_DEPS &&\
20 | rm -rf /var/cache/apk/*
21 |
--------------------------------------------------------------------------------
/pwnedhub/templates/reset_question.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
4 |
10 |
11 | {% endblock %}
12 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/components/background.css:
--------------------------------------------------------------------------------
1 | .background {
2 | position: fixed;
3 | z-index: -10;
4 | top: 0;
5 | left: 0;
6 | min-width: 100%;
7 | min-height: 100%;
8 | background-position: center center;
9 | background-repeat: no-repeat;
10 | background-attachment: fixed;
11 | background-size: cover;
12 | }
13 |
14 | .background-auth {
15 | background-image: linear-gradient(120deg, #fdfbfb 0%, #ebedee 100%);
16 | }
17 |
18 | .background-unauth {
19 | background-image: url(/static/common/images/background-dark.jpg);
20 | }
21 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/components/toasts.js:
--------------------------------------------------------------------------------
1 | import { useAppStore } from '../stores/app-store.js';
2 |
3 | const template = `
4 |
5 | {{ toast.text }}
6 |
7 | `;
8 |
9 | export default {
10 | name: 'Toasts',
11 | template,
12 | setup () {
13 | const appStore = useAppStore();
14 | return {
15 | appStore,
16 | };
17 | },
18 | };
19 |
--------------------------------------------------------------------------------
/pwnedsso/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.10-alpine
2 |
3 | ENV BUILD_DEPS="build-base gcc libc-dev mariadb-dev"
4 | ENV RUNTIME_DEPS="mariadb-connector-c-dev"
5 |
6 | ENV PYTHONDONTWRITEBYTECODE=1
7 | ENV PYTHONUNBUFFERED=1
8 |
9 | RUN mkdir -p /src
10 |
11 | WORKDIR /src
12 |
13 | ADD ./REQUIREMENTS.txt /src/REQUIREMENTS.txt
14 |
15 | RUN apk update &&\
16 | apk add --no-cache $BUILD_DEPS $RUNTIME_DEPS &&\
17 | pip install --no-cache-dir --upgrade pip &&\
18 | pip install --no-cache-dir -r REQUIREMENTS.txt &&\
19 | apk del $BUILD_DEPS &&\
20 | rm -rf /var/cache/apk/*
21 |
--------------------------------------------------------------------------------
/pwnedsso/__init__.py:
--------------------------------------------------------------------------------
1 | from flask import Flask
2 | from pwnedsso.extensions import db
3 | import os
4 |
5 | def create_app():
6 |
7 | # create the Flask application
8 | app = Flask(__name__, static_url_path='/static')
9 |
10 | # configure the Flask application
11 | config_class = os.getenv('CONFIG', default='Development')
12 | app.config.from_object('pwnedsso.config.{}'.format(config_class.title()))
13 |
14 | db.init_app(app)
15 |
16 | from pwnedsso.routes.sso import blp as SsoBlueprint
17 | app.register_blueprint(SsoBlueprint)
18 |
19 | return app
20 |
--------------------------------------------------------------------------------
/pwnedadmin/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.10-alpine
2 |
3 | ENV BUILD_DEPS="build-base gcc libc-dev mariadb-dev"
4 | ENV RUNTIME_DEPS="mariadb-connector-c-dev"
5 |
6 | ENV PYTHONDONTWRITEBYTECODE=1
7 | ENV PYTHONUNBUFFERED=1
8 |
9 | RUN mkdir -p /src
10 |
11 | WORKDIR /src
12 |
13 | ADD ./REQUIREMENTS.txt /src/REQUIREMENTS.txt
14 |
15 | RUN apk update &&\
16 | apk add --no-cache $BUILD_DEPS $RUNTIME_DEPS &&\
17 | pip install --no-cache-dir --upgrade pip &&\
18 | pip install --no-cache-dir -r REQUIREMENTS.txt &&\
19 | apk del $BUILD_DEPS &&\
20 | rm -rf /var/cache/apk/*
21 |
--------------------------------------------------------------------------------
/pwnedapi/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.10-alpine
2 |
3 | ENV BUILD_DEPS="build-base gcc libc-dev libxslt-dev mariadb-dev"
4 | ENV RUNTIME_DEPS="libxslt mariadb-connector-c-dev"
5 |
6 | ENV PYTHONDONTWRITEBYTECODE=1
7 | ENV PYTHONUNBUFFERED=1
8 |
9 | RUN mkdir -p /src
10 |
11 | WORKDIR /src
12 |
13 | ADD ./REQUIREMENTS.txt /src/REQUIREMENTS.txt
14 |
15 | RUN apk update &&\
16 | apk add --no-cache $BUILD_DEPS $RUNTIME_DEPS &&\
17 | pip install --no-cache-dir --upgrade pip &&\
18 | pip install --no-cache-dir -r REQUIREMENTS.txt &&\
19 | apk del $BUILD_DEPS &&\
20 | rm -rf /var/cache/apk/*
21 |
--------------------------------------------------------------------------------
/pwnedhub/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.10-alpine
2 |
3 | ENV BUILD_DEPS="build-base gcc libc-dev libxslt-dev mariadb-dev"
4 | ENV RUNTIME_DEPS="libxslt mariadb-connector-c-dev"
5 |
6 | ENV PYTHONDONTWRITEBYTECODE=1
7 | ENV PYTHONUNBUFFERED=1
8 |
9 | RUN mkdir -p /src
10 |
11 | WORKDIR /src
12 |
13 | ADD ./REQUIREMENTS.txt /src/REQUIREMENTS.txt
14 |
15 | RUN apk update &&\
16 | apk add --no-cache $BUILD_DEPS $RUNTIME_DEPS &&\
17 | pip install --no-cache-dir --upgrade pip &&\
18 | pip install --no-cache-dir -r REQUIREMENTS.txt &&\
19 | apk del $BUILD_DEPS &&\
20 | rm -rf /var/cache/apk/*
21 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/components/background.js:
--------------------------------------------------------------------------------
1 | import { useAuthStore } from '../stores/auth-store.js';
2 |
3 | const { computed } = Vue;
4 |
5 | const template = `
6 |
7 | `;
8 |
9 | export default {
10 | name: 'Background',
11 | template,
12 | setup () {
13 | const authStore = useAuthStore();
14 | const backgroundClass = computed(() => {
15 | return authStore.isLoggedIn ? 'background-auth' : 'background-unauth';
16 | });
17 | return {
18 | backgroundClass,
19 | };
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/pwnedhub/REQUIREMENTS.txt:
--------------------------------------------------------------------------------
1 | async-timeout==4.0.2
2 | blinker==1.6.2
3 | cachelib==0.13.0
4 | certifi==2023.5.7
5 | charset-normalizer==3.2.0
6 | click==8.1.4
7 | Flask==2.3.2
8 | Flask-MySQLdb==1.0.1
9 | Flask-Session==0.8.0
10 | Flask-SQLAlchemy==3.0.5
11 | greenlet==2.0.2
12 | gunicorn==20.1.0
13 | idna==3.4
14 | itsdangerous==2.1.2
15 | Jinja2==3.1.2
16 | lxml==4.9.3
17 | Markdown==3.4.3
18 | MarkupSafe==2.1.3
19 | msgspec==0.18.6
20 | mysqlclient==2.2.0
21 | PyJWT==2.7.0
22 | redis==4.6.0
23 | requests==2.31.0
24 | rq==1.15.1
25 | SQLAlchemy==2.0.18
26 | typing_extensions==4.7.1
27 | urllib3==2.0.3
28 | Werkzeug==2.3.6
29 |
--------------------------------------------------------------------------------
/pwnedapi/static/swaggerui/swagger-initializer.js:
--------------------------------------------------------------------------------
1 | window.onload = function() {
2 | //
3 |
4 | // the following lines will be replaced by docker/configurator, when it runs in a docker-container
5 | window.ui = SwaggerUIBundle({
6 | url: "http://api.pwnedhub.com/static/openapi.json",
7 | dom_id: '#swagger-ui',
8 | deepLinking: true,
9 | validatorUrl: null,
10 | presets: [
11 | SwaggerUIBundle.presets.apis,
12 | SwaggerUIStandalonePreset
13 | ],
14 | plugins: [
15 | SwaggerUIBundle.plugins.DownloadUrl
16 | ],
17 | layout: "StandaloneLayout"
18 | });
19 |
20 | //
21 | };
22 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/views/signup.css:
--------------------------------------------------------------------------------
1 | .signup {
2 | display: flex;
3 | flex-direction: column;
4 | color: white;
5 | padding: 1rem;
6 | }
7 |
8 | .signup .form {
9 | padding: 1rem 2rem;
10 | background-color: rgba(0,0,0,0.6);
11 | }
12 |
13 | .signup .about {
14 | text-align: justify;
15 | }
16 |
17 | /* desktop signup */
18 | @media all and (min-width: 960px) {
19 |
20 | .signup {
21 | flex-direction: row;
22 | flex-wrap: wrap;
23 | justify-content: center;
24 | }
25 |
26 | .signup > * {
27 | flex-basis: 40rem;
28 | }
29 |
30 | .signup .form {
31 | margin: 0 auto;
32 | width: 30rem;
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/pwnedadmin/routes/email.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, request, render_template, redirect, url_for
2 | from pwnedadmin import db
3 | from pwnedadmin.models import Email
4 |
5 | blp = Blueprint('email', __name__, url_prefix='/inbox')
6 |
7 | @blp.route('/', methods=['GET', 'POST'])
8 | def index():
9 | if user := request.args.get('user'):
10 | emails = Email.get_by_receiver(user)
11 | else:
12 | emails = Email.get_unrestricted()
13 | return render_template('emails.html', emails=emails.order_by(Email.created.desc()).all())
14 |
15 | @blp.route('/empty')
16 | def empty():
17 | emails = Email.query.delete()
18 | db.session.commit()
19 | return redirect(url_for('email.index'))
20 |
--------------------------------------------------------------------------------
/pwnedadmin/templates/layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {% block title %}{% endblock %}
7 |
8 |
9 |
10 |
11 |
12 |
13 | {% block content %}{% endblock %}
14 |
15 |
16 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/views/tools.css:
--------------------------------------------------------------------------------
1 | .tools {
2 | padding: 1rem;
3 | }
4 |
5 | .tools .tool-form {
6 | display: flex;
7 | flex-direction: column;
8 | margin-bottom: 1rem;
9 | }
10 |
11 | .tools .tools-table pre {
12 | white-space: pre-wrap;
13 | }
14 |
15 | /* desktop tools */
16 | @media all and (min-width: 960px) {
17 |
18 | /*flex-row flex-wrap flex-align-center */
19 | .tools .tool-form {
20 | flex-direction: row;
21 | flex-wrap: wrap;
22 | }
23 |
24 | .tools .tool-form input {
25 | margin-right: 1rem;
26 | }
27 |
28 | .tools .tools-table .tools-table-row .actions-cell {
29 | width: 100%;
30 | text-align: center;
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/components/toasts.css:
--------------------------------------------------------------------------------
1 | .toasts {
2 | position: fixed;
3 | z-index: 10;
4 | width: 100%;
5 | top: 0;
6 | }
7 |
8 | .toast {
9 | width: 100%;
10 | text-align: center;
11 | padding: 1rem 0;
12 | font-size: 1.5rem;
13 | background-color: red;
14 | color: #fff;
15 | }
16 |
17 | .toast:last-child {
18 | box-shadow: 0 0 1rem rgba(0, 0, 0, 0.5);
19 | }
20 |
21 | .toasts-move,
22 | .toasts-enter-active,
23 | .toasts-leave-active {
24 | transition: all 1s ease;
25 | /*transition-delay: .75s;*/
26 | }
27 |
28 | .toasts-enter-from,
29 | .toasts-leave-to {
30 | transform: translateY(-100%);
31 | }
32 |
33 | .toasts-leave-active {
34 | position: absolute;
35 | }
36 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/app.js:
--------------------------------------------------------------------------------
1 | import Background from './components/background.js';
2 | import Toasts from './components/toasts.js';
3 | import Modal from './components/modal.js';
4 | import Navigation from './components/navigation.js';
5 |
6 | const template = `
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | `;
15 |
16 | export default {
17 | name: 'App',
18 | template,
19 | components: {
20 | 'background': Background,
21 | 'toasts': Toasts,
22 | 'modal': Modal,
23 | 'navigation': Navigation,
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/adminbot/tasks.py:
--------------------------------------------------------------------------------
1 | from adminbot.bot import bot_driver, HubBot, Hub20Bot
2 |
3 | def www_login_read_first_mail_respond(name, username, password, receiver_id, subject, content):
4 | with bot_driver() as driver:
5 | bot = HubBot(driver, name)
6 | # login
7 | bot.log_in(username, password)
8 | # read first mail
9 | bot.read_mail()
10 | # respond
11 | bot.compose_mail(receiver_id, subject, content)
12 |
13 | def test_login_send_private_message(name, email, room_id, message):
14 | with bot_driver() as driver:
15 | bot = Hub20Bot(driver, name)
16 | # login
17 | bot.log_in(email)
18 | # send private message
19 | bot.send_private_message(room_id, message)
20 |
--------------------------------------------------------------------------------
/pwnedsso/config.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 |
4 | class BaseConfig(object):
5 |
6 | # base
7 | DEBUG = False
8 | TESTING = False
9 | SECRET_KEY = os.getenv('SECRET_KEY', default='$ecretKey')
10 | # prevents connection pool exhaustion but disables interactive debugging
11 | PRESERVE_CONTEXT_ON_EXCEPTION = False
12 |
13 | # database
14 | DATABASE_HOST = os.environ.get('DATABASE_HOST', 'localhost')
15 | SQLALCHEMY_DATABASE_URI = f"mysql://pwnedhub:dbconnectpass@{DATABASE_HOST}/pwnedhub"
16 | SQLALCHEMY_TRACK_MODIFICATIONS = False
17 |
18 |
19 | class Development(BaseConfig):
20 |
21 | DEBUG = True
22 |
23 |
24 | class Test(BaseConfig):
25 |
26 | DEBUG = True
27 | TESTING = True
28 |
29 |
30 | class Production(BaseConfig):
31 |
32 | pass
33 |
--------------------------------------------------------------------------------
/pwnedadmin/routes/config.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, request, render_template, abort, redirect, url_for
2 | from pwnedadmin import db
3 | from pwnedadmin.constants import ConfigTypes
4 | from pwnedadmin.models import Config
5 |
6 | blp = Blueprint('config', __name__, url_prefix='/config')
7 |
8 | @blp.route('/', methods=['GET', 'POST'])
9 | def index():
10 | if Config.get_value('CTF_MODE'):
11 | abort(404)
12 | configs = Config.query.all()
13 | if request.method == 'POST':
14 | for config in configs:
15 | config.value = request.form.get(config.name.lower()) == 'on'
16 | db.session.commit()
17 | return redirect(url_for('config.index'))
18 | return render_template('config.html', configs=configs, config_types=ConfigTypes().serialized)
19 |
--------------------------------------------------------------------------------
/pwnedadmin/config.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 |
4 | class BaseConfig(object):
5 |
6 | # base
7 | DEBUG = False
8 | TESTING = False
9 | SECRET_KEY = os.getenv('SECRET_KEY', default='$ecretKey')
10 | # prevents connection pool exhaustion but disables interactive debugging
11 | PRESERVE_CONTEXT_ON_EXCEPTION = False
12 |
13 | # database
14 | DATABASE_HOST = os.environ.get('DATABASE_HOST', 'localhost')
15 | SQLALCHEMY_DATABASE_URI = f"mysql://pwnedhub:dbconnectpass@{DATABASE_HOST}/pwnedhub-admin"
16 | SQLALCHEMY_TRACK_MODIFICATIONS = False
17 |
18 |
19 | class Development(BaseConfig):
20 |
21 | DEBUG = True
22 |
23 |
24 | class Test(BaseConfig):
25 |
26 | DEBUG = True
27 | TESTING = True
28 |
29 |
30 | class Production(BaseConfig):
31 |
32 | pass
33 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/components/password-field.js:
--------------------------------------------------------------------------------
1 | const { ref } = Vue;
2 |
3 | const template = `
4 |
5 |
6 |
7 |
8 | `;
9 |
10 | export default {
11 | name: 'PasswordField',
12 | template,
13 | props: {
14 | name: String,
15 | value: String,
16 | },
17 | setup () {
18 | const showPassword = ref(false);
19 | return {
20 | showPassword,
21 | };
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/pwnedhub/templates/macros.html:
--------------------------------------------------------------------------------
1 | {% macro pagination(route, items) %}
2 |
20 | {% endmacro %}
21 |
--------------------------------------------------------------------------------
/pwnedapi/constants.py:
--------------------------------------------------------------------------------
1 | ROLES = {
2 | 0: 'admin',
3 | 1: 'user',
4 | }
5 |
6 | USER_STATUSES = {
7 | 0: 'disabled',
8 | 1: 'enabled',
9 | }
10 |
11 | DEFAULT_NOTE = '''##### Welcome to PwnedHub 2.0!
12 |
13 | A collaborative space to conduct hosted security assessments.
14 |
15 | **Find flaws.**
16 |
17 | * This is your notes space. Keep your personal notes here.
18 | * Leverage popular security testing tools right from your browser in the tools space.
19 |
20 | **Collaborate.**
21 |
22 | * Privately collaborate with coworkers in the messaging space.
23 | * Join public rooms in the messaging space to share information and socialize.
24 |
25 | **On the Move**
26 |
27 | * PwnedHub 2.0 is built with mobility in mind. No need for a separate app!
28 |
29 | Happy hunting!
30 |
31 | \- The PwnedHub Team
32 |
33 | '''
34 |
--------------------------------------------------------------------------------
/pwnedapi/static/swaggerui/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Swagger UI
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/pwnedspa/__init__.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, Blueprint
2 | import os
3 |
4 | def create_app():
5 |
6 | # create the Flask application
7 | app = Flask(__name__, static_url_path='/static')
8 |
9 | # configure the Flask application
10 | config_class = os.getenv('CONFIG', default='Development')
11 | app.config.from_object('pwnedspa.config.{}'.format(config_class.title()))
12 |
13 | # misc jinja configuration variables
14 | app.jinja_env.trim_blocks = True
15 | app.jinja_env.lstrip_blocks = True
16 |
17 | StaticBlueprint = Blueprint('common', __name__, static_url_path='/static/common', static_folder='../common/static')
18 | app.register_blueprint(StaticBlueprint)
19 |
20 | from pwnedspa.routes.core import blp as CoreBlueprint
21 | app.register_blueprint(CoreBlueprint)
22 |
23 | return app
24 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/components/modal.js:
--------------------------------------------------------------------------------
1 | import { useAppStore } from '../stores/app-store.js';
2 |
3 | const template = `
4 |
5 |
13 |
14 | `;
15 |
16 | export default {
17 | name: 'Modal',
18 | template,
19 | setup () {
20 | const appStore = useAppStore();
21 | return {
22 | appStore,
23 | };
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/views/account.css:
--------------------------------------------------------------------------------
1 | .account {
2 | display: flex;
3 | flex-direction: column;
4 | padding: 1rem;
5 | }
6 |
7 | .account .avatar {
8 | width: 80%;
9 | padding-bottom: 80%;
10 | margin: 1.5rem auto;
11 | position: relative;
12 | }
13 |
14 | .account .avatar img {
15 | position: absolute;
16 | left: 0;
17 | top: 0;
18 | width: 100%;
19 | height: 100%;
20 | object-fit: cover;
21 | }
22 |
23 | .account .form {
24 | padding: 0.5rem 0;
25 | }
26 |
27 | /* desktop account */
28 | @media all and (min-width: 960px) {
29 |
30 | .account {
31 | flex-direction: row;
32 | flex-wrap: wrap;
33 | justify-content: center;
34 | }
35 |
36 | .account > * {
37 | flex-basis: 40rem;
38 | }
39 |
40 | .account .form {
41 | margin: 0 auto;
42 | width: 30rem;
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/pwnedapi/REQUIREMENTS.txt:
--------------------------------------------------------------------------------
1 | aniso8601==9.0.1
2 | async-timeout==4.0.2
3 | bidict==0.22.1
4 | blinker==1.6.2
5 | certifi==2023.5.7
6 | cffi==1.16.0
7 | charset-normalizer==3.2.0
8 | click==8.1.4
9 | cryptography==41.0.4
10 | Deprecated==1.2.14
11 | dnspython==2.3.0
12 | eventlet==0.33.3
13 | Flask==2.3.2
14 | Flask-Cors==4.0.0
15 | Flask-MySQLdb==1.0.1
16 | Flask-RESTful==0.3.10
17 | Flask-SocketIO==5.3.4
18 | Flask-SQLAlchemy==3.0.5
19 | greenlet==2.0.2
20 | gunicorn==21.2.0
21 | idna==3.4
22 | itsdangerous==2.1.2
23 | Jinja2==3.1.2
24 | jsonpickle==3.0.1
25 | jwcrypto==1.5.0
26 | lxml==4.9.3
27 | MarkupSafe==2.1.3
28 | mysqlclient==2.2.0
29 | packaging==23.1
30 | pycparser==2.21
31 | PyJWT==2.7.0
32 | python-engineio==4.5.1
33 | python-socketio==5.8.0
34 | pytz==2023.3
35 | redis==4.6.0
36 | requests==2.31.0
37 | rq==1.15.1
38 | six==1.16.0
39 | SQLAlchemy==2.0.18
40 | typing_extensions==4.7.1
41 | urllib3==2.0.3
42 | Werkzeug==2.3.6
43 | wrapt==1.15.0
44 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/views/login.css:
--------------------------------------------------------------------------------
1 | .login {
2 | display: flex;
3 | flex-direction: column;
4 | color: white;
5 | padding: 1rem;
6 | }
7 |
8 | .login .form {
9 | padding: 1rem 2rem;
10 | background-color: rgba(0,0,0,0.6);
11 | }
12 |
13 | .login .form .oidc-button {
14 | cursor: pointer;
15 | max-width: 100%;
16 | }
17 |
18 | .login .panels > * {
19 | flex-basis: 20rem;
20 | margin: 2rem 0;
21 | }
22 |
23 | /* desktop login */
24 | @media all and (min-width: 960px) {
25 |
26 | .login {
27 | flex-direction: row;
28 | flex-wrap: wrap;
29 | justify-content: center;
30 | width: 960px;
31 | }
32 |
33 | .login > * {
34 | flex-basis: 40rem;
35 | }
36 |
37 | .login .form {
38 | margin: 0 auto;
39 | width: 30rem;
40 | }
41 |
42 | .login .panels {
43 | flex-basis: 100%;
44 | margin-top: 5rem;
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/pwnedhub/templates/reset_password.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
4 | {% if app_config('OOB_RESET_ENABLE') %}
5 |
17 |
18 | {% endblock %}
19 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/views/profile.css:
--------------------------------------------------------------------------------
1 | .profile {
2 | padding: 1rem;
3 | }
4 |
5 | .profile .avatar {
6 | width: 80%;
7 | padding-bottom: 80%;
8 | margin: 2rem auto;
9 | position: relative;
10 | }
11 |
12 | .profile .avatar img {
13 | position: absolute;
14 | left: 0;
15 | top: 0;
16 | width: 100%;
17 | height: 100%;
18 | object-fit: cover;
19 | }
20 |
21 | .profile blockquote {
22 | font-style: italic;
23 | margin-left: 0;
24 | margin-right: 0;
25 | quotes: "\201C""\201D""\2018""\2019";
26 | }
27 |
28 | .profile blockquote:before {
29 | color: #bbb;
30 | font-family: Arial;
31 | content: open-quote;
32 | font-size: 4em;
33 | line-height: 0.1em;
34 | margin-right: 0.25em;
35 | vertical-align: -0.4em;
36 | }
37 |
38 | /* desktop profile */
39 | @media all and (min-width: 960px) {
40 |
41 | .profile {
42 | margin: 0 auto;
43 | width: 30rem;
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/pwnedsso/routes/sso.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, request, redirect
2 | from pwnedsso.models import User
3 | from pwnedsso.utils import encode_jwt
4 | from urllib.parse import urlencode
5 |
6 | blp = Blueprint('sso', __name__)
7 |
8 | # sso controllers
9 |
10 | @blp.route('/authenticate', methods=['POST'])
11 | def authenticate():
12 | username = request.form.get('username')
13 | password = request.form.get('password')
14 | user = None
15 | if username and password:
16 | untrusted_user = User.get_by_username(username)
17 | if untrusted_user and untrusted_user.check_password(password):
18 | user = untrusted_user
19 | params = {}
20 | params['id_token'] = encode_jwt(user.username if user else None)
21 | if 'next' in request.args:
22 | params['next'] = request.args.get('next')
23 | redirect_url = '?'.join(['http://www.pwnedhub.com/sso/login', urlencode(params)])
24 | return redirect(redirect_url)
25 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/views/activate.js:
--------------------------------------------------------------------------------
1 | import { useAppStore } from '../stores/app-store.js';
2 | import { User } from '../services/api.js';
3 |
4 | const { ref } = Vue;
5 | const { useRouter } = VueRouter;
6 |
7 | export default {
8 | name: 'Activate',
9 | props: {
10 | activateToken: String,
11 | },
12 | setup (props) {
13 | const appStore = useAppStore();
14 | const router = useRouter();
15 |
16 | const activateForm = ref({
17 | activate_token: props.activateToken,
18 | });
19 |
20 | async function activateUser() {
21 | try {
22 | await User.create(activateForm.value);
23 | appStore.createToast('Account activated. Please log in.');
24 | } catch (error) {
25 | appStore.createToast(error.message);
26 | };
27 | router.push({ name: 'login' });
28 | };
29 |
30 | activateUser();
31 | },
32 | };
33 |
--------------------------------------------------------------------------------
/pwnedhub/templates/mail_reply.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
18 | {% endblock %}
19 |
--------------------------------------------------------------------------------
/pwnedsso/utils.py:
--------------------------------------------------------------------------------
1 | from flask import current_app
2 | from datetime import datetime, timezone, timedelta
3 | from itertools import cycle
4 | import base64
5 | import jwt
6 |
7 | def get_current_utc_time():
8 | return datetime.now(tz=timezone.utc)
9 |
10 | def get_local_from_utc(dtg):
11 | return dtg.replace(tzinfo=timezone.utc).astimezone(tz=None)
12 |
13 | def xor_encrypt(s, k):
14 | ciphertext = ''.join([ chr(ord(c)^ord(k)) for c,k in zip(s, cycle(k)) ])
15 | return base64.b64encode(ciphertext.encode()).decode()
16 |
17 | def encode_jwt(user_id, claims={}, expire_delta={'days': 1, 'seconds': 0}):
18 | payload = {
19 | 'exp': get_current_utc_time() + timedelta(**expire_delta),
20 | 'iat': get_current_utc_time(),
21 | 'sub': user_id
22 | }
23 | for claim, value in claims.items():
24 | payload[claim] = value
25 | return jwt.encode(
26 | payload,
27 | current_app.config['SECRET_KEY'],
28 | algorithm='HS256'
29 | )
30 |
--------------------------------------------------------------------------------
/pwnedapi/fixtures/base/rooms.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "general",
4 | "private": false,
5 | "id": 1,
6 | "created": "2019-02-16 01:51:59",
7 | "modified": "2019-02-16 01:51:59"
8 | },
9 | {
10 | "name": "f9adeea0",
11 | "private": true,
12 | "id": 2,
13 | "created": "2023-07-17 04:58:05",
14 | "modified": "2023-07-17 04:58:05"
15 | },
16 | {
17 | "name": "a28b1e3e",
18 | "private": true,
19 | "id": 3,
20 | "created": "2023-07-17 04:58:06",
21 | "modified": "2023-07-17 04:58:06"
22 | },
23 | {
24 | "name": "2ce70a5f",
25 | "private": true,
26 | "id": 4,
27 | "created": "2023-07-17 04:58:08",
28 | "modified": "2023-07-17 04:58:08"
29 | },
30 | {
31 | "name": "ae206386",
32 | "private": true,
33 | "id": 5,
34 | "created": "2023-07-17 04:58:09",
35 | "modified": "2023-07-17 04:58:09"
36 | }
37 | ]
38 |
--------------------------------------------------------------------------------
/pwnedapi/tasks.py:
--------------------------------------------------------------------------------
1 | from pwnedapi import create_app, db
2 | from pwnedapi.models import Scan
3 | from rq import get_current_job
4 | import os
5 | import subprocess
6 | import sys
7 | import traceback
8 |
9 | def execute_tool(cmd):
10 | app, socketio = create_app()
11 | with app.app_context():
12 | try:
13 | output = ''
14 | env = os.environ.copy()
15 | env['PATH'] = os.pathsep.join(('/usr/bin', env['PATH']))
16 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, env=env)
17 | out, err = p.communicate()
18 | output = (out + err).decode()
19 | except:
20 | app.logger.error('Unhandled exception', exc_info=sys.exc_info())
21 | output = traceback.format_exc()
22 | finally:
23 | job = get_current_job()
24 | scan = Scan.query.get(job.get_id())
25 | scan.complete = True
26 | scan.results = output
27 | db.session.commit()
28 |
--------------------------------------------------------------------------------
/pwnedspa/config.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 |
4 | class BaseConfig(object):
5 |
6 | # base
7 | DEBUG = False
8 | TESTING = False
9 | SECRET_KEY = os.getenv('SECRET_KEY', default='$ecretKey')
10 | # prevents connection pool exhaustion but disables interactive debugging
11 | PRESERVE_CONTEXT_ON_EXCEPTION = False
12 |
13 | # oidc
14 | OAUTH_PROVIDERS = {
15 | 'google': {
16 | 'CLIENT_ID': '1098478339188-pvi39gpsvclmmucvu16vhrh0179sd100.apps.googleusercontent.com',
17 | 'CLIENT_SECRET': '5LFAbNk7rLa00PZOHceQfudp',
18 | 'DISCOVERY_DOC': 'https://accounts.google.com/.well-known/openid-configuration',
19 | },
20 | }
21 |
22 | # csrf
23 | CSRF_TOKEN_NAME = 'X-Csrf-Token'
24 |
25 | # other
26 | API_BASE_URL = 'http://api.pwnedhub.com'
27 |
28 |
29 | class Development(BaseConfig):
30 |
31 | DEBUG = True
32 |
33 |
34 | class Test(BaseConfig):
35 |
36 | DEBUG = True
37 | TESTING = True
38 |
39 |
40 | class Production(BaseConfig):
41 |
42 | pass
43 |
--------------------------------------------------------------------------------
/pwnedhub/templates/about.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
4 |
Welcome to PwnedHub !
5 |
The ability to consolidate and organize testing tools and results during client engagements is key for consultants dealing with short timelines and high expectations. Unfortunately, today's options for cloud resourced security testing are poorly designed and fail to support even the most basic needs. PwnedHub attempts to solve this problem by providing a space to share knowledge, execute test cases, and store the results.
6 |
Developed by child prodigies Cooper ("Cooperman"), Taylor ("Babygirl#1"), and Tanner ("Hack3rPrincess"), PwnedHub was designed based on experience gained through months of security testing. The PwnedHub team is ambitions, talented, and so confident in their product, if you don't like it, they'll issue a full refund. No questions asked.
7 |
So what are you waiting for? Click here and get to work!
8 |
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/pwnedhub/templates/mail_compose.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
23 | {% endblock %}
24 |
--------------------------------------------------------------------------------
/pwnedhub/templates/index.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
4 |
5 |
A collaborative space to conduct hosted security assessments.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Scan.
13 |
14 |
15 |
16 |
Find.
17 |
18 |
19 |
20 |
Win.
21 |
22 |
23 |
24 |
25 | {% endblock %}
26 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/style.css:
--------------------------------------------------------------------------------
1 | @import "/static/common/css/fontawesome.css";
2 | @import "/static/common/css/normalize.css";
3 | @import "/static/common/css/custom-flex.css";
4 | @import "/static/common/css/custom-utility.css";
5 | @import "/static/common/css/baseline.css";
6 | @import "/static/common/css/pwnedspa.css";
7 | @import "/static/vue/components/modal.css";
8 | @import "/static/vue/components/toasts.css";
9 | @import "/static/vue/components/navigation.css";
10 | @import "/static/vue/components/background.css";
11 | @import "/static/vue/views/users.css";
12 | @import "/static/vue/views/tools.css";
13 | @import "/static/vue/views/messages.css";
14 | @import "/static/vue/components/link-preview.css";
15 | @import "/static/vue/views/scans.css";
16 | @import "/static/vue/views/notes.css";
17 | @import "/static/vue/views/profile.css";
18 | @import "/static/vue/views/account.css";
19 | @import "/static/vue/views/reset.css";
20 | @import "/static/vue/views/passwordless.css";
21 | @import "/static/vue/views/login.css";
22 | @import "/static/vue/views/signup.css";
23 |
24 | .content-wrapper {
25 | position: relative;
26 | z-index: auto;
27 | }
28 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/components/modal.css:
--------------------------------------------------------------------------------
1 | .modal-mask {
2 | position: fixed;
3 | z-index: 20;
4 | top: 0;
5 | left: 0;
6 | width: 100%;
7 | height: 100%;
8 | background-color: rgba(0, 0, 0, .5);
9 | transition: opacity .3s ease;
10 | overflow-x: auto;
11 | }
12 |
13 | .modal-mask .modal-container {
14 | width: 100%;
15 | height: 100%;
16 | position: relative;
17 | padding: 3rem 2rem 2rem;
18 | background-color: #fff;
19 | transition: all .3s ease;
20 | }
21 |
22 | .modal-mask .modal-container a.img-btn {
23 | position: absolute;
24 | top: 0.5rem;
25 | right: 0.5rem;
26 | }
27 |
28 | .modal-enter-active,
29 | .modal-leave-active {
30 | opacity: 0;
31 | }
32 |
33 | .modal-enter-active .modal-container,
34 | .modal-leave-active .modal-container {
35 | -webkit-transform: scale(1.1);
36 | transform: scale(1.1);
37 | }
38 |
39 | /* desktop modal */
40 | @media all and (min-width: 960px) {
41 |
42 | .modal-mask .modal-container {
43 | width: 80%;
44 | height: 80%;
45 | border-radius: .5rem;
46 | box-shadow: 0 0 1rem rgba(0, 0, 0, 0.5);
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/pwnedhub/templates/mail_inbox.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
24 | {% endblock %}
25 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/stores/app-store.js:
--------------------------------------------------------------------------------
1 | const { defineStore } = Pinia;
2 | const { ref, shallowRef } = Vue;
3 |
4 | export const useAppStore = defineStore('app', () => {
5 | const toasts = ref([]);
6 | const modalVisible = ref(false);
7 | const modalComponent = shallowRef(null);
8 | const modalProps = ref({});
9 |
10 | let maxToastId = 0;
11 |
12 | function createToast(message) {
13 | const id = ++maxToastId;
14 | toasts.value.push({id: id, text: message});
15 | setTimeout(() => {
16 | toasts.value = toasts.value.filter(t => t.id !== id)
17 | }, 5000);
18 | };
19 |
20 | function showModal(payload) {
21 | modalVisible.value = true;
22 | modalComponent.value = payload.componentName;
23 | modalProps.value = payload.props;
24 | };
25 |
26 | function hideModal() {
27 | modalVisible.value = false;
28 | modalComponent.value = null;
29 | modalProps.value = {};
30 | };
31 |
32 | return {
33 | toasts,
34 | modalVisible,
35 | modalComponent,
36 | modalProps,
37 | createToast,
38 | showModal,
39 | hideModal,
40 | };
41 | });
42 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/views/profile.js:
--------------------------------------------------------------------------------
1 | import { useAppStore } from '../stores/app-store.js';
2 | import { User } from '../services/api.js';
3 |
4 | const { ref } = Vue;
5 |
6 | const template = `
7 |
8 |
9 |
{{ user.name }}
10 |
Member since: {{ user.created }}
11 |
12 |
13 | `;
14 |
15 | export default {
16 | name: 'Profile',
17 | template,
18 | props: {
19 | userId: [Number, String],
20 | },
21 | setup (props) {
22 | const appStore = useAppStore();
23 |
24 | const user = ref(null);
25 |
26 | async function getUser() {
27 | try {
28 | const json = await User.get(props.userId);
29 | user.value = json;
30 | } catch (error) {
31 | appStore.createToast(error.message);
32 | };
33 | };
34 |
35 | getUser();
36 |
37 | return {
38 | user,
39 | };
40 | },
41 | };
42 |
--------------------------------------------------------------------------------
/pwnedhub/templates/mail_view.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
4 |
{{ letter.subject }}
5 |
6 |
7 |
{{ letter.sender.name }} → {{ letter.receiver.name }} @ {{ letter.created_as_string }}
8 |
9 |
{{ letter.content|safe }}
10 |
11 |
16 |
17 | {% endblock %}
18 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/views/notes.css:
--------------------------------------------------------------------------------
1 | .notes {
2 | height: 100%;
3 | }
4 |
5 | .notes textarea {
6 | height: auto; /* skeleton override */
7 | padding: 1rem 1.5rem;
8 | border: 1px solid #bbb;
9 | margin: 1rem;
10 | resize: none;
11 | }
12 |
13 | .markdown {
14 | padding: 2rem 3rem;
15 | border: 1px solid #e1e1e1;
16 | }
17 |
18 | .markdown li {
19 | margin-bottom: 0;
20 | }
21 |
22 | .markdown code {
23 | overflow: scroll;
24 | }
25 |
26 | /* tabs */
27 |
28 | .tabs {
29 | }
30 |
31 | .tabs > input[type=radio] {
32 | display: none;
33 | }
34 |
35 | .tabs > label {
36 | background-color: #eee;
37 | border: 1px solid #e1e1e1;
38 | padding: 0.5rem 1rem;
39 | margin: 0;
40 | cursor: pointer;
41 | z-index: 1;
42 | }
43 |
44 | .tabs > input[type=radio]:checked + label {
45 | background: #fff;
46 | border-bottom: 1px solid #fff;
47 | }
48 |
49 | .tab-content > div {
50 | margin-top: -1px; /* overlaps the border of the lab */
51 | background-color: #fff;
52 | border: 1px solid #e1e1e1;
53 | }
54 |
55 | .tab-content > div:not(.active) {
56 | position: absolute;
57 | top: -9999px;
58 | left: -9999px;
59 | height: 1px;
60 | width: 1px;
61 | }
62 |
63 | /* tabs end */
64 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/views/passwordless.js:
--------------------------------------------------------------------------------
1 | import { useAuthStore } from '../stores/auth-store.js';
2 |
3 | const { ref, onBeforeUnmount } = Vue;
4 |
5 | const template = `
6 |
15 | `;
16 |
17 | export default {
18 | name: 'PasswordlessAuth',
19 | template,
20 | setup () {
21 | const authStore = useAuthStore();
22 |
23 | const codeForm = ref({
24 | code: '',
25 | code_token: '',
26 | });
27 |
28 | function doSubmitCode() {
29 | codeForm.value.code_token = authStore.codeToken;
30 | authStore.doLogin(codeForm.value);
31 | };
32 |
33 | onBeforeUnmount(() => {
34 | authStore.unsetCodeToken();
35 | });
36 |
37 | return {
38 | codeForm,
39 | doSubmitCode,
40 | };
41 | },
42 | };
43 |
--------------------------------------------------------------------------------
/pwnedhub/constants.py:
--------------------------------------------------------------------------------
1 | ROLES = {
2 | 0: 'admin',
3 | 1: 'user',
4 | }
5 |
6 | USER_STATUSES = {
7 | 0: 'disabled',
8 | 1: 'enabled',
9 | }
10 |
11 | QUESTIONS = {
12 | 0: 'Favorite food?',
13 | 1: 'Pet\'s name?',
14 | 2: 'High school mascot?',
15 | 3: 'Birthplace?',
16 | 4: 'First employer?',
17 | }
18 |
19 | DEFAULT_NOTE = '''##### Welcome to PwnedHub!
20 |
21 | A collaborative space to conduct hosted security assessments.
22 |
23 | **Find flaws.**
24 |
25 | * This is your notes space. Keep your personal notes here.
26 | * Store artifacts from local and external tools in the artifacts space.
27 | * Leverage popular security testing tools right from your browser in the tools space.
28 |
29 | **Collaborate.**
30 |
31 | * Privately collaborate with coworkers in the PwnMail space.
32 | * Share public information and socialize in the messages space.
33 |
34 | Happy hunting!
35 |
36 | \\- The PwnedHub Team
37 |
38 | '''
39 |
40 | ADMIN_RESPONSE = {
41 | 'default': 'I would be more than happy to help you with that. Unfortunately, the person responsible for that is unavailable at the moment. We\'ll get back with you soon. Thanks.',
42 | 'password': 'Hey no problem. We all forget our password every now and then. Your current password is {password}, but you can simply reset it using the Forgot Password link on the login page. I hope this helps. Have a great day!'
43 | }
44 |
--------------------------------------------------------------------------------
/pwnedhub/templates/admin_tools.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
30 | {% endblock %}
31 |
--------------------------------------------------------------------------------
/pwnedadmin/templates/config.html:
--------------------------------------------------------------------------------
1 | {% extends 'layout.html' %}
2 | {% block title %}PwnedAdmin | Config{% endblock %}
3 | {% block content %}
4 |
5 | {% for config_type in config_types %}
6 |
7 |
8 |
{{ config_type|title }}
9 |
10 |
11 | {% for config in configs %}
12 | {% if config.type == config_type %}
13 |
14 |
15 | {{ config.description }} - {% if not app_config(config.name) %}DISABLED {% else %}ENABLED {% endif %}
16 |
17 | {% endif %}
18 | {% endfor %}
19 |
20 |
21 | {% endfor %}
22 |
23 |
30 | {% endblock %}
31 |
--------------------------------------------------------------------------------
/pwnedapi/fixtures/base/tools.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "Dig",
4 | "path": "dig",
5 | "description": "(Domain Internet Groper) Network administration tool for Domain Name System (DNS) name server interrogation.",
6 | "id": 1,
7 | "created": "2019-02-16 02:09:59",
8 | "modified": "2019-02-16 02:09:59"
9 | },
10 | {
11 | "name": "Nmap",
12 | "path": "nmap",
13 | "description": "(Network Mapper) Utility for network discovery and security auditing.",
14 | "id": 2,
15 | "created": "2019-02-16 02:10:29",
16 | "modified": "2019-02-16 02:10:29"
17 | },
18 | {
19 | "name": "Nikto",
20 | "path": "nikto",
21 | "description": "Signature-based web server scanner.",
22 | "id": 3,
23 | "created": "2019-02-16 02:10:59",
24 | "modified": "2019-02-16 02:10:59"
25 | },
26 | {
27 | "name": "SSLyze",
28 | "path": "sslyze",
29 | "description": "Fast and powerful SSL/TLS server scanning library.",
30 | "id": 4,
31 | "created": "2019-02-16 02:11:29",
32 | "modified": "2019-02-16 02:11:29"
33 | },
34 | {
35 | "name": "SQLmap",
36 | "path": "sqlmap --batch",
37 | "description": "Penetration testing tool that automates the process of detecting and exploiting SQL injection flaws.",
38 | "id": 5,
39 | "created": "2019-02-16 02:11:59",
40 | "modified": "2019-02-16 02:11:59"
41 | }
42 | ]
43 |
--------------------------------------------------------------------------------
/pwnedhub/fixtures/base/tools.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "Dig",
4 | "path": "dig",
5 | "description": "(Domain Internet Groper) Network administration tool for Domain Name System (DNS) name server interrogation.",
6 | "id": 1,
7 | "created": "2019-02-16 02:09:59",
8 | "modified": "2019-02-16 02:09:59"
9 | },
10 | {
11 | "name": "Nmap",
12 | "path": "nmap",
13 | "description": "(Network Mapper) Utility for network discovery and security auditing.",
14 | "id": 2,
15 | "created": "2019-02-16 02:10:29",
16 | "modified": "2019-02-16 02:10:29"
17 | },
18 | {
19 | "name": "Nikto",
20 | "path": "nikto",
21 | "description": "Signature-based web server scanner.",
22 | "id": 3,
23 | "created": "2019-02-16 02:10:59",
24 | "modified": "2019-02-16 02:10:59"
25 | },
26 | {
27 | "name": "SSLyze",
28 | "path": "sslyze",
29 | "description": "Fast and powerful SSL/TLS server scanning library.",
30 | "id": 4,
31 | "created": "2019-02-16 02:11:29",
32 | "modified": "2019-02-16 02:11:29"
33 | },
34 | {
35 | "name": "SQLmap",
36 | "path": "sqlmap --batch",
37 | "description": "Penetration testing tool that automates the process of detecting and exploiting SQL injection flaws.",
38 | "id": 5,
39 | "created": "2019-02-16 02:11:59",
40 | "modified": "2019-02-16 02:11:59"
41 | }
42 | ]
43 |
--------------------------------------------------------------------------------
/pwnedspa/templates/spa.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | PwnedHub 2.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/pwnedadmin/templates/emails.html:
--------------------------------------------------------------------------------
1 | {% extends 'layout.html' %}
2 | {% block title %}PwnedAdmin | Inbox{% endblock %}
3 | {% block content %}
4 |
5 |
6 | Empty
7 |
8 | {% if emails|length > 0 %}
9 | {% for email in emails %}
10 |
11 |
12 |
13 |
{{ email.subject }}
14 |
{{ email.created_as_string }}
15 |
16 |
From: {{ email.sender }}
17 |
To: {{ email.receiver }}
18 |
19 |
20 |
{{ email.body|safe }}
21 |
22 |
23 | {% endfor %}
24 | {% else %}
25 |
26 |
Inbox is empty.
27 |
28 | {% endif %}
29 |
30 |
37 | {% endblock %}
38 |
--------------------------------------------------------------------------------
/pwnedhub/templates/register.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
32 | {% endblock %}
33 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/views/scans.css:
--------------------------------------------------------------------------------
1 | .scans {
2 | padding: 1rem;
3 | }
4 |
5 | .scans .scan-form {
6 | display: flex;
7 | flex-direction: column;
8 | margin-bottom: 1rem;
9 | }
10 |
11 | .scans .scan-form .scan-form-args {
12 | position: relative;
13 | }
14 |
15 | .scans .scan-form .scan-form-args button {
16 | line-height: 1em;
17 | border: none;
18 | background-color: transparent;
19 | position: absolute;
20 | right: 0;
21 | top: 0;
22 | border: 0;
23 | }
24 |
25 | .scans .scans-table pre {
26 | white-space: pre-wrap;
27 | }
28 |
29 | .scans .scans-table .scans-table-row:hover {
30 | cursor: pointer;
31 | }
32 |
33 | /* desktop scans */
34 | @media all and (min-width: 960px) {
35 |
36 | /*flex-row flex-wrap flex-align-center */
37 | .scans .scan-form {
38 | flex-direction: row;
39 | flex-wrap: wrap;
40 | }
41 |
42 | .scans .scan-form select {
43 | margin-right: 1rem;
44 | }
45 |
46 | .scans .scan-form .scan-form-args {
47 | flex-grow: 1;
48 | }
49 |
50 | .scans .scan-form .scan-form-meta {
51 | flex-basis: 100%;
52 | }
53 |
54 | .scans .scan-form .scan-form-args button {
55 | display: none;
56 | }
57 |
58 | .scans .scans-table .scans-table-row .actions-cell {
59 | width: 100%;
60 | text-align: center;
61 | }
62 |
63 | }
64 |
65 | /* needed for scrolling overflow */
66 | .scans-modal {
67 | position: absolute;
68 | top: 3rem;
69 | left: 2rem;
70 | right: 2rem;
71 | bottom: 2rem;
72 | overflow: auto;
73 | }
74 |
75 | .scans-modal pre {
76 | margin: 0;
77 | }
78 |
--------------------------------------------------------------------------------
/pwnedapi/config.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 |
4 | class BaseConfig(object):
5 |
6 | # base
7 | DEBUG = False
8 | TESTING = False
9 | SECRET_KEY = os.getenv('SECRET_KEY', default='$ecretKey')
10 | # prevents connection pool exhaustion but disables interactive debugging
11 | PRESERVE_CONTEXT_ON_EXCEPTION = False
12 |
13 | # database
14 | DATABASE_HOST = os.environ.get('DATABASE_HOST', 'localhost')
15 | SQLALCHEMY_DATABASE_URI = f"mysql://pwnedhub:dbconnectpass@{DATABASE_HOST}/pwnedhub-test"
16 | SQLALCHEMY_BINDS = {
17 | 'admin': f"mysql://pwnedhub:dbconnectpass@{DATABASE_HOST}/pwnedhub-admin"
18 | }
19 | SQLALCHEMY_TRACK_MODIFICATIONS = False
20 |
21 | # csrf
22 | CSRF_TOKEN_NAME = 'X-Csrf-Token'
23 |
24 | # redis
25 | REDIS_URL = os.environ.get('REDIS_URL', 'redis://')
26 |
27 | # cors
28 | CORS_SUPPORTS_CREDENTIALS = True
29 | ALLOWED_ORIGINS = ['http://www.pwnedhub.com', 'http://test.pwnedhub.com']
30 |
31 | # oidc
32 | OAUTH_PROVIDERS = {
33 | 'google': {
34 | 'CLIENT_ID': '1098478339188-pvi39gpsvclmmucvu16vhrh0179sd100.apps.googleusercontent.com',
35 | 'CLIENT_SECRET': '5LFAbNk7rLa00PZOHceQfudp',
36 | 'DISCOVERY_DOC': 'https://accounts.google.com/.well-known/openid-configuration',
37 | },
38 | }
39 |
40 | # unused
41 | API_CONFIG_KEY_NAME = 'X-API-Key'
42 | API_CONFIG_KEY_VALUE = 'verysekrit'
43 |
44 |
45 | class Development(BaseConfig):
46 |
47 | DEBUG = True
48 |
49 |
50 | class Test(BaseConfig):
51 |
52 | DEBUG = True
53 | TESTING = True
54 |
55 |
56 | class Production(BaseConfig):
57 |
58 | pass
59 |
--------------------------------------------------------------------------------
/pwnedhub/templates/mobile.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | PwnedHub
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
PwnedHub
18 |
This application is not suited for use with mobile browsers. Download our mobile application for an enhanced mobile experience!
19 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/pwnedhub/templates/login.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
4 | {% if app_config('OIDC_ENABLE') %}
5 |
6 |
7 |
Login with your Google Account
8 |
9 |
No need to register or remember another pesky password! Just click the button below to register and/or sign-in and get started.
10 |
11 |
12 |
13 |
14 | {% else %}
15 | {% if app_config('SSO_ENABLE') %}
16 |
17 | {% else %}
18 |
19 | {% endif %}
20 | Please log in.
21 | Username:
22 |
23 | Password:
24 |
25 |
26 |
27 |
28 | Forget your password?
29 |
30 |
31 | {% endif %}
32 |
33 | {% endblock %}
34 |
--------------------------------------------------------------------------------
/pwnedhub/templates/admin_users.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 | {% if users|length > 0 %}
4 |
5 |
6 |
7 | created display name username role status action
8 |
9 |
10 | {% for user in users %}
11 |
12 | {{ user.created_as_string }}
13 | {{ user.name }}
14 | {{ user.username }}
15 | {{ user.role_as_string }}
16 | {{ user.status_as_string }}
17 |
18 | {% if user.is_admin %}
19 |
20 | {% else %}
21 |
22 | {% endif %}
23 | {% if user.is_enabled %}
24 |
25 | {% else %}
26 |
27 | {% endif %}
28 |
29 |
30 | {% endfor %}
31 |
32 |
33 |
34 | {% endif %}
35 | {% endblock %}
36 |
--------------------------------------------------------------------------------
/pwnedhub/routes/errors.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, request, render_template, render_template_string, jsonify
2 | from pwnedhub import db
3 | from urllib.parse import unquote
4 | import traceback
5 |
6 | blp = Blueprint('errors', __name__)
7 |
8 | CONTENT_TYPE = 'application/json'
9 |
10 | # error handling controllers
11 |
12 | @blp.app_errorhandler(400)
13 | def bad_request(e):
14 | if request.content_type == CONTENT_TYPE:
15 | return jsonify(status=400, message=e.description), 400
16 | else:
17 | return e
18 |
19 | @blp.app_errorhandler(403)
20 | def forbidden(e):
21 | if request.content_type == CONTENT_TYPE:
22 | return jsonify(status=403, message="Resource forbidden."), 403
23 | else:
24 | return e
25 |
26 | # affected by werkzeug v0.15.0
27 | # https://github.com/pallets/werkzeug/pull/1433
28 | @blp.app_errorhandler(404)
29 | def not_found(e):
30 | if request.content_type == CONTENT_TYPE:
31 | return jsonify(status=404, message="Resource not found."), 404
32 | else:
33 | template = '''{% extends "layout.html" %}
34 | {% block body %}
35 |
36 |
Oops! That page doesn't exist.
37 | '''+unquote(request.url)+'''
38 |
39 | {% endblock %}'''
40 | return render_template_string(template), 404
41 |
42 | @blp.app_errorhandler(405)
43 | def method_not_allowed(e):
44 | if request.content_type == CONTENT_TYPE:
45 | return jsonify(status=405, message="Method not allowed."), 405
46 | else:
47 | return e
48 |
49 | @blp.app_errorhandler(500)
50 | def internal_server_error(e):
51 | db.session.rollback()
52 | message = traceback.format_exc()
53 | if request.content_type == CONTENT_TYPE:
54 | return jsonify(status=500, message=message), 500
55 | else:
56 | return render_template('500.html', message=message), 500
57 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/components/google-login.js:
--------------------------------------------------------------------------------
1 | import { useAuthStore } from '../stores/auth-store.js';
2 | import { useAppStore } from '../stores/app-store.js';
3 |
4 | const { onMounted } = Vue;
5 |
6 | const template = `
7 |
8 | `;
9 |
10 | export default {
11 | name: 'GoogleLogin',
12 | template,
13 | setup (props, context) {
14 | const authStore = useAuthStore();
15 | const appStore = useAppStore();
16 |
17 | onMounted(() => {
18 | if (window.hasOwnProperty("gapi")) {
19 | gapi.load('auth2', () => {
20 | const auth2 = window.gapi.auth2.init({
21 | cookiepolicy: 'single_host_origin',
22 | });
23 | auth2.attachClickHandler(
24 | 'signinBtn',
25 | {},
26 | (googleUser) => {
27 | authStore.doLogin({id_token: googleUser.getAuthResponse().id_token});
28 | },
29 | (error) => {
30 | if (error.error === 'network_error') {
31 | appStore.createToast('OpenID Connect provider unreachable.');
32 | } else if (error.error !== 'popup_closed_by_user') {
33 | appStore.createToast('OpenID Connect error ({0}).'.format(error.error));
34 | };
35 | },
36 | );
37 | });
38 | } else {
39 | document.getElementById("signinBtn").addEventListener("click", (e) => {
40 | appStore.createToast('Google sign-in is not available. Check your internet connection and TLS configuration.');
41 | });
42 | }
43 | });
44 | },
45 | };
46 |
--------------------------------------------------------------------------------
/common/static/css/custom-utility.css:
--------------------------------------------------------------------------------
1 | /*! custom-utility.css v1.0.0 */
2 |
3 | .center {
4 | margin-left: auto;
5 | margin-right: auto;
6 | }
7 |
8 | .center-content {
9 | text-align: center;
10 | }
11 |
12 | .left-content {
13 | text-align: left;
14 | }
15 |
16 | .right-content {
17 | text-align: right;
18 | }
19 |
20 | .clear {
21 | clear: both;
22 | }
23 |
24 | .bolded {
25 | font-weight: bold;
26 | }
27 |
28 | .rounded {
29 | -webkit-border-radius: .5rem;
30 | -moz-border-radius: .5rem;
31 | border-radius: .5rem;
32 | }
33 |
34 | .circular {
35 | -webkit-border-radius: 50%;
36 | -moz-border-radius: 50%;
37 | border-radius: 50%;
38 | }
39 |
40 | .shaded {
41 | -webkit-box-shadow: 0 0 1rem rgba(0, 0, 0, 0.2);
42 | -moz-box-shadow: 0 0 1rem rgba(0, 0, 0, 0.2);
43 | box-shadow: 0 0 1rem rgba(0, 0, 0, 0.2);
44 | }
45 |
46 | .shaded-light {
47 | -webkit-box-shadow: 0.5rem 0.5rem 1.5rem rgba(0, 0, 0, 0.1);
48 | -moz-box-shadow: 0.5rem 0.5rem 1.5rem rgba(0, 0, 0, 0.1);
49 | box-shadow: 0.5rem 0.5rem 1.5rem rgba(0, 0, 0, 0.1);
50 | }
51 |
52 | .gutter-right {
53 | margin-right: 1rem;
54 | }
55 |
56 | .gutter-half-right {
57 | margin-right: 0.5;
58 | }
59 |
60 | .gutter-bottom {
61 | margin-bottom: 1rem;
62 | }
63 |
64 | .gutter-half-bottom {
65 | margin-bottom: 0.5;
66 | }
67 |
68 | .gutter-left {
69 | margin-left: 1rem;
70 | }
71 |
72 | .gutter-half-left {
73 | margin-left: 0.5;
74 | }
75 |
76 | /*
77 | The following:
78 | * Creates issues when the parent already has assigned margins.
79 | * Doesn't allow for a border when using border box.
80 |
81 | Both of these can be fixed by using a wrapper between the
82 | parent and child and making the wrapper the parent.
83 | */
84 |
85 | .gutter-parent {
86 | margin-left: -0.5rem;
87 | margin-right: -0.5rem;
88 | }
89 |
90 | .gutter-child {
91 | padding-left: 0.5rem;
92 | padding-right: 0.5rem;
93 | }
94 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/components/link-preview.js:
--------------------------------------------------------------------------------
1 | import { LinkPreview } from '../services/api.js';
2 |
3 | const { ref } = Vue;
4 |
5 | const template = `
6 |
11 | `;
12 |
13 | export default {
14 | name: 'LinkPreview',
15 | template,
16 | props: {
17 | message: Object,
18 | },
19 | setup (props) {
20 | const previews = ref([]);
21 |
22 | function parseUrls(message) {
23 | var pattern = /\w+:\/\/[^\s]+/gi;
24 | var matches = message.comment.match(pattern);
25 | return matches || [];
26 | };
27 |
28 | async function doPreview(message) {
29 | const urls = parseUrls(message);
30 | for (let url of urls) {
31 | // remove punctuation from URLs ending a sentence
32 | const sanitizedUrl = url.replace(/[!.?]+$/g, '');
33 | try {
34 | const json = await LinkPreview.create({url: sanitizedUrl});
35 | const preview = {
36 | url: json.url,
37 | values: []
38 | };
39 | const keys = ['site_name', 'title', 'description'];
40 | for (let key of keys) {
41 | if (json[key] !== null) {
42 | preview.values.push(json[key]);
43 | };
44 | };
45 | if (preview.values.length > 0) {
46 | previews.value.push(preview);
47 | };
48 | } catch (error) {};
49 | };
50 | };
51 |
52 | doPreview(props.message);
53 |
54 | return {
55 | previews,
56 | };
57 | },
58 | };
59 |
--------------------------------------------------------------------------------
/pwnedadmin/models.py:
--------------------------------------------------------------------------------
1 | from pwnedadmin import db
2 | from pwnedadmin.constants import RESTRICTED_USERS
3 | from pwnedadmin.utils import get_current_utc_time, get_local_from_utc
4 |
5 |
6 | class BaseModel(db.Model):
7 | __abstract__ = True
8 |
9 | def serialize_for_export(self):
10 | return {c.name: getattr(self, c.name) for c in self.__mapper__.columns}
11 |
12 |
13 | class Config(BaseModel):
14 | __tablename__ = 'configs'
15 | id = db.Column(db.Integer, primary_key=True)
16 | name = db.Column(db.String(255), nullable=False)
17 | description = db.Column(db.Text, nullable=False)
18 | type = db.Column(db.String(255), nullable=False)
19 | value = db.Column(db.Boolean, nullable=False)
20 |
21 | @staticmethod
22 | def get_by_name(name):
23 | return Config.query.filter_by(name=name).first()
24 |
25 | @staticmethod
26 | def get_value(name):
27 | return Config.query.filter_by(name=name).first().value
28 |
29 | def __repr__(self):
30 | return "".format(self.name)
31 |
32 |
33 | class Email(BaseModel):
34 | __tablename__ = 'emails'
35 | id = db.Column(db.Integer, primary_key=True)
36 | created = db.Column(db.DateTime, nullable=False, default=get_current_utc_time)
37 | sender = db.Column(db.String(255), nullable=False)
38 | receiver = db.Column(db.String(255), nullable=False)
39 | subject = db.Column(db.Text, nullable=False)
40 | body = db.Column(db.Text, nullable=False)
41 |
42 | @property
43 | def created_as_string(self):
44 | return get_local_from_utc(self.created).strftime("%Y-%m-%d %H:%M:%S")
45 |
46 | @staticmethod
47 | def get_unrestricted():
48 | return Email.query.filter(Email.receiver.notin_(RESTRICTED_USERS))
49 |
50 | @staticmethod
51 | def get_by_receiver(receiver):
52 | return Email.query.filter_by(receiver=receiver)
53 |
54 | def __repr__(self):
55 | return "".format(self.id)
56 |
--------------------------------------------------------------------------------
/pwnedhub/templates/profile.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
33 | {% endblock %}
34 |
--------------------------------------------------------------------------------
/pwnedhub/fixtures/base/mail.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "subject": "QA Results",
4 | "content": "Hey Cooper,\r\n\r\nI just finished checking out the latest push of PwnedHub. I encountered a couple of errors as I was testing and placed them in a paste for you to check out at https://pastebin.com/F2mzJJJ0.",
5 | "sender_id": 5,
6 | "receiver_id": 2,
7 | "read": 1,
8 | "id": 1,
9 | "created": "2019-02-14 14:12:17",
10 | "modified": "2019-02-14 14:12:17"
11 | },
12 | {
13 | "subject": "Training",
14 | "content": "Hey Cooper,\r\n\r\nHave you heard about that PWAPT class by Tim Tomes? Sounds like some top notch stuff. We should get him in here to do some training.",
15 | "sender_id": 4,
16 | "receiver_id": 2,
17 | "read": 1,
18 | "id": 2,
19 | "created": "2019-02-17 22:30:14",
20 | "modified": "2019-02-17 22:30:14"
21 | },
22 | {
23 | "subject": "RE: Training",
24 | "content": "Tanner,\r\n\r\nSounds good to me. I'll put a request in to Taylor.",
25 | "sender_id": 2,
26 | "receiver_id": 4,
27 | "read": 1,
28 | "id": 3,
29 | "created": "2019-02-17 22:45:38",
30 | "modified": "2019-02-17 22:45:38"
31 | },
32 | {
33 | "subject": "PWAPT Training",
34 | "content": "Taylor,\r\n\r\nTanner and some of the folks have been asking about some training. Specifically, the PWAPT class by Tim Tomes. You ever heard of it?",
35 | "sender_id": 2,
36 | "receiver_id": 3,
37 | "read": 1,
38 | "id": 4,
39 | "created": "2019-02-17 22:46:29",
40 | "modified": "2019-02-17 22:46:29"
41 | },
42 | {
43 | "subject": "RE: PWAPT Training",
44 | "content": "Cooper,\r\n\r\nYeah, I've heard about that guy. He's a hack!",
45 | "sender_id": 3,
46 | "receiver_id": 2,
47 | "read": 1,
48 | "id": 5,
49 | "created": "2019-02-17 22:48:12",
50 | "modified": "2019-02-17 22:48:12"
51 | }
52 | ]
53 |
--------------------------------------------------------------------------------
/pwnedhub/templates/artifacts.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
38 | {% endblock %}
39 |
--------------------------------------------------------------------------------
/pwnedhub/validators.py:
--------------------------------------------------------------------------------
1 | from flask import current_app
2 | from pwnedhub.models import Config
3 | from urllib.parse import urlparse
4 | import re
5 |
6 | def is_valid_command(cmd):
7 | pattern = r'[;&|]'
8 | if Config.get_value('OSCI_PROTECT'):
9 | pattern = r'[;&|<>`$(){}]'
10 | if re.search(pattern, cmd):
11 | return False
12 | return True
13 |
14 | def is_valid_filename(filename):
15 | # validate that the filename includes an allowed extension
16 | for ext in current_app.config['ALLOWED_EXTENSIONS']:
17 | if '.'+ext in filename:
18 | return True
19 | return False
20 |
21 | def is_valid_mimetype(mimetype):
22 | # validate that the mimetype is allowed
23 | if mimetype in current_app.config['ALLOWED_MIMETYPES']:
24 | return True
25 | return False
26 |
27 | # 6 or more characters
28 | PASSWORD_REGEX = r'.{6,}'
29 |
30 | def is_valid_password(password):
31 | if not re.match(r'^{}$'.format(PASSWORD_REGEX), password):
32 | return False
33 | return True
34 |
35 | EMAIL_REGEX = r'[^@]+@[a-zA-Z\d-]+(?:\.[a-zA-Z\d-]+)+'
36 |
37 | def is_valid_email(email):
38 | if not re.match(r'^{}$'.format(EMAIL_REGEX), email):
39 | return False
40 | return True
41 |
42 | def is_safe_url(url, origin):
43 | host = urlparse(origin).netloc
44 | proto = urlparse(origin).scheme
45 | # reject blank urls
46 | if not url:
47 | return False
48 | url = url.strip()
49 | url = url.replace('\\', '/')
50 | # simplify down to proto://, //, and /
51 | if url.startswith('///'):
52 | return False
53 | url_info = urlparse(url)
54 | # prevent browser manipulation via proto:///...
55 | if url_info.scheme and not url_info.netloc:
56 | return False
57 | # no proto for relative paths, or a matching proto for absolute paths
58 | if not url_info.scheme or url_info.scheme == proto:
59 | # no host for relative paths, or a matching host for absolute paths
60 | if not url_info.netloc or url_info.netloc == host:
61 | return True
62 | return False
63 |
--------------------------------------------------------------------------------
/pwnedapi/fixtures/base/users.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "email": "admin@pwnedhub.com",
4 | "name": "Administrator",
5 | "avatar": "/static/common/images/avatars/admin.png",
6 | "signature": "All your base are belong to me.",
7 | "role": 0,
8 | "status": 1,
9 | "id": 1,
10 | "created": "2019-02-16 01:51:59",
11 | "modified": "2019-02-16 01:51:59"
12 | },
13 | {
14 | "email": "cooper@pwnedhub.com",
15 | "name": "Cooper",
16 | "avatar": "/static/common/images/avatars/c-man.png",
17 | "signature": "Gamer, hacker, and basketball player. Energy sword FTW!",
18 | "role": 1,
19 | "status": 1,
20 | "id": 2,
21 | "created": "2019-02-16 04:46:27",
22 | "modified": "2019-02-16 04:46:27"
23 | },
24 | {
25 | "email": "taylor@pwnedhub.com",
26 | "name": "Taylor",
27 | "avatar": "/static/common/images/avatars/wolf.jpg",
28 | "signature": "Wolf in a past life. Nerd in the current. Johnny 5 is indeed alive.",
29 | "role": 1,
30 | "status": 1,
31 | "id": 3,
32 | "created": "2019-02-16 04:47:14",
33 | "modified": "2019-02-16 04:47:14"
34 | },
35 | {
36 | "email": "tanner@pwnedhub.com",
37 | "name": "Tanner",
38 | "avatar": "/static/common/images/avatars/kitty.jpg",
39 | "signature": "I might be small, cute, and cuddly, but remember... dynamite comes in small tightly wrapped packages that go boom.",
40 | "role": 1,
41 | "status": 1,
42 | "id": 4,
43 | "created": "2019-02-16 04:48:19",
44 | "modified": "2019-02-16 04:48:19"
45 | },
46 | {
47 | "email": "emilee@pwnedhub.com",
48 | "name": "Emilee",
49 | "avatar": "/static/common/images/avatars/bacon.png",
50 | "signature": "Late to the party, but still the life of the party.",
51 | "role": 1,
52 | "status": 1,
53 | "id": 5,
54 | "created": "2019-02-16 04:49:34",
55 | "modified": "2019-02-16 04:49:34"
56 | }
57 | ]
58 |
--------------------------------------------------------------------------------
/pwnedhub/utils.py:
--------------------------------------------------------------------------------
1 | from flask import session
2 | from datetime import datetime, timezone
3 | from hashlib import md5
4 | from itertools import cycle
5 | from lxml import etree
6 | from uuid import uuid4
7 | import base64
8 | import hashlib
9 | import os
10 | import random
11 | import requests
12 |
13 | def get_current_utc_time():
14 | return datetime.now(tz=timezone.utc)
15 |
16 | def get_local_from_utc(dtg):
17 | return dtg.replace(tzinfo=timezone.utc).astimezone(tz=None)
18 |
19 | def xor_encrypt(s, k):
20 | ciphertext = ''.join([ chr(ord(c)^ord(k)) for c,k in zip(s, cycle(k)) ])
21 | return base64.b64encode(ciphertext.encode()).decode()
22 |
23 | def xor_decrypt(c, k):
24 | ciphertext = base64.b64decode(c.encode()).decode()
25 | return ''.join([ chr(ord(c)^ord(k)) for c,k in zip(ciphertext, cycle(k)) ])
26 |
27 | def generate_state(length=1024):
28 | """Generates a random string of characters."""
29 | return hashlib.sha256(os.urandom(length)).hexdigest()
30 |
31 | def generate_nonce(length=8):
32 | """Generates a pseudorandom number."""
33 | return ''.join([str(random.randint(0, 9)) for i in range(length)])
34 |
35 | def generate_token():
36 | return str(uuid4())
37 |
38 | def generate_timestamp_token():
39 | return md5(str(int(get_current_utc_time().timestamp()*100)).encode()).hexdigest()
40 |
41 | def generate_csrf_token():
42 | session['csrf_token'] = generate_token()
43 | return session['csrf_token']
44 |
45 | def unfurl_url(url, headers={}):
46 | # request resource
47 | resp = requests.get(url, headers=headers)
48 | # parse meta tags
49 | html = etree.HTML(resp.content)
50 | data = {'url': url}
51 | for kw in ('site_name', 'title', 'description'):
52 | # standard
53 | prop = kw
54 | values = html.xpath('//meta[@property=\'{}\']/@content'.format(prop))
55 | data[kw] = ' '.join(values) or None
56 | # OpenGraph
57 | prop = 'og:{}'.format(kw)
58 | values = html.xpath('//meta[@property=\'{}\']/@content'.format(prop))
59 | data[kw] = ' '.join(values) or None
60 | return data
61 |
--------------------------------------------------------------------------------
/pwnedsso/models.py:
--------------------------------------------------------------------------------
1 | from flask import current_app
2 | from pwnedsso import db
3 | from pwnedsso.utils import get_current_utc_time, get_local_from_utc, xor_encrypt
4 | from secrets import token_urlsafe
5 |
6 |
7 | class BaseModel(db.Model):
8 | __abstract__ = True
9 | id = db.Column(db.Integer, primary_key=True)
10 | created = db.Column(db.DateTime, nullable=False, default=get_current_utc_time)
11 | modified = db.Column(db.DateTime, nullable=False, default=get_current_utc_time, onupdate=get_current_utc_time)
12 |
13 | @property
14 | def _name(self):
15 | return self.__class__.__name__.lower()
16 |
17 | @property
18 | def created_as_string(self):
19 | return get_local_from_utc(self.created).strftime("%Y-%m-%d %H:%M:%S")
20 |
21 | @property
22 | def modified_as_string(self):
23 | return get_local_from_utc(self.modified).strftime("%Y-%m-%d %H:%M:%S")
24 |
25 |
26 | class User(BaseModel):
27 | __tablename__ = 'users'
28 | username = db.Column(db.String(255), nullable=False, unique=True)
29 | email = db.Column(db.String(255), nullable=False, unique=True)
30 | name = db.Column(db.String(255), nullable=False)
31 | avatar = db.Column(db.Text)
32 | signature = db.Column(db.Text)
33 | password_hash = db.Column(db.String(255))
34 | question = db.Column(db.Integer, nullable=False, default=0)
35 | answer = db.Column(db.String(255), nullable=False, default=token_urlsafe(10))
36 | role = db.Column(db.Integer, nullable=False, default=1)
37 | status = db.Column(db.Integer, nullable=False, default=1)
38 |
39 | @property
40 | def is_enabled(self):
41 | if self.status == 1:
42 | return True
43 | return False
44 |
45 | def check_password(self, password):
46 | if self.password_hash == xor_encrypt(password, current_app.config['SECRET_KEY']):
47 | return True
48 | return False
49 |
50 | @staticmethod
51 | def get_by_username(username):
52 | return User.query.filter_by(username=username).first()
53 |
54 | def __repr__(self):
55 | return "".format(self.username)
56 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/views/login.js:
--------------------------------------------------------------------------------
1 | import GoogleLogin from '../components/google-login.js';
2 | import { useAuthStore } from '../stores/auth-store.js';
3 |
4 | const { ref } = Vue;
5 |
6 | const template = `
7 |
8 |
9 |
10 |
11 |
12 |
13 |
A collaborative space to conduct hosted security assessments.
14 |
15 |
16 |
27 |
28 |
29 |
Scan.
30 |
31 |
32 |
33 |
Find.
34 |
35 |
36 |
37 |
Win.
38 |
39 |
40 |
41 |
42 | `;
43 |
44 | export default {
45 | name: 'Login',
46 | template,
47 | components: {
48 | 'google-oidc': GoogleLogin,
49 | },
50 | setup () {
51 | const authStore = useAuthStore();
52 |
53 | const loginForm = ref({
54 | email: '',
55 | });
56 |
57 | function doFormLogin() {
58 | authStore.doLogin(loginForm.value);
59 | };
60 |
61 | return {
62 | loginForm,
63 | doFormLogin,
64 | };
65 | },
66 | };
67 |
--------------------------------------------------------------------------------
/pwnedhub/config.py:
--------------------------------------------------------------------------------
1 | from cachelib.file import FileSystemCache
2 | import os
3 |
4 |
5 | class BaseConfig(object):
6 |
7 | # base
8 | DEBUG = False
9 | TESTING = False
10 | SECRET_KEY = os.getenv('SECRET_KEY', default='$ecretKey')
11 | # prevents connection pool exhaustion but disables interactive debugging
12 | PRESERVE_CONTEXT_ON_EXCEPTION = False
13 | MESSAGES_PER_PAGE = 5
14 |
15 | # database
16 | DATABASE_HOST = os.environ.get('DATABASE_HOST', 'localhost')
17 | SQLALCHEMY_DATABASE_URI = f"mysql://pwnedhub:dbconnectpass@{DATABASE_HOST}/pwnedhub"
18 | SQLALCHEMY_BINDS = {
19 | 'admin': f"mysql://pwnedhub:dbconnectpass@{DATABASE_HOST}/pwnedhub-admin"
20 | }
21 | SQLALCHEMY_TRACK_MODIFICATIONS = False
22 |
23 | # redis
24 | REDIS_URL = os.environ.get('REDIS_URL', 'redis://')
25 |
26 | # file upload
27 | UPLOAD_FOLDER = '/tmp/artifacts'
28 | ALLOWED_EXTENSIONS = set(['txt', 'xml', 'jpg', 'png', 'gif', 'pdf'])
29 | ALLOWED_MIMETYPES = set(['text/plain', 'application/xml', 'image/jpeg', 'image/png', 'image/gif', 'application/pdf'])
30 |
31 | # session
32 | SESSION_TYPE = 'cachelib'
33 | SESSION_SERIALIZATION_FORMAT = 'json'
34 | SESSION_CACHELIB = FileSystemCache(threshold=500, cache_dir='/tmp/sessions')
35 | SESSION_COOKIE_NAME = 'session'
36 | SESSION_COOKIE_HTTPONLY = False
37 | SESSION_REFRESH_EACH_REQUEST = False
38 | PERMANENT_SESSION_LIFETIME = 3600 # 1 hour
39 |
40 | # oidc
41 | OAUTH_PROVIDERS = {
42 | 'google': {
43 | 'CLIENT_ID': '1098478339188-pvi39gpsvclmmucvu16vhrh0179sd100.apps.googleusercontent.com',
44 | 'CLIENT_SECRET': '5LFAbNk7rLa00PZOHceQfudp',
45 | 'DISCOVERY_DOC': 'https://accounts.google.com/.well-known/openid-configuration',
46 | },
47 | }
48 |
49 | # markdown
50 | MARKDOWN_EXTENSIONS = [
51 | 'markdown.extensions.tables',
52 | 'markdown.extensions.extra',
53 | 'markdown.extensions.attr_list',
54 | 'markdown.extensions.fenced_code',
55 | ]
56 |
57 |
58 | class Development(BaseConfig):
59 |
60 | DEBUG = True
61 |
62 |
63 | class Test(BaseConfig):
64 |
65 | DEBUG = True
66 | TESTING = True
67 |
68 |
69 | class Production(BaseConfig):
70 |
71 | pass
72 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/components/navigation.css:
--------------------------------------------------------------------------------
1 | .nav {
2 | background-color: #222;/*#fafafa;*/
3 | color: #fff;
4 | padding: 0 2rem;
5 | box-shadow: 0 0 1rem rgba(0, 0, 0, 0.5);
6 | }
7 |
8 | .nav a {
9 | color: inherit;
10 | text-decoration: inherit;
11 | }
12 |
13 | .nav a:hover {
14 | color: inherit;
15 | }
16 |
17 | .nav ul {
18 | list-style-type: none;
19 | }
20 |
21 | .nav ul,
22 | .nav li {
23 | margin: 0;
24 | padding: 0;
25 | }
26 |
27 | .nav .menu {
28 | display: flex;
29 | flex-wrap: wrap;
30 | align-items: center;
31 | }
32 |
33 | .nav .menu .brand {
34 | flex-grow: 1;
35 | }
36 |
37 | .nav .menu .brand img {
38 | height: 5rem;
39 | display: block;
40 | }
41 |
42 | .nav .menu li.item {
43 | width: 100%;
44 | text-align: right;
45 | display: none;
46 | }
47 |
48 | .nav .menu li.item a,
49 | .nav .menu li.item span {
50 | display: block;
51 | cursor: pointer;
52 | font-size: 1.25rem;
53 | letter-spacing: 0.1rem;
54 | text-transform: uppercase;
55 | font-weight: bold;
56 | padding: 1rem 0;
57 | }
58 |
59 | .nav .menu li.avatar img {
60 | display: block;
61 | object-fit: cover;
62 | width: 4rem;
63 | height: 4rem;
64 | margin: 0 auto;
65 | margin-right: 0;
66 | }
67 |
68 | .nav .active li.item {
69 | display: block;
70 | }
71 |
72 | /* desktop navigation */
73 | @media all and (min-width: 960px) {
74 |
75 | .nav .menu {
76 | flex-wrap: nowrap;
77 | justify-content: center;
78 | }
79 |
80 | .nav .menu .brand {
81 | flex: 1;
82 | }
83 |
84 | .nav .menu li.item {
85 | display: block;
86 | width: auto;
87 | }
88 |
89 | .nav .menu li.item:hover {
90 | background: red;
91 | }
92 |
93 | .nav .menu li.item a,
94 | .nav .menu li.item span {
95 | padding: 1rem;
96 | }
97 |
98 | .nav .menu li.avatar {
99 | order: 1;
100 | }
101 |
102 | .nav .menu li.avatar:hover {
103 | background: inherit;
104 | }
105 |
106 | .nav .menu li.avatar a {
107 | padding: 0 1rem;
108 | }
109 |
110 | .nav .menu li.toggle {
111 | display: none;
112 | }
113 |
114 | }
115 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | PwnedHub is a vulnerable application designed exclusively for [PractiSec training courses](https://www.practisec.com/training/). PwnedHub contains intentional vulnerability and should never be exposed to the open Internet. This software is NOT Open Source in a traditional sense. See the `LICENSE.txt` file for more information.
4 |
5 | ## Requirements
6 |
7 | * Docker
8 |
9 | ## Installation and Usage
10 |
11 | 1. Install Docker Desktop.
12 | 2. Clone the PwnedHub repository.
13 |
14 | ```
15 | $ git clone https://github.com/lanmaster53/pwnedhub.git
16 | ```
17 |
18 | 3. Change into the PwnedHub directory.
19 |
20 | ```
21 | $ cd pwnedhub
22 | ```
23 |
24 | 4. Build the PwnedHub Docker images.
25 |
26 | ```
27 | docker compose build
28 | ```
29 |
30 | 5. Launch the PwnedHub environment using Docker Compose.
31 |
32 | ```
33 | docker compose up
34 | ```
35 |
36 | * To launch as a daemon (no terminal logging), add the `-d` switch.
37 |
38 | 6. Modify the hosts file to create the following records:
39 |
40 | ```
41 | 127.0.0.1 www.pwnedhub.com
42 | 127.0.0.1 sso.pwnedhub.com
43 | 127.0.0.1 test.pwnedhub.com
44 | 127.0.0.1 api.pwnedhub.com
45 | 127.0.0.1 admin.pwnedhub.com
46 | ```
47 |
48 | 7. Access the various target applications and interfaces:
49 | * http://www.pwnedhub.com
50 | * http://test.pwnedhub.com
51 | * http://api.pwnedhub.com/static/swaggerui/index.html
52 | * http://api.pwnedhub.com/static/postman.json (for use with [Postman](https://www.postman.com/))
53 | 8. When done using PwnedHub, shut down the Docker environment with the following command:
54 |
55 | ```
56 | docker compose down
57 | ```
58 |
59 | ## Information
60 |
61 | The PwnedHub environment includes several resources that are not targets.
62 |
63 | * http://admin.pwnedhub.com/inbox/ - A webmail interface for receiving email from out-of-band systems. PwnedHub does not send email to external mail services, so when an application sends an email, this is where the user will receive it.
64 | * http://admin.pwnedhub.com/config/ - A configuration interface for enabling/disabling security controls and features. Modifying these settings change how the target applications behave.
65 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/helpers/fetch-wrapper.js:
--------------------------------------------------------------------------------
1 | import { useAuthStore } from '../stores/auth-store.js';
2 | import { router } from '../router.js';
3 |
4 | export const fetchWrapper = {
5 | get: request('GET'),
6 | post: request('POST'),
7 | put: request('PUT'),
8 | patch: request('PATCH'),
9 | delete: request('DELETE'),
10 | };
11 |
12 | function request(method) {
13 | return async (url, body) => {
14 | const options = {
15 | method: method,
16 | credentials: 'include',
17 | headers: {},
18 | };
19 | const authStore = useAuthStore();
20 | if (authStore.accessToken) {
21 | options.headers['Authorization'] = `Bearer ${authStore.accessToken}`;
22 | };
23 | if (authStore.csrfToken) {
24 | options.headers[CSRF_TOKEN_NAME] = authStore.csrfToken;
25 | };
26 | if (body) {
27 | options.headers['Content-Type'] = 'application/json';
28 | options.body = JSON.stringify(body);
29 | };
30 | const response = await fetch(url, options);
31 | return handleErrors(response);
32 | };
33 | };
34 |
35 | async function handleErrors(response) {
36 | // handle empty responses
37 | if (response.status === 204) {
38 | return {};
39 | // handle good responses
40 | } else if (response.ok) {
41 | return await response.json();
42 | // route unauthenticated users to login
43 | } else if (response.status === 401) {
44 | const authStore = useAuthStore();
45 | authStore.unsetAuthInfo();
46 | router.push('login');
47 | throw new Error('Unauthenticated.');
48 | // treat everything else like an error
49 | } else {
50 | const json = await response.json();
51 | // handle Passwordless
52 | if (json.error === 'code_required') {
53 | const authStore = useAuthStore();
54 | authStore.setCodeToken(json.code_token);
55 | router.push({ name: 'passwordless', params: { nextUrl: router.currentRoute.value.params.nextUrl } });
56 | throw new Error('Code required for Passwordless Authentication.');
57 | // raise an error to trigger the catch block
58 | } else {
59 | throw new Error(json.message || response.statusText);
60 | };
61 | };
62 | };
63 |
--------------------------------------------------------------------------------
/pwnedhub/static/js/pwnedhub.js:
--------------------------------------------------------------------------------
1 | // add the format method to the String object to add string formatting behavior
2 | String.prototype.format = function() {
3 | a = this;
4 | for (k in arguments) {
5 | a = a.replace("{" + k + "}", arguments[k])
6 | }
7 | return a
8 | }
9 |
10 | function showFlash(msg) {
11 | var div = document.createElement("div");
12 | div.className = "center-content rounded shaded";
13 | div.innerHTML = msg;
14 | var id = "flash-" + Date.now();
15 | div.id = id
16 | var flash = document.getElementById("flash");
17 | flash.appendChild(div);
18 | setTimeout(function() {
19 | flash.removeChild(document.getElementById(id));
20 | }, 5000);
21 | }
22 |
23 | function cleanRedirect(event, url) {
24 | event.preventDefault();
25 | event.stopPropagation();
26 | window.location = url;
27 | }
28 |
29 | function confirmRedirect(event, url) {
30 | if (confirm("Are you sure?")) {
31 | cleanRedirect(event, url);
32 | }
33 | }
34 |
35 | function cleanSubmit(event, form) {
36 | event.preventDefault();
37 | event.stopPropagation();
38 | form.submit();
39 | }
40 |
41 | function confirmSubmit(event, form) {
42 | if (confirm("Are you sure?")) {
43 | cleanSubmit(event, form);
44 | }
45 | }
46 |
47 | function toggleShow() {
48 | var el = document.getElementById("password");
49 | if (el.type =="password") {
50 | el.type = "text";
51 | } else {
52 | el.type = "password";
53 | }
54 | }
55 |
56 | window.addEventListener("load", function() {
57 | // flash on load if needed
58 | var queryString = window.location.search;
59 | var urlParams = new URLSearchParams(queryString);
60 | var error = urlParams.get('error')
61 | if (error !== null) {
62 | showFlash(error);
63 | }
64 |
65 | // event handler for tab navigation
66 | var tabs = document.querySelectorAll(".tabs > input[type='radio']")
67 | var panes = document.querySelectorAll(".tab-content > div")
68 | tabs.forEach(function(tab) {
69 | tab.addEventListener("click", function(evt) {
70 | panes.forEach(function(pane) {
71 | pane.classList.remove("active");
72 | });
73 | document.querySelector(evt.target.value).classList.add("active");
74 | });
75 | });
76 | });
77 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/services/api.js:
--------------------------------------------------------------------------------
1 | import { fetchWrapper } from '../helpers/fetch-wrapper.js';
2 |
3 | const AccessToken = {
4 | create(data) {
5 | return fetchWrapper.post(`${API_BASE_URL}/access-token`, data);
6 | },
7 | delete() {
8 | return fetchWrapper.delete(`${API_BASE_URL}/access-token`);
9 | },
10 | };
11 |
12 | const User = {
13 | all() {
14 | return fetchWrapper.get(`${API_BASE_URL}/users`);
15 | },
16 | get(uid) {
17 | return fetchWrapper.get(`${API_BASE_URL}/users/${uid}`);
18 | },
19 | create(data) {
20 | return fetchWrapper.post(`${API_BASE_URL}/users`, data);
21 | },
22 | update(uid, data) {
23 | return fetchWrapper.patch(`${API_BASE_URL}/users/${uid}`, data);
24 | },
25 | };
26 |
27 | const AdminUser = {
28 | update(uid, data) {
29 | return fetchWrapper.patch(`${API_BASE_URL}/admin/users/${uid}`, data);
30 | },
31 | };
32 |
33 | const Message = {
34 | all(rid, query) {
35 | return fetchWrapper.get(`${API_BASE_URL}/rooms/${rid}/messages${query}`);
36 | },
37 | };
38 |
39 | const LinkPreview = {
40 | create(data) {
41 | return fetchWrapper.post(`${API_BASE_URL}/unfurl`, data);
42 | },
43 | };
44 |
45 | const Note = {
46 | all() {
47 | return fetchWrapper.get(`${API_BASE_URL}/notes`);
48 | },
49 | replace(data) {
50 | return fetchWrapper.put(`${API_BASE_URL}/notes`, data);
51 | },
52 | };
53 |
54 | const Tool = {
55 | all() {
56 | return fetchWrapper.get(`${API_BASE_URL}/tools`);
57 | },
58 | create(data) {
59 | return fetchWrapper.post(`${API_BASE_URL}/tools`, data);
60 | },
61 | delete(tid) {
62 | return fetchWrapper.delete(`${API_BASE_URL}/tools/${tid}`);
63 | },
64 | };
65 |
66 | const Scan = {
67 | all() {
68 | return fetchWrapper.get(`${API_BASE_URL}/scans`);
69 | },
70 | get(sid) {
71 | return fetchWrapper.get(`${API_BASE_URL}/scans/${sid}/results`);
72 | },
73 | create(data) {
74 | return fetchWrapper.post(`${API_BASE_URL}/scans`, data);
75 | },
76 | delete(sid) {
77 | return fetchWrapper.delete(`${API_BASE_URL}/scans/${sid}`);
78 | },
79 | };
80 |
81 |
82 | export {
83 | AccessToken,
84 | User,
85 | AdminUser,
86 | Message,
87 | LinkPreview,
88 | Note,
89 | Tool,
90 | Scan,
91 | };
92 |
--------------------------------------------------------------------------------
/common/static/css/custom-flex.css:
--------------------------------------------------------------------------------
1 | /*! custom-flex.css v1.0.0 */
2 |
3 | html, body, #app {
4 | height: 100%;
5 | }
6 |
7 | /*
8 | flex item default = flex: 0 1 auto
9 | */
10 |
11 | /* flex classes */
12 |
13 | .flex-row {
14 | display: flex;
15 | flex-direction: row;
16 | }
17 |
18 | .flex-row-reverse {
19 | display: flex;
20 | flex-direction: row-reverse;
21 | }
22 |
23 | .flex-column {
24 | display: flex;
25 | flex-direction: column;
26 | }
27 |
28 | .flex-column-reverse {
29 | display: flex;
30 | flex-direction: column-reverse;
31 | }
32 |
33 | .flex-no-basis {
34 | flex-basis: 0;
35 | }
36 |
37 | .flex-no-shrink {
38 | flex-shrink: 0;
39 | }
40 |
41 | .flex-grow {
42 | flex-grow: 1;
43 | }
44 |
45 | .flex-shrink {
46 | flex-shrink: 1;
47 | }
48 |
49 | .flex-both {
50 | flex-grow: 1;
51 | flex-shrink: 1;
52 | }
53 |
54 | .flex-align-center {
55 | align-items: center;
56 | }
57 |
58 | .flex-justify-center {
59 | justify-content: center;
60 | }
61 |
62 | .flex-justify-end {
63 | justify-content: flex-end;
64 | }
65 |
66 | .flex-justify-space-between {
67 | justify-content: space-between;
68 | }
69 |
70 | .flex-justify-space-around {
71 | justify-content: space-around;
72 | }
73 |
74 | .flex-justify-space-evenly {
75 | justify-content: space-evenly;
76 | }
77 |
78 | .flex-wrap {
79 | flex-wrap: wrap;
80 | }
81 |
82 | .flex-break {
83 | flex-basis: 100%;
84 | height: 0;
85 | }
86 |
87 | /* grid 1/10/1 */
88 |
89 | .flex-width-10 {
90 | max-width: 83.33333333%;
91 | }
92 |
93 | .flex-basis-10 {
94 | flex-basis: 83.33333333%;
95 | }
96 |
97 | .flex-offset-1 {
98 | margin-left: 8.33333333%;
99 | }
100 |
101 | /* grid 2/8/8 */
102 |
103 | .flex-width-8 {
104 | max-width: 66.66666667%;
105 | }
106 |
107 | .flex-basis-8 {
108 | flex-basis: 66.66666667%;
109 | }
110 |
111 | .flex-offset-2 {
112 | margin-left: 16.66666667%;
113 | }
114 |
115 | /* grid 3/6/4 */
116 |
117 | .flex-width-6 {
118 | max-width: 50%;
119 | }
120 |
121 | .flex-basis-6 {
122 | flex-basis: 50%;
123 | }
124 |
125 | .flex-offset-3 {
126 | margin-left: 25%;
127 | }
128 |
129 | /* grid 4/4/4 */
130 |
131 | .flex-width-4 {
132 | max-width: 33.33333333%;
133 | }
134 |
135 | .flex-basis-4 {
136 | flex-basis: 33.33333333%;
137 | }
138 |
139 | .flex-offset-4 {
140 | margin-left: 33.33333333%;
141 | }
142 |
--------------------------------------------------------------------------------
/pwnedadmin/fixtures/base/configs.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 1,
4 | "name": "CSRF_PROTECT",
5 | "description": "Profile CSRF Protection (PwnedHub)",
6 | "type": "security control",
7 | "value": true
8 | },
9 | {
10 | "id": 2,
11 | "name": "OSCI_PROTECT",
12 | "description": "Tools OSCI Protection (PwnedHub)",
13 | "type": "security control",
14 | "value": false
15 | },
16 | {
17 | "id": 3,
18 | "name": "SQLI_PROTECT",
19 | "description": "Login SQLi Protection (PwnedHub)",
20 | "type": "security control",
21 | "value": false
22 | },
23 | {
24 | "id": 4,
25 | "name": "CSP_PROTECT",
26 | "description": "Content Security Policy (PwnedHub)",
27 | "type": "security control",
28 | "value": false
29 | },
30 | {
31 | "id": 5,
32 | "name": "CORS_RESTRICT",
33 | "description": "Restricted CORS (PwnedAPI)",
34 | "type": "security control",
35 | "value": true
36 | },
37 | {
38 | "id": 6,
39 | "name": "JWT_VERIFY",
40 | "description": "Verify JWT Signatures (PwnedAPI)",
41 | "type": "security control",
42 | "value": true
43 | },
44 | {
45 | "id": 7,
46 | "name": "JWT_ENCRYPT",
47 | "description": "Encrypt JWTs (PwnedAPI)",
48 | "type": "security control",
49 | "value": false
50 | },
51 | {
52 | "id": 8,
53 | "name": "BEARER_AUTH_ENABLE",
54 | "description": "Bearer Token Authentication (PwnedAPI)",
55 | "type": "feature",
56 | "value": true
57 | },
58 | {
59 | "id": 9,
60 | "name": "OIDC_ENABLE",
61 | "description": "OpenID Connect Authentication (PwnedHub)",
62 | "type": "feature",
63 | "value": false
64 | },
65 | {
66 | "id": 10,
67 | "name": "SSO_ENABLE",
68 | "description": "SSO Authentication (PwnedHub)",
69 | "type": "feature",
70 | "value": false
71 | },
72 | {
73 | "id": 11,
74 | "name": "OOB_RESET_ENABLE",
75 | "description": "Out-of-Band Password Reset (PwnedHub)",
76 | "type": "feature",
77 | "value": false
78 | },
79 | {
80 | "id": 12,
81 | "name": "CTF_MODE",
82 | "description": "CTF Mode (Warning: Disables this interface!)",
83 | "type": "feature",
84 | "value": false
85 | }
86 | ]
87 |
--------------------------------------------------------------------------------
/pwnedapi/decorators.py:
--------------------------------------------------------------------------------
1 | from flask import g, request, current_app, abort
2 | from pwnedapi.constants import ROLES
3 | from pwnedapi.models import Config
4 | from pwnedapi.utils import CsrfToken, ParamValidator
5 | from functools import wraps
6 | import base64
7 | import jsonpickle
8 |
9 | def token_auth_required(func):
10 | @wraps(func)
11 | def wrapped(*args, **kwargs):
12 | if g.user:
13 | return func(*args, **kwargs)
14 | abort(401)
15 | return wrapped
16 |
17 | def key_auth_required(func):
18 | @wraps(func)
19 | def wrapped(*args, **kwargs):
20 | key = request.headers.get(current_app.config['API_CONFIG_KEY_NAME'])
21 | if key == current_app.config['API_CONFIG_KEY_VALUE']:
22 | return func(*args, **kwargs)
23 | abort(401)
24 | return wrapped
25 |
26 | def roles_required(*roles):
27 | def wrapper(func):
28 | @wraps(func)
29 | def wrapped(*args, **kwargs):
30 | if ROLES[g.user.role] not in roles:
31 | return abort(403)
32 | return func(*args, **kwargs)
33 | return wrapped
34 | return wrapper
35 |
36 | def validate_json(params):
37 | def wrapper(func):
38 | @wraps(func)
39 | def wrapped(*args, **kwargs):
40 | input_dict = getattr(request, 'json', {})
41 | v = ParamValidator(input_dict, params)
42 | v.validate()
43 | if not v.passed:
44 | abort(400, v.reason)
45 | return func(*args, **kwargs)
46 | return wrapped
47 | return wrapper
48 |
49 | def csrf_protect(func):
50 | @wraps(func)
51 | def wrapped(*args, **kwargs):
52 | if not Config.get_value('BEARER_AUTH_ENABLE'):
53 | # no Bearer token means cookies (default) are used and CSRF is an issue
54 | csrf_token = request.headers.get(current_app.config['CSRF_TOKEN_NAME'])
55 | try:
56 | untrusted_csrf_obj = jsonpickle.decode(base64.b64decode(csrf_token))
57 | untrusted_csrf_obj.sign(current_app.config['SECRET_KEY'])
58 | trusted_csrf_obj = CsrfToken(g.user.id, untrusted_csrf_obj.ts)
59 | trusted_csrf_obj.sign(current_app.config['SECRET_KEY'])
60 | except:
61 | untrusted_csrf_obj = None
62 | if not untrusted_csrf_obj or trusted_csrf_obj.sig != untrusted_csrf_obj.sig:
63 | abort(400, 'CSRF detected.')
64 | return func(*args, **kwargs)
65 | return wrapped
66 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/views/notes.js:
--------------------------------------------------------------------------------
1 | import { useAppStore } from '../stores/app-store.js';
2 | import { Note } from '../services/api.js';
3 | import { marked } from '../libs/marked.js'; // esm build
4 |
5 | const { ref } = Vue;
6 |
7 | const template = `
8 |
22 | `;
23 |
24 | export default {
25 | name: 'Notes',
26 | template,
27 | setup () {
28 | const appStore = useAppStore();
29 |
30 | const note = ref('');
31 | const markdown = ref('');
32 | const activePane = ref('view');
33 |
34 | async function getNote() {
35 | try {
36 | const json = await Note.all();
37 | note.value = json.content;
38 | renderNote();
39 | } catch (error) {
40 | appStore.createToast(error.message);
41 | };
42 | };
43 |
44 | function renderNote() {
45 | if (note.value != null) {
46 | markdown.value = marked.parse(note.value);
47 | };
48 | };
49 |
50 | async function updateNote() {
51 | try {
52 | await Note.replace({content: note.value});
53 | } catch (error) {
54 | appStore.createToast(error.message);
55 | };
56 | };
57 |
58 | function isActive(tab) {
59 | return activePane.value === tab;
60 | };
61 |
62 | function setActive(tab) {
63 | activePane.value = tab;
64 | };
65 |
66 | getNote();
67 |
68 | return {
69 | note,
70 | markdown,
71 | isActive,
72 | setActive,
73 | renderNote,
74 | updateNote,
75 | };
76 | },
77 | };
78 |
--------------------------------------------------------------------------------
/common/static/images/hub.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 |
10 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/pwnedadmin/__init__.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, Blueprint
2 | from pwnedadmin.extensions import db
3 | import click
4 | import os
5 |
6 | def create_app():
7 |
8 | # create the Flask application
9 | app = Flask(__name__, static_url_path='/static')
10 |
11 | # configure the Flask application
12 | config_class = os.getenv('CONFIG', default='Development')
13 | app.config.from_object('pwnedadmin.config.{}'.format(config_class.title()))
14 |
15 | db.init_app(app)
16 |
17 | # custom jinja global for accessing dynamic configuration values
18 | from pwnedadmin.models import Config
19 | app.jinja_env.globals['app_config'] = Config.get_value
20 |
21 | # misc jinja configuration variables
22 | app.jinja_env.trim_blocks = True
23 | app.jinja_env.lstrip_blocks = True
24 |
25 | StaticBlueprint = Blueprint('common', __name__, static_url_path='/static/common', static_folder='../common/static')
26 | app.register_blueprint(StaticBlueprint)
27 |
28 | from pwnedadmin.routes.config import blp as ConfigBlurprint
29 | from pwnedadmin.routes.email import blp as EmailBlurprint
30 | app.register_blueprint(ConfigBlurprint)
31 | app.register_blueprint(EmailBlurprint)
32 |
33 | @app.cli.command('init')
34 | @click.argument('dataset')
35 | def init_data(dataset):
36 | from flask import current_app
37 | from pwnedadmin import models
38 | import json
39 | import os
40 | db.create_all(bind_key=None)
41 | for cls in models.BaseModel.__subclasses__():
42 | fixture_path = os.path.join(current_app.root_path, 'fixtures', dataset, f"{cls.__table__.name}.json")
43 | if os.path.exists(fixture_path):
44 | print(f"Processing {fixture_path}.")
45 | with open(fixture_path) as fp:
46 | for row in json.load(fp):
47 | db.session.add(cls(**row))
48 | db.session.commit()
49 | print('Database initialized.')
50 |
51 | @app.cli.command('export')
52 | def export_data():
53 | from pwnedadmin.models import BaseModel
54 | import json
55 | for cls in BaseModel.__subclasses__():
56 | objs = [obj.serialize_for_export() for obj in cls.query.all()]
57 | if objs:
58 | print(f"\n***** {cls.__table__.name}.json *****\n")
59 | print(json.dumps(objs, indent=4, default=str))
60 | print('Database exported.')
61 |
62 | @app.cli.command('purge')
63 | def purge_data():
64 | db.drop_all(bind_key=None)
65 | db.session.commit()
66 | print('Database purged.')
67 |
68 | return app
69 |
--------------------------------------------------------------------------------
/proxy/nginx.conf:
--------------------------------------------------------------------------------
1 | worker_processes 1;
2 |
3 | events {
4 | worker_connections 1024;
5 | }
6 |
7 | http {
8 |
9 | include mime.types;
10 | default_type application/octet-stream;
11 |
12 | sendfile on;
13 |
14 | keepalive_timeout 65;
15 |
16 | server {
17 | listen 80;
18 | server_name localhost;
19 |
20 | location / {
21 | root html;
22 | index index.html index.htm;
23 | }
24 |
25 | error_page 500 502 503 504 /50x.html;
26 | location = /50x.html {
27 | root html;
28 | }
29 | }
30 |
31 | upstream app_server {
32 | server app:80 fail_timeout=0;
33 | }
34 |
35 | server {
36 | listen 80;
37 | server_name www.pwnedhub.com;
38 |
39 | location / {
40 | include proxy_params;
41 | proxy_pass http://app_server;
42 | }
43 | }
44 |
45 | upstream sso_server {
46 | server sso:80 fail_timeout=0;
47 | }
48 |
49 | server {
50 | listen 80;
51 | server_name sso.pwnedhub.com;
52 |
53 | location / {
54 | include proxy_params;
55 | proxy_pass http://sso_server;
56 | }
57 | }
58 |
59 | upstream spa_server {
60 | server spa:80 fail_timeout=0;
61 | }
62 |
63 | server {
64 | listen 80;
65 | server_name test.pwnedhub.com;
66 |
67 | location / {
68 | include proxy_params;
69 | proxy_pass http://spa_server;
70 | }
71 | }
72 |
73 | upstream api_server {
74 | server api:80 fail_timeout=0;
75 | }
76 |
77 | server {
78 | listen 80;
79 | server_name api.pwnedhub.com;
80 |
81 | location / {
82 | include proxy_params;
83 | proxy_pass http://api_server;
84 | }
85 |
86 | location /socket.io {
87 | include proxy_params;
88 | proxy_http_version 1.1;
89 | proxy_buffering off;
90 | proxy_set_header Upgrade $http_upgrade;
91 | proxy_set_header Connection "Upgrade";
92 | proxy_pass http://api_server/socket.io;
93 | }
94 | }
95 |
96 | upstream admin_server {
97 | server admin:80 fail_timeout=0;
98 | }
99 |
100 | server {
101 | listen 80;
102 | server_name admin.pwnedhub.com;
103 |
104 | location / {
105 | include proxy_params;
106 | proxy_pass http://admin_server;
107 | }
108 | }
109 |
110 | }
111 |
--------------------------------------------------------------------------------
/pwnedhub/fixtures/base/messages.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "comment": "Hey, did you guys hear that we're having a security assessment this week?",
4 | "user_id": 3,
5 | "id": 1,
6 | "created": "2019-02-18 04:55:11",
7 | "modified": "2019-02-18 04:55:11"
8 | },
9 | {
10 | "comment": "No.",
11 | "user_id": 4,
12 | "id": 2,
13 | "created": "2019-02-18 04:55:19",
14 | "modified": "2019-02-18 04:55:19"
15 | },
16 | {
17 | "comment": "First I'm hearing of it. I hope they don't find any bugs. This is my \"get rich quick\" scheme.",
18 | "user_id": 2,
19 | "id": 3,
20 | "created": "2019-02-18 04:56:09",
21 | "modified": "2019-02-18 04:56:09"
22 | },
23 | {
24 | "comment": "Heh. Me too. So looking forward to afternoons on my yacht. :-)",
25 | "user_id": 3,
26 | "id": 4,
27 | "created": "2019-02-18 04:57:02",
28 | "modified": "2019-02-18 04:57:02"
29 | },
30 | {
31 | "comment": "Wait... didn't we go live this week?",
32 | "user_id": 4,
33 | "id": 5,
34 | "created": "2019-02-18 04:57:08",
35 | "modified": "2019-02-18 04:57:08"
36 | },
37 | {
38 | "comment": "Well, as the most interesting man in the world says, \"I don't always get apps tested, but when I do, I get it done in prod.\"",
39 | "user_id": 2,
40 | "id": 6,
41 | "created": "2019-02-18 04:57:20",
42 | "modified": "2019-02-18 04:57:20"
43 | },
44 | {
45 | "comment": "LOL! So, yeah, did any of you guys fix those things I found during QA testing? I sent Cooper a link to them in a private message.",
46 | "user_id": 5,
47 | "id": 7,
48 | "created": "2019-02-18 04:57:32",
49 | "modified": "2019-02-18 04:57:32"
50 | },
51 | {
52 | "comment": "No.",
53 | "user_id": 4,
54 | "id": 8,
55 | "created": "2019-02-18 04:57:37",
56 | "modified": "2019-02-18 04:57:37"
57 | },
58 | {
59 | "comment": "My bad.",
60 | "user_id": 2,
61 | "id": 9,
62 | "created": "2019-02-18 04:57:41",
63 | "modified": "2019-02-18 04:57:41"
64 | },
65 | {
66 | "comment": "Uh oh...",
67 | "user_id": 3,
68 | "id": 10,
69 | "created": "2019-02-18 04:57:46",
70 | "modified": "2019-02-18 04:57:46"
71 | },
72 | {
73 | "comment": "Wow. We're totally going to end up on https://haveibeenpwned.com/PwnedWebsites.",
74 | "user_id": 5,
75 | "id": 11,
76 | "created": "2019-02-18 04:59:31",
77 | "modified": "2019-02-18 04:59:31"
78 | }
79 | ]
80 |
--------------------------------------------------------------------------------
/pwnedhub/fixtures/base/users.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "username": "admin",
4 | "email": "admin@pwnedhub.com",
5 | "name": "Administrator",
6 | "avatar": "/static/common/images/avatars/admin.png",
7 | "signature": "All your base are belong to me.",
8 | "password_hash": "QQsEEwIRJgAXUBY=",
9 | "question": 1,
10 | "answer": "Diego",
11 | "role": 0,
12 | "status": 1,
13 | "id": 1,
14 | "created": "2019-02-16 01:51:59",
15 | "modified": "2019-02-16 01:51:59"
16 | },
17 | {
18 | "username": "Cooperman",
19 | "email": "cooper@pwnedhub.com",
20 | "name": "Cooper",
21 | "avatar": "/static/common/images/avatars/c-man.png",
22 | "signature": "Gamer, hacker, and basketball player. Energy sword FTW!",
23 | "password_hash": "cBdTBwdALwoLAlY=",
24 | "question": 3,
25 | "answer": "Augusta",
26 | "role": 1,
27 | "status": 1,
28 | "id": 2,
29 | "created": "2019-02-16 04:46:27",
30 | "modified": "2019-02-16 04:46:27"
31 | },
32 | {
33 | "username": "Babygirl#1",
34 | "email": "taylor@pwnedhub.com",
35 | "name": "Taylor",
36 | "avatar": "/static/common/images/avatars/wolf.jpg",
37 | "signature": "Wolf in a past life. Nerd in the current. Johnny 5 is indeed alive.",
38 | "password_hash": "RwoRAAAXPw0WVhYG",
39 | "question": 2,
40 | "answer": "Rocket",
41 | "role": 1,
42 | "status": 1,
43 | "id": 3,
44 | "created": "2019-02-16 04:47:14",
45 | "modified": "2019-02-16 04:47:14"
46 | },
47 | {
48 | "username": "Hack3rPrincess",
49 | "email": "tanner@pwnedhub.com",
50 | "name": "Tanner",
51 | "avatar": "/static/common/images/avatars/kitty.jpg",
52 | "signature": "I might be small, cute, and cuddly, but remember... dynamite comes in small tightly wrapped packages that go boom.",
53 | "password_hash": "RgQXBgAGMhYNRRUPFw==",
54 | "question": 0,
55 | "answer": "Drumstick",
56 | "role": 1,
57 | "status": 1,
58 | "id": 4,
59 | "created": "2019-02-16 04:48:19",
60 | "modified": "2019-02-16 04:48:19"
61 | },
62 | {
63 | "username": "Baconator",
64 | "email": "emilee@pwnedhub.com",
65 | "name": "Emilee",
66 | "avatar": "/static/common/images/avatars/bacon.png",
67 | "signature": "Late to the party, but still the life of the party.",
68 | "password_hash": "XA4AFksXJAhWHVZVXQ==",
69 | "question": 4,
70 | "answer": "Chick-fil-a",
71 | "role": 1,
72 | "status": 1,
73 | "id": 5,
74 | "created": "2019-02-16 04:49:34",
75 | "modified": "2019-02-16 04:49:34"
76 | }
77 | ]
78 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/views/account.js:
--------------------------------------------------------------------------------
1 | import { useAuthStore } from '../stores/auth-store.js';
2 | import { useAppStore } from '../stores/app-store.js';
3 | import { User } from '../services/api.js';
4 |
5 | const { ref } = Vue;
6 |
7 | const template = `
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
31 |
32 | `;
33 |
34 | export default {
35 | name: 'Account',
36 | template,
37 | setup () {
38 | const authStore = useAuthStore();
39 | const appStore = useAppStore();
40 |
41 | const userForm = ref({
42 | email: '',
43 | name: '',
44 | avatar: '',
45 | signature: '',
46 | });
47 | // intentionally not reactive to avoid re-rendering on logout
48 | const currentUser = authStore.userInfo;
49 |
50 | function setFormValues() {
51 | userForm.value.email = currentUser.email;
52 | userForm.value.name = currentUser.name;
53 | userForm.value.avatar = currentUser.avatar;
54 | userForm.value.signature = currentUser.signature;
55 | };
56 |
57 | async function updateUser() {
58 | try {
59 | const json = await User.update(currentUser.id, userForm.value);
60 | authStore.setAuthUserInfo(json);
61 | appStore.createToast('Account updated.');
62 | } catch (error) {
63 | appStore.createToast(error.message);
64 | };
65 | };
66 |
67 | setFormValues();
68 |
69 | return {
70 | currentUser,
71 | userForm,
72 | updateUser,
73 | };
74 | },
75 | };
76 |
--------------------------------------------------------------------------------
/pwnedhub/decorators.py:
--------------------------------------------------------------------------------
1 | from flask import g, request, session, redirect, url_for, abort, make_response, flash
2 | from pwnedhub.constants import ROLES
3 | from pwnedhub.models import Config
4 | from functools import wraps
5 | from urllib.parse import urlparse
6 |
7 | def validate(params, method='POST'):
8 | def wrapper(func):
9 | @wraps(func)
10 | def wrapped(*args, **kwargs):
11 | if request.method == method:
12 | for param in params:
13 | valid = None
14 | # iterate through all request inputs
15 | for attr in ('args', 'form', 'files'):
16 | valid = getattr(request, attr).get(param)
17 | if valid:
18 | break
19 | if not valid:
20 | if request.referrer:
21 | flash('Required field(s) missing.')
22 | return redirect(request.referrer)
23 | abort(400)
24 | return func(*args, **kwargs)
25 | return wrapped
26 | return wrapper
27 |
28 | def login_required(func):
29 | @wraps(func)
30 | def wrapped(*args, **kwargs):
31 | if g.user:
32 | return func(*args, **kwargs)
33 | parsed_url = urlparse(request.url)
34 | location = parsed_url.path
35 | if parsed_url.query:
36 | location += '?{}'.format(parsed_url.query)
37 | return redirect(url_for('auth.login', next=location))
38 | return wrapped
39 |
40 | def roles_required(*roles):
41 | def wrapper(func):
42 | @wraps(func)
43 | def wrapped(*args, **kwargs):
44 | if ROLES[g.user.role] not in roles:
45 | return abort(403)
46 | return func(*args, **kwargs)
47 | return wrapped
48 | return wrapper
49 |
50 | def csrf_protect(func):
51 | @wraps(func)
52 | def wrapped(*args, **kwargs):
53 | if Config.get_value('CSRF_PROTECT'):
54 | # only apply CSRF protection to POSTs
55 | if request.method == 'POST':
56 | csrf_token = session.pop('csrf_token', None)
57 | untrusted_token = request.values.get('csrf_token')
58 | if not csrf_token or untrusted_token != csrf_token:
59 | flash('CSRF detected!')
60 | return redirect(request.base_url)
61 | return func(*args, **kwargs)
62 | return wrapped
63 |
64 | def no_cache(func):
65 | @wraps(func)
66 | def wrapped(*args, **kwargs):
67 | response = make_response(func(*args, **kwargs))
68 | response.headers['Pragma'] = 'no-cache'
69 | response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
70 | response.headers['Expires'] = '0'
71 | return response
72 | return wrapped
73 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/views/signup.js:
--------------------------------------------------------------------------------
1 | import { useAppStore } from '../stores/app-store.js';
2 | import { User } from '../services/api.js';
3 |
4 | const { ref } = Vue;
5 | const { useRouter } = VueRouter;
6 |
7 | const template = `
8 |
9 |
10 |
Welcome to PwnedHub !
11 |
The ability to consolidate and organize testing tools and results during client engagements is key for consultants dealing with short timelines and high expectations. Unfortunately, today's options for cloud resourced security testing are poorly designed and fail to support even the most basic needs. PwnedHub attempts to solve this problem by providing a space to share knowledge, execute test cases, and store the results.
12 |
Developed by child prodigies Cooper ("Cooperman"), Taylor ("Babygirl#1"), and Tanner ("Hack3rPrincess"), PwnedHub was designed based on experience gained through months of security testing. The PwnedHub team is ambitions, talented, and so confident in their product, if you don't like it, they'll issue a full refund. No questions asked.
13 |
So what are you waiting for? Signup today!
14 |
15 |
28 |
29 | `;
30 |
31 | export default {
32 | name: 'Signup',
33 | template,
34 | setup () {
35 | const appStore = useAppStore();
36 | const router = useRouter();
37 |
38 | const signupForm = ref({
39 | email: '',
40 | name: '',
41 | avatar: '',
42 | signature: '',
43 | });
44 |
45 | async function doSignup() {
46 | try {
47 | await User.create(signupForm.value);
48 | appStore.createToast('Account activation email sent. Please activate your account to log in.');
49 | router.push({ name: 'login' });
50 | } catch (error) {
51 | appStore.createToast(error.message);
52 | };
53 | };
54 |
55 | return {
56 | signupForm,
57 | doSignup,
58 | };
59 | },
60 | };
61 |
--------------------------------------------------------------------------------
/pwnedapi/fixtures/base/messages.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "comment": "Hey, did you guys hear that we're having a security assessment this week?",
4 | "user_id": 3,
5 | "room_id": 1,
6 | "id": 1,
7 | "created": "2019-02-18 04:55:11",
8 | "modified": "2019-02-18 04:55:11"
9 | },
10 | {
11 | "comment": "No.",
12 | "user_id": 4,
13 | "room_id": 1,
14 | "id": 2,
15 | "created": "2019-02-18 04:55:19",
16 | "modified": "2019-02-18 04:55:19"
17 | },
18 | {
19 | "comment": "First I'm hearing of it. I hope they don't find any bugs. This is my \"get rich quick\" scheme.",
20 | "user_id": 2,
21 | "room_id": 1,
22 | "id": 3,
23 | "created": "2019-02-18 04:56:09",
24 | "modified": "2019-02-18 04:56:09"
25 | },
26 | {
27 | "comment": "Heh. Me too. So looking forward to afternoons on my yacht. :-)",
28 | "user_id": 3,
29 | "room_id": 1,
30 | "id": 4,
31 | "created": "2019-02-18 04:57:02",
32 | "modified": "2019-02-18 04:57:02"
33 | },
34 | {
35 | "comment": "Wait... didn't we go live this week?",
36 | "user_id": 4,
37 | "room_id": 1,
38 | "id": 5,
39 | "created": "2019-02-18 04:57:08",
40 | "modified": "2019-02-18 04:57:08"
41 | },
42 | {
43 | "comment": "Well, as the most interesting man in the world says, \"I don't always get apps tested, but when I do, I get it done in prod.\"",
44 | "user_id": 2,
45 | "room_id": 1,
46 | "id": 6,
47 | "created": "2019-02-18 04:57:20",
48 | "modified": "2019-02-18 04:57:20"
49 | },
50 | {
51 | "comment": "LOL! So, yeah, did any of you guys fix those things I found during QA testing? I sent Cooper a link to them in a private message.",
52 | "user_id": 5,
53 | "room_id": 1,
54 | "id": 7,
55 | "created": "2019-02-18 04:57:32",
56 | "modified": "2019-02-18 04:57:32"
57 | },
58 | {
59 | "comment": "No.",
60 | "user_id": 4,
61 | "room_id": 1,
62 | "id": 8,
63 | "created": "2019-02-18 04:57:37",
64 | "modified": "2019-02-18 04:57:37"
65 | },
66 | {
67 | "comment": "My bad.",
68 | "user_id": 2,
69 | "room_id": 1,
70 | "id": 9,
71 | "created": "2019-02-18 04:57:41",
72 | "modified": "2019-02-18 04:57:41"
73 | },
74 | {
75 | "comment": "Uh oh...",
76 | "user_id": 3,
77 | "room_id": 1,
78 | "id": 10,
79 | "created": "2019-02-18 04:57:46",
80 | "modified": "2019-02-18 04:57:46"
81 | },
82 | {
83 | "comment": "Wow. We're totally going to end up on https://haveibeenpwned.com/PwnedWebsites.",
84 | "user_id": 5,
85 | "room_id": 1,
86 | "id": 11,
87 | "created": "2019-02-18 04:59:31",
88 | "modified": "2019-02-18 04:59:31"
89 | }
90 | ]
91 |
--------------------------------------------------------------------------------
/pwnedhub/templates/notes.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
4 |
5 |
6 |
7 | Edit
8 |
9 | View
10 |
11 |
12 |
13 | {{ notes }}
14 |
15 |
16 |
17 |
18 |
76 | {% endblock %}
77 |
--------------------------------------------------------------------------------
/pwnedapi/static/swaggerui/oauth2-redirect.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Swagger UI: OAuth2 Redirect
5 |
6 |
7 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/pwnedhub/templates/diagnostics.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | PwnedHub
5 |
6 |
68 |
69 |
70 | Platform Status
71 |
72 |
73 | {% for key, value in platform_stats.items() %}
74 |
75 | {{ key }}
76 | {{ value }}
77 |
78 | {% endfor %}
79 |
80 |
81 | Log Status
82 | {% for log in log_stats %}
83 |
84 |
85 | {% for key, value in log.items() %}
86 |
87 | {{ key }}
88 | {{ value }}
89 |
90 | {% endfor %}
91 |
92 |
93 | {% endfor %}
94 |
95 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/libs/vue3-infinite-loading.js:
--------------------------------------------------------------------------------
1 | (function(s,e){typeof exports=="object"&&typeof module<"u"?e(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],e):(s=typeof globalThis<"u"?globalThis:s||self,e(s.V3InfiniteLoading={},s.Vue))})(this,function(s,e){"use strict";function y(t,i){const o=t.getBoundingClientRect();if(!i)return o.top>=0&&o.bottom<=window.innerHeight;const n=i.getBoundingClientRect();return o.top>=n.top&&o.bottom<=n.bottom}async function E(t){return await e.nextTick(),t.value instanceof HTMLElement?t.value:t.value?document.querySelector(t.value):null}function f(t){let i=`0px 0px ${t.distance}px 0px`;t.top&&(i=`${t.distance}px 0px 0px 0px`);const o=new IntersectionObserver(n=>{n[0].isIntersecting&&(t.firstload&&t.emit(),t.firstload=!0)},{root:t.parentEl,rootMargin:i});return o.observe(t.infiniteLoading.value),o}const H="",u=(t,i)=>{const o=t.__vccOpts||t;for(const[n,d]of i)o[n]=d;return o},h={},S=t=>(e.pushScopeId("data-v-d3e37633"),t=t(),e.popScopeId(),t),x={class:"container"},w=[S(()=>e.createElementVNode("div",{class:"spinner"},null,-1))];function V(t,i){return e.openBlock(),e.createElementBlock("div",x,w)}const k=u(h,[["render",V],["__scopeId","data-v-d3e37633"]]),I={class:"state-error"},N=e.defineComponent({__name:"InfiniteLoading",props:{top:{type:Boolean,default:!1},target:{},distance:{default:0},identifier:{},firstload:{type:Boolean,default:!0},slots:{}},emits:["infinite"],setup(t,{emit:i}){const o=t;let n=null,d=0;const p=e.ref(null),c=e.ref(""),{top:_,firstload:L,distance:T}=o,{identifier:$,target:b}=e.toRefs(o),l={infiniteLoading:p,top:_,firstload:L,distance:T,parentEl:null,emit(){d=(l.parentEl||document.documentElement).scrollHeight,m.loading(),i("infinite",m)}},m={loading(){c.value="loading"},async loaded(){c.value="loaded";const r=l.parentEl||document.documentElement;await e.nextTick(),_&&(r.scrollTop=r.scrollHeight-d),y(p.value,l.parentEl)&&l.emit()},complete(){c.value="complete",n==null||n.disconnect()},error(){c.value="error"}};return e.watch($,()=>{n==null||n.disconnect(),n=f(l)}),e.onMounted(async()=>{l.parentEl=await E(b),n=f(l)}),e.onUnmounted(()=>{n==null||n.disconnect()}),(r,g)=>(e.openBlock(),e.createElementBlock("div",{ref_key:"infiniteLoading",ref:p,style:{"min-height":"1px"}},[e.withDirectives(e.createElementVNode("div",null,[e.renderSlot(r.$slots,"spinner",{},()=>[e.createVNode(k)],!0)],512),[[e.vShow,c.value=="loading"]]),c.value=="complete"?e.renderSlot(r.$slots,"complete",{key:0},()=>{var a;return[e.createElementVNode("span",null,e.toDisplayString(((a=r.slots)==null?void 0:a.complete)||"No more results!"),1)]},!0):e.createCommentVNode("",!0),c.value=="error"?e.renderSlot(r.$slots,"error",{key:1,retry:l.emit},()=>{var a;return[e.createElementVNode("span",I,[e.createElementVNode("span",null,e.toDisplayString(((a=r.slots)==null?void 0:a.error)||"Oops something went wrong!"),1),e.createElementVNode("button",{class:"retry",onClick:g[0]||(g[0]=(...C)=>l.emit&&l.emit(...C))},"retry")])]},!0):e.createCommentVNode("",!0)],512))}}),O="",B=u(N,[["__scopeId","data-v-a7077831"]]);s.default=B,Object.defineProperties(s,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
2 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/components/navigation.js:
--------------------------------------------------------------------------------
1 | import { useAuthStore } from '../stores/auth-store.js';
2 |
3 | const { ref, watch } = Vue;
4 | const { useRoute } = VueRouter;
5 |
6 | const template = `
7 |
8 |
21 |
22 | `;
23 |
24 | export default {
25 | name: 'Navigation',
26 | template,
27 | setup () {
28 | const authStore = useAuthStore();
29 | const route = useRoute();
30 |
31 | const isOpen = ref(false);
32 | const permissions = {
33 | guest: [
34 | {
35 | id: 0,
36 | text: 'Login',
37 | name: 'login',
38 | },
39 | {
40 | id: 1,
41 | text: 'Signup',
42 | name: 'signup',
43 | },
44 | ],
45 | admin: [
46 | {
47 | id: 0,
48 | text: 'Users',
49 | name: 'users',
50 | },
51 | {
52 | id: 1,
53 | text: 'Tools',
54 | name: 'tools',
55 | },
56 | {
57 | id: 2,
58 | text: 'Messaging',
59 | name: 'messaging',
60 | },
61 | ],
62 | user: [
63 | {
64 | id: 0,
65 | text: 'Notes',
66 | name: 'notes',
67 | },
68 | {
69 | id: 1,
70 | text: 'Scans',
71 | name: 'scans',
72 | },
73 | {
74 | id: 2,
75 | text: 'Messaging',
76 | name: 'messaging',
77 | },
78 | ],
79 | };
80 |
81 | watch(() => route.name, () => {
82 | isOpen.value = false;
83 | });
84 |
85 | function toggleMenu() {
86 | isOpen.value = !isOpen.value;
87 | };
88 |
89 | return {
90 | authStore,
91 | isOpen,
92 | permissions,
93 | toggleMenu,
94 | };
95 | },
96 | };
97 |
--------------------------------------------------------------------------------
/pwnedhub/templates/tools.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block body %}
3 |
19 |
75 | {% endblock %}
76 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/router.js:
--------------------------------------------------------------------------------
1 | import Signup from './views/signup.js';
2 | import Activate from './views/activate.js';
3 | import Login from './views/login.js';
4 | import PasswordlessAuth from './views/passwordless.js';
5 | import Account from './views/account.js';
6 | import Profile from './views/profile.js';
7 | import Notes from './views/notes.js';
8 | import Scans from './views/scans.js';
9 | import Messaging from './views/messages.js';
10 | import Tools from './views/tools.js'
11 | import Users from './views/users.js'
12 | import { useAuthStore } from './stores/auth-store.js';
13 |
14 | const { createRouter, createWebHashHistory } = VueRouter;
15 | const routes = [
16 | {
17 | path: '/signup',
18 | name: 'signup',
19 | component: Signup,
20 | },
21 | {
22 | path: '/signup/activate/:activateToken',
23 | name: 'activate',
24 | component: Activate,
25 | props: true,
26 | },
27 | {
28 | path: '/login',
29 | name: 'login',
30 | component: Login,
31 | },
32 | {
33 | path: '/login/passwordless',
34 | name: 'passwordless',
35 | component: PasswordlessAuth,
36 | },
37 | {
38 | path: '/account',
39 | name: 'account',
40 | component: Account,
41 | meta: {
42 | authRequired: true,
43 | },
44 | },
45 | {
46 | path: '/profile/:userId',
47 | name: 'profile',
48 | component: Profile,
49 | props: true,
50 | meta: {
51 | authRequired: true,
52 | },
53 | },
54 | {
55 | path: '/notes',
56 | name: 'notes',
57 | component: Notes,
58 | meta: {
59 | authRequired: true,
60 | },
61 | },
62 | {
63 | path: '/scans',
64 | name: 'scans',
65 | component: Scans,
66 | meta: {
67 | authRequired: true,
68 | },
69 | },
70 | {
71 | path: '/messaging',
72 | name: 'messaging',
73 | component: Messaging,
74 | meta: {
75 | authRequired: true,
76 | },
77 | },
78 | {
79 | path: '/admin/tools',
80 | name: 'tools',
81 | component: Tools,
82 | meta: {
83 | authRequired: true,
84 | },
85 | },
86 | {
87 | path: '/admin/users',
88 | name: 'users',
89 | component: Users,
90 | meta: {
91 | authRequired: true,
92 | },
93 | },
94 | {
95 | path: '/:catchAll(.*)',
96 | redirect: '/login',
97 | },
98 | ];
99 |
100 | export const router = createRouter({
101 | history: createWebHashHistory(),
102 | routes,
103 | });
104 |
105 | router.beforeEach((to, from, next) => {
106 | const authStore = useAuthStore();
107 | if (to.matched.some(record => record.meta.authRequired)) {
108 | if (!authStore.isLoggedIn) {
109 | next({
110 | name: 'login',
111 | params: { nextUrl: to.fullPath },
112 | });
113 | } else {
114 | next();
115 | };
116 | } else {
117 | // the login/passwordless views use similar logic to handle routing of the nextUrl parameter
118 | // all must be updated if there is a change
119 | if (authStore.isLoggedIn) {
120 | if (authStore.isAdmin) {
121 | next({ name: 'users' });
122 | };
123 | next({ name: 'notes' });
124 | } else {
125 | next();
126 | };
127 | };
128 | });
129 |
--------------------------------------------------------------------------------
/pwnedapi/__init__.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, Blueprint
2 | from pwnedapi.extensions import db, cors, socketio
3 | from redis import Redis
4 | import click
5 | import os
6 | import rq
7 |
8 | def create_app():
9 |
10 | # create the Flask application
11 | app = Flask(__name__, static_url_path='/static')
12 |
13 | # configure the Flask application
14 | config_class = os.getenv('CONFIG', default='Development')
15 | app.config.from_object('pwnedapi.config.{}'.format(config_class.title()))
16 |
17 | app.redis = Redis.from_url(app.config['REDIS_URL'])
18 | app.api_task_queue = rq.Queue('pwnedapi-tasks', connection=app.redis)
19 | app.bot_task_queue = rq.Queue('adminbot-tasks', connection=app.redis)
20 |
21 | def is_allowed_origin(response):
22 | for k, v in response.headers:
23 | if k == 'Access-Control-Allow-Origin':
24 | return v in app.config['ALLOWED_ORIGINS']
25 | return False
26 |
27 | def remove_cors_headers(response):
28 | to_remove = []
29 | for k, v in response.headers:
30 | if any(k.startswith(s) for s in ['Access-Control-', 'Vary']):
31 | to_remove.append(k)
32 | for name in to_remove:
33 | del response.headers[name]
34 | return response
35 |
36 | # must be set before initializing the CORS extension to modify
37 | # headers created by the extension's `after_request` methods
38 | @app.after_request
39 | def config_cors(response):
40 | from pwnedapi.models import Config
41 | if Config.get_value('CORS_RESTRICT'):
42 | # apply the CORS whitelist from the config
43 | if not is_allowed_origin(response):
44 | response = remove_cors_headers(response)
45 | return response
46 |
47 | db.init_app(app)
48 | cors.init_app(app)
49 | socketio.init_app(app, cors_allowed_origins=app.config['ALLOWED_ORIGINS'])
50 |
51 | StaticBlueprint = Blueprint('common', __name__, static_url_path='/static/common', static_folder='../common/static')
52 | app.register_blueprint(StaticBlueprint)
53 |
54 | from pwnedapi.routes.api import blp as ApiBlueprint
55 | app.register_blueprint(ApiBlueprint)
56 |
57 | from pwnedapi.routes import websockets
58 |
59 | @app.cli.command('init')
60 | @click.argument('dataset')
61 | def init_data(dataset):
62 | from flask import current_app
63 | from pwnedapi import models
64 | import json
65 | import os
66 | db.create_all(bind_key=None)
67 | for cls in models.BaseModel.__subclasses__():
68 | fixture_path = os.path.join(current_app.root_path, 'fixtures', dataset, f"{cls.__table__.name}.json")
69 | if os.path.exists(fixture_path):
70 | print(f"Processing {fixture_path}.")
71 | with open(fixture_path) as fp:
72 | for row in json.load(fp):
73 | db.session.add(cls(**row))
74 | db.session.commit()
75 | print('Database initialized.')
76 |
77 | @app.cli.command('export')
78 | def export_data():
79 | from pwnedapi.models import BaseModel
80 | import json
81 | for cls in BaseModel.__subclasses__():
82 | objs = [obj.serialize_for_export() for obj in cls.query.all()]
83 | if objs:
84 | print(f"\n***** {cls.__table__.name}.json *****\n")
85 | print(json.dumps(objs, indent=4, default=str))
86 | print('Database exported.')
87 |
88 | @app.cli.command('purge')
89 | def purge_data():
90 | db.drop_all(bind_key=None)
91 | db.session.commit()
92 | print('Database purged.')
93 |
94 | return app, socketio
95 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/views/messages.css:
--------------------------------------------------------------------------------
1 | .rooms {
2 | position: relative;
3 | }
4 |
5 | .rooms .closed {
6 | transform: translateX(-15rem);
7 | }
8 |
9 | .rooms .tab {
10 | position: absolute;
11 | top: calc(50vh - 2rem - 50px);
12 | left: 15rem;
13 | z-index: 10;
14 | background-color: #333;
15 | color: #eee;
16 | padding: 2rem 1rem;
17 | border-radius: 0 0.5rem 0.5rem 0;
18 | box-shadow: 0 0 1rem rgba(0, 0, 0, 0.5);
19 | transition: all 0.3s ease-in;
20 | }
21 |
22 | .rooms .rooms-wrapper {
23 | position: absolute;
24 | top: 0;
25 | left: 0;
26 | width: 15rem;
27 | z-index: 20;
28 | box-shadow: 0 0 1rem rgba(0, 0, 0, 0.5);
29 | transition: all 0.3s ease-in;
30 | }
31 |
32 | .rooms .rooms-wrapper {
33 | background-color: #333;
34 | color: #eee;
35 | height: calc(100vh - 50px); /* height of nav (50) */
36 | overflow-y: auto;
37 | }
38 |
39 | .rooms .rooms-wrapper > * {
40 | padding: 1rem;
41 | border-bottom: 1px solid #444;
42 | }
43 |
44 | .rooms .rooms-wrapper .label {
45 | font-size: 1.25rem;
46 | letter-spacing: 0.1rem;
47 | text-transform: uppercase;
48 | }
49 |
50 | .rooms .rooms-wrapper .room {
51 | cursor: pointer;
52 | }
53 |
54 | .rooms .rooms-wrapper .room.active {
55 | font-weight: bold;
56 | }
57 |
58 | .rooms .rooms-wrapper .room.tagged:before {
59 | content:"• ";
60 | color: red;
61 | }
62 |
63 | /* desktop rooms */
64 | @media all and (min-width: 960px) {
65 |
66 | .rooms .tab {
67 | display: none;
68 | }
69 |
70 | .rooms .rooms-wrapper {
71 | position: static;
72 | }
73 |
74 | .rooms .closed {
75 | transform: none;
76 | }
77 |
78 | }
79 |
80 | .messages {
81 | height: calc(100vh - 50px); /* height of nav (50) */
82 | padding: 0 1rem 1rem 1rem;
83 | }
84 |
85 | .messages .message-container {
86 | overflow-y: scroll;
87 | }
88 |
89 | .messages .message-container .message {
90 | position: relative;
91 | padding: 1rem 0;
92 | }
93 |
94 | .messages .message-container .message .avatar {
95 | margin: 0 1rem;
96 | }
97 |
98 | .messages .message-container .message .avatar img {
99 | display: block;
100 | object-fit: cover;
101 | width: 4rem;
102 | height: 4rem;
103 | }
104 |
105 | .messages .message-container .message .name {
106 | font-size: 1.5rem;
107 | font-weight: bold;
108 | margin-bottom: .5rem;
109 | }
110 |
111 | .messages .message-container .message .comment {
112 | margin-bottom: .5rem;
113 | }
114 |
115 | .messages .message-container .message a.img-btn {
116 | position: absolute;
117 | top: 0.5rem;
118 | right: 0.5rem;
119 | }
120 |
121 | .messages .message-container .message .timestamp {
122 | font-size: 1rem;
123 | margin-bottom: 0;
124 | }
125 |
126 | .messages .message-form {
127 | position: relative;
128 | }
129 |
130 | .messages .message-form input[type=text] {
131 | margin: 0;
132 | }
133 |
134 | .messages .message-form button {
135 | line-height: 1em;
136 | border: none;
137 | background-color: transparent;
138 | position: absolute;
139 | right: 0;
140 | top: 0;
141 | border: 0;
142 | }
143 |
144 | /* desktop messages */
145 | @media all and (min-width: 960px) {
146 |
147 | .messages .message-container .message:hover {
148 | background-color: #eee;
149 | }
150 |
151 | .messages .message-container .message a.img-btn {
152 | display: none;
153 | }
154 |
155 | .messages .message-container .message:hover a.img-btn {
156 | display: inline;
157 | }
158 |
159 | .messages .message-form button {
160 | display: none;
161 | }
162 |
163 | }
164 |
--------------------------------------------------------------------------------
/database/cs/04-pwnedhub-admin.sql:
--------------------------------------------------------------------------------
1 | -- Attach to database
2 | USE `pwnedhub-admin`;
3 |
4 | -- MySQL dump 10.13 Distrib 8.0.33, for Linux (x86_64)
5 | --
6 | -- Host: localhost Database: pwnedhub-admin
7 | -- ------------------------------------------------------
8 | -- Server version 8.0.33
9 |
10 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
11 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
12 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
13 | /*!50503 SET NAMES utf8mb4 */;
14 | /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
15 | /*!40103 SET TIME_ZONE='+00:00' */;
16 | /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
17 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
18 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
19 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
20 |
21 | --
22 | -- Table structure for table `configs`
23 | --
24 |
25 | DROP TABLE IF EXISTS `configs`;
26 | /*!40101 SET @saved_cs_client = @@character_set_client */;
27 | /*!50503 SET character_set_client = utf8mb4 */;
28 | CREATE TABLE `configs` (
29 | `id` int NOT NULL AUTO_INCREMENT,
30 | `name` varchar(255) NOT NULL,
31 | `description` text NOT NULL,
32 | `type` varchar(255) NOT NULL,
33 | `value` tinyint(1) NOT NULL,
34 | PRIMARY KEY (`id`)
35 | ) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
36 | /*!40101 SET character_set_client = @saved_cs_client */;
37 |
38 | --
39 | -- Dumping data for table `configs`
40 | --
41 |
42 | LOCK TABLES `configs` WRITE;
43 | /*!40000 ALTER TABLE `configs` DISABLE KEYS */;
44 | INSERT INTO `configs` VALUES (1,'CSRF_PROTECT','Profile CSRF Protection (PwnedHub)','security control',1),(2,'OSCI_PROTECT','Tools OSCI Protection (PwnedHub)','security control',0),(3,'SQLI_PROTECT','Login SQLi Protection (PwnedHub)','security control',0),(4,'CSP_PROTECT','Content Security Policy (PwnedHub)','security control',0),(5,'CORS_RESTRICT','Restricted CORS (PwnedAPI)','security control',1),(6,'JWT_VERIFY','Verify JWT Signatures (PwnedAPI)','security control',1),(7,'JWT_ENCRYPT','Encrypt JWTs (PwnedAPI)','security control',0),(8,'BEARER_AUTH_ENABLE','Bearer Token Authentication (PwnedAPI)','feature',1),(9,'OIDC_ENABLE','OpenID Connect Authentication (PwnedHub)','feature',0),(10,'SSO_ENABLE','SSO Authentication (PwnedHub)','feature',0),(11,'OOB_RESET_ENABLE','Out-of-Band Password Reset (PwnedHub)','feature',0),(12,'CTF_MODE','CTF Mode (Warning: Disables this interface!)','feature',0);
45 | /*!40000 ALTER TABLE `configs` ENABLE KEYS */;
46 | UNLOCK TABLES;
47 |
48 | --
49 | -- Table structure for table `emails`
50 | --
51 |
52 | DROP TABLE IF EXISTS `emails`;
53 | /*!40101 SET @saved_cs_client = @@character_set_client */;
54 | /*!50503 SET character_set_client = utf8mb4 */;
55 | CREATE TABLE `emails` (
56 | `id` int NOT NULL AUTO_INCREMENT,
57 | `created` datetime NOT NULL,
58 | `sender` varchar(255) NOT NULL,
59 | `receiver` varchar(255) NOT NULL,
60 | `subject` text NOT NULL,
61 | `body` text NOT NULL,
62 | PRIMARY KEY (`id`)
63 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
64 | /*!40101 SET character_set_client = @saved_cs_client */;
65 |
66 | --
67 | -- Dumping data for table `emails`
68 | --
69 |
70 | LOCK TABLES `emails` WRITE;
71 | /*!40000 ALTER TABLE `emails` DISABLE KEYS */;
72 | /*!40000 ALTER TABLE `emails` ENABLE KEYS */;
73 | UNLOCK TABLES;
74 | /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
75 |
76 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
77 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
78 | /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
79 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
80 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
81 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
82 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
83 |
84 | -- Dump completed on 2023-07-17 4:14:26
85 |
--------------------------------------------------------------------------------
/database/ctf/04-pwnedhub-admin.sql:
--------------------------------------------------------------------------------
1 | -- Attach to database
2 | USE `pwnedhub-admin`;
3 |
4 | -- MySQL dump 10.13 Distrib 8.0.33, for Linux (x86_64)
5 | --
6 | -- Host: localhost Database: pwnedhub-admin
7 | -- ------------------------------------------------------
8 | -- Server version 8.0.33
9 |
10 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
11 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
12 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
13 | /*!50503 SET NAMES utf8mb4 */;
14 | /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
15 | /*!40103 SET TIME_ZONE='+00:00' */;
16 | /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
17 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
18 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
19 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
20 |
21 | --
22 | -- Table structure for table `configs`
23 | --
24 |
25 | DROP TABLE IF EXISTS `configs`;
26 | /*!40101 SET @saved_cs_client = @@character_set_client */;
27 | /*!50503 SET character_set_client = utf8mb4 */;
28 | CREATE TABLE `configs` (
29 | `id` int NOT NULL AUTO_INCREMENT,
30 | `name` varchar(255) NOT NULL,
31 | `description` text NOT NULL,
32 | `type` varchar(255) NOT NULL,
33 | `value` tinyint(1) NOT NULL,
34 | PRIMARY KEY (`id`)
35 | ) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
36 | /*!40101 SET character_set_client = @saved_cs_client */;
37 |
38 | --
39 | -- Dumping data for table `configs`
40 | --
41 |
42 | LOCK TABLES `configs` WRITE;
43 | /*!40000 ALTER TABLE `configs` DISABLE KEYS */;
44 | INSERT INTO `configs` VALUES (1,'CSRF_PROTECT','Profile CSRF Protection (PwnedHub)','security control',0),(2,'OSCI_PROTECT','Tools OSCI Protection (PwnedHub)','security control',1),(3,'SQLI_PROTECT','Login SQLi Protection (PwnedHub)','security control',1),(4,'CSP_PROTECT','Content Security Policy (PwnedHub)','security control',1),(5,'CORS_RESTRICT','Restricted CORS (PwnedAPI)','security control',0),(6,'JWT_VERIFY','Verify JWT Signatures (PwnedAPI)','security control',1),(7,'JWT_ENCRYPT','Encrypt JWTs (PwnedAPI)','security control',0),(8,'BEARER_AUTH_ENABLE','Bearer Token Authentication (PwnedAPI)','feature',0),(9,'OIDC_ENABLE','OpenID Connect Authentication (PwnedHub)','feature',0),(10,'SSO_ENABLE','SSO Authentication (PwnedHub)','feature',0),(11,'OOB_RESET_ENABLE','Out-of-Band Password Reset (PwnedHub)','feature',1),(12,'CTF_MODE','CTF Mode (Warning: Disables this interface!)','feature',1);
45 | /*!40000 ALTER TABLE `configs` ENABLE KEYS */;
46 | UNLOCK TABLES;
47 |
48 | --
49 | -- Table structure for table `emails`
50 | --
51 |
52 | DROP TABLE IF EXISTS `emails`;
53 | /*!40101 SET @saved_cs_client = @@character_set_client */;
54 | /*!50503 SET character_set_client = utf8mb4 */;
55 | CREATE TABLE `emails` (
56 | `id` int NOT NULL AUTO_INCREMENT,
57 | `created` datetime NOT NULL,
58 | `sender` varchar(255) NOT NULL,
59 | `receiver` varchar(255) NOT NULL,
60 | `subject` text NOT NULL,
61 | `body` text NOT NULL,
62 | PRIMARY KEY (`id`)
63 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
64 | /*!40101 SET character_set_client = @saved_cs_client */;
65 |
66 | --
67 | -- Dumping data for table `emails`
68 | --
69 |
70 | LOCK TABLES `emails` WRITE;
71 | /*!40000 ALTER TABLE `emails` DISABLE KEYS */;
72 | /*!40000 ALTER TABLE `emails` ENABLE KEYS */;
73 | UNLOCK TABLES;
74 | /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
75 |
76 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
77 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
78 | /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
79 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
80 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
81 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
82 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
83 |
84 | -- Dump completed on 2023-07-17 4:14:26
85 |
--------------------------------------------------------------------------------
/database/init/04-pwnedhub-admin.sql:
--------------------------------------------------------------------------------
1 | -- Attach to database
2 | USE `pwnedhub-admin`;
3 |
4 | -- MySQL dump 10.13 Distrib 8.0.33, for Linux (x86_64)
5 | --
6 | -- Host: localhost Database: pwnedhub-admin
7 | -- ------------------------------------------------------
8 | -- Server version 8.0.33
9 |
10 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
11 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
12 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
13 | /*!50503 SET NAMES utf8mb4 */;
14 | /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
15 | /*!40103 SET TIME_ZONE='+00:00' */;
16 | /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
17 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
18 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
19 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
20 |
21 | --
22 | -- Table structure for table `configs`
23 | --
24 |
25 | DROP TABLE IF EXISTS `configs`;
26 | /*!40101 SET @saved_cs_client = @@character_set_client */;
27 | /*!50503 SET character_set_client = utf8mb4 */;
28 | CREATE TABLE `configs` (
29 | `id` int NOT NULL AUTO_INCREMENT,
30 | `name` varchar(255) NOT NULL,
31 | `description` text NOT NULL,
32 | `type` varchar(255) NOT NULL,
33 | `value` tinyint(1) NOT NULL,
34 | PRIMARY KEY (`id`)
35 | ) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
36 | /*!40101 SET character_set_client = @saved_cs_client */;
37 |
38 | --
39 | -- Dumping data for table `configs`
40 | --
41 |
42 | LOCK TABLES `configs` WRITE;
43 | /*!40000 ALTER TABLE `configs` DISABLE KEYS */;
44 | INSERT INTO `configs` VALUES (1,'CSRF_PROTECT','Profile CSRF Protection (PwnedHub)','security control',1),(2,'OSCI_PROTECT','Tools OSCI Protection (PwnedHub)','security control',0),(3,'SQLI_PROTECT','Login SQLi Protection (PwnedHub)','security control',0),(4,'CSP_PROTECT','Content Security Policy (PwnedHub)','security control',0),(5,'CORS_RESTRICT','Restricted CORS (PwnedAPI)','security control',1),(6,'JWT_VERIFY','Verify JWT Signatures (PwnedAPI)','security control',1),(7,'JWT_ENCRYPT','Encrypt JWTs (PwnedAPI)','security control',0),(8,'BEARER_AUTH_ENABLE','Bearer Token Authentication (PwnedAPI)','feature',1),(9,'OIDC_ENABLE','OpenID Connect Authentication (PwnedHub)','feature',0),(10,'SSO_ENABLE','SSO Authentication (PwnedHub)','feature',0),(11,'OOB_RESET_ENABLE','Out-of-Band Password Reset (PwnedHub)','feature',0),(12,'CTF_MODE','CTF Mode (Warning: Disables this interface!)','feature',0);
45 | /*!40000 ALTER TABLE `configs` ENABLE KEYS */;
46 | UNLOCK TABLES;
47 |
48 | --
49 | -- Table structure for table `emails`
50 | --
51 |
52 | DROP TABLE IF EXISTS `emails`;
53 | /*!40101 SET @saved_cs_client = @@character_set_client */;
54 | /*!50503 SET character_set_client = utf8mb4 */;
55 | CREATE TABLE `emails` (
56 | `id` int NOT NULL AUTO_INCREMENT,
57 | `created` datetime NOT NULL,
58 | `sender` varchar(255) NOT NULL,
59 | `receiver` varchar(255) NOT NULL,
60 | `subject` text NOT NULL,
61 | `body` text NOT NULL,
62 | PRIMARY KEY (`id`)
63 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
64 | /*!40101 SET character_set_client = @saved_cs_client */;
65 |
66 | --
67 | -- Dumping data for table `emails`
68 | --
69 |
70 | LOCK TABLES `emails` WRITE;
71 | /*!40000 ALTER TABLE `emails` DISABLE KEYS */;
72 | /*!40000 ALTER TABLE `emails` ENABLE KEYS */;
73 | UNLOCK TABLES;
74 | /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
75 |
76 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
77 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
78 | /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
79 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
80 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
81 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
82 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
83 |
84 | -- Dump completed on 2023-07-17 4:14:26
85 |
--------------------------------------------------------------------------------
/pwnedspa/static/vue/libs/vue-demi.js:
--------------------------------------------------------------------------------
1 | var VueDemi = (function (VueDemi, Vue, VueCompositionAPI) {
2 | if (VueDemi.install) {
3 | return VueDemi
4 | }
5 | if (!Vue) {
6 | console.error('[vue-demi] no Vue instance found, please be sure to import `vue` before `vue-demi`.')
7 | return VueDemi
8 | }
9 |
10 | // Vue 2.7
11 | if (Vue.version.slice(0, 4) === '2.7.') {
12 | for (var key in Vue) {
13 | VueDemi[key] = Vue[key]
14 | }
15 | VueDemi.isVue2 = true
16 | VueDemi.isVue3 = false
17 | VueDemi.install = function () {}
18 | VueDemi.Vue = Vue
19 | VueDemi.Vue2 = Vue
20 | VueDemi.version = Vue.version
21 | VueDemi.warn = Vue.util.warn
22 | VueDemi.hasInjectionContext = () => !!VueDemi.getCurrentInstance()
23 | function createApp(rootComponent, rootProps) {
24 | var vm
25 | var provide = {}
26 | var app = {
27 | config: Vue.config,
28 | use: Vue.use.bind(Vue),
29 | mixin: Vue.mixin.bind(Vue),
30 | component: Vue.component.bind(Vue),
31 | provide: function (key, value) {
32 | provide[key] = value
33 | return this
34 | },
35 | directive: function (name, dir) {
36 | if (dir) {
37 | Vue.directive(name, dir)
38 | return app
39 | } else {
40 | return Vue.directive(name)
41 | }
42 | },
43 | mount: function (el, hydrating) {
44 | if (!vm) {
45 | vm = new Vue(Object.assign({ propsData: rootProps }, rootComponent, { provide: Object.assign(provide, rootComponent.provide) }))
46 | vm.$mount(el, hydrating)
47 | return vm
48 | } else {
49 | return vm
50 | }
51 | },
52 | unmount: function () {
53 | if (vm) {
54 | vm.$destroy()
55 | vm = undefined
56 | }
57 | },
58 | }
59 | return app
60 | }
61 | VueDemi.createApp = createApp
62 | }
63 | // Vue 2.6.x
64 | else if (Vue.version.slice(0, 2) === '2.') {
65 | if (VueCompositionAPI) {
66 | for (var key in VueCompositionAPI) {
67 | VueDemi[key] = VueCompositionAPI[key]
68 | }
69 | VueDemi.isVue2 = true
70 | VueDemi.isVue3 = false
71 | VueDemi.install = function () {}
72 | VueDemi.Vue = Vue
73 | VueDemi.Vue2 = Vue
74 | VueDemi.version = Vue.version
75 | VueDemi.hasInjectionContext = () => !!VueDemi.getCurrentInstance()
76 | } else {
77 | console.error('[vue-demi] no VueCompositionAPI instance found, please be sure to import `@vue/composition-api` before `vue-demi`.')
78 | }
79 | }
80 | // Vue 3
81 | else if (Vue.version.slice(0, 2) === '3.') {
82 | for (var key in Vue) {
83 | VueDemi[key] = Vue[key]
84 | }
85 | VueDemi.isVue2 = false
86 | VueDemi.isVue3 = true
87 | VueDemi.install = function () {}
88 | VueDemi.Vue = Vue
89 | VueDemi.Vue2 = undefined
90 | VueDemi.version = Vue.version
91 | VueDemi.set = function (target, key, val) {
92 | if (Array.isArray(target)) {
93 | target.length = Math.max(target.length, key)
94 | target.splice(key, 1, val)
95 | return val
96 | }
97 | target[key] = val
98 | return val
99 | }
100 | VueDemi.del = function (target, key) {
101 | if (Array.isArray(target)) {
102 | target.splice(key, 1)
103 | return
104 | }
105 | delete target[key]
106 | }
107 | } else {
108 | console.error('[vue-demi] Vue version ' + Vue.version + ' is unsupported.')
109 | }
110 | return VueDemi
111 | })(
112 | (this.VueDemi = this.VueDemi || (typeof VueDemi !== 'undefined' ? VueDemi : {})),
113 | this.Vue || (typeof Vue !== 'undefined' ? Vue : undefined),
114 | this.VueCompositionAPI || (typeof VueCompositionAPI !== 'undefined' ? VueCompositionAPI : undefined)
115 | );
116 |
--------------------------------------------------------------------------------