Files
CTFd/CTFd/plugins/dynamic_challenges/__init__.py
2019-04-17 01:36:30 -04:00

248 lines
9.0 KiB
Python

from __future__ import division # Use floating point for math calculations
from CTFd.plugins.challenges import BaseChallenge, CHALLENGE_CLASSES
from CTFd.plugins import register_plugin_assets_directory
from CTFd.plugins.flags import get_flag_class
from CTFd.models import db, Solves, Fails, Flags, Challenges, ChallengeFiles, Tags, Hints
from CTFd.utils.user import get_ip
from CTFd.utils.uploads import delete_file
from CTFd.utils.modes import get_model
from flask import Blueprint
import math
class DynamicValueChallenge(BaseChallenge):
id = "dynamic" # Unique identifier used to register challenges
name = "dynamic" # Name of a challenge type
templates = { # Handlebars templates used for each aspect of challenge editing & viewing
'create': '/plugins/dynamic_challenges/assets/create.html',
'update': '/plugins/dynamic_challenges/assets/update.html',
'view': '/plugins/dynamic_challenges/assets/view.html',
}
scripts = { # Scripts that are loaded when a template is loaded
'create': '/plugins/dynamic_challenges/assets/create.js',
'update': '/plugins/dynamic_challenges/assets/update.js',
'view': '/plugins/dynamic_challenges/assets/view.js',
}
# Route at which files are accessible. This must be registered using register_plugin_assets_directory()
route = '/plugins/dynamic_challenges/assets/'
# Blueprint used to access the static_folder directory.
blueprint = Blueprint('dynamic_challenges', __name__, template_folder='templates', static_folder='assets')
@staticmethod
def create(request):
"""
This method is used to process the challenge creation request.
:param request:
:return:
"""
data = request.form or request.get_json()
challenge = DynamicChallenge(**data)
db.session.add(challenge)
db.session.commit()
return challenge
@staticmethod
def read(challenge):
"""
This method is in used to access the data of a challenge in a format processable by the front end.
:param challenge:
:return: Challenge object, data dictionary to be returned to the user
"""
challenge = DynamicChallenge.query.filter_by(id=challenge.id).first()
data = {
'id': challenge.id,
'name': challenge.name,
'value': challenge.value,
'initial': challenge.initial,
'decay': challenge.decay,
'minimum': challenge.minimum,
'description': challenge.description,
'category': challenge.category,
'state': challenge.state,
'max_attempts': challenge.max_attempts,
'type': challenge.type,
'type_data': {
'id': DynamicValueChallenge.id,
'name': DynamicValueChallenge.name,
'templates': DynamicValueChallenge.templates,
'scripts': DynamicValueChallenge.scripts,
}
}
return data
@staticmethod
def update(challenge, request):
"""
This method is used to update the information associated with a challenge. This should be kept strictly to the
Challenges table and any child tables.
:param challenge:
:param request:
:return:
"""
data = request.form or request.get_json()
for attr, value in data.items():
# We need to set these to floats so that the next operations don't operate on strings
if attr in ('initial', 'minimum', 'decay'):
value = float(value)
setattr(challenge, attr, value)
Model = get_model()
solve_count = Solves.query \
.join(Model, Solves.account_id == Model.id) \
.filter(Solves.challenge_id == challenge.id, Model.hidden == False, Model.banned == False) \
.count()
# It is important that this calculation takes into account floats.
# Hence this file uses from __future__ import division
value = (((challenge.minimum - challenge.initial) / (challenge.decay ** 2)) * (solve_count ** 2)) + challenge.initial
value = math.ceil(value)
if value < challenge.minimum:
value = challenge.minimum
challenge.value = value
db.session.commit()
return challenge
@staticmethod
def delete(challenge):
"""
This method is used to delete the resources used by a challenge.
:param challenge:
:return:
"""
Fails.query.filter_by(challenge_id=challenge.id).delete()
Solves.query.filter_by(challenge_id=challenge.id).delete()
Flags.query.filter_by(challenge_id=challenge.id).delete()
files = ChallengeFiles.query.filter_by(challenge_id=challenge.id).all()
for f in files:
delete_file(f.id)
ChallengeFiles.query.filter_by(challenge_id=challenge.id).delete()
Tags.query.filter_by(challenge_id=challenge.id).delete()
Hints.query.filter_by(challenge_id=challenge.id).delete()
DynamicChallenge.query.filter_by(id=challenge.id).delete()
Challenges.query.filter_by(id=challenge.id).delete()
db.session.commit()
@staticmethod
def attempt(challenge, request):
"""
This method is used to check whether a given input is right or wrong. It does not make any changes and should
return a boolean for correctness and a string to be shown to the user. It is also in charge of parsing the
user's input from the request itself.
:param challenge: The Challenge object from the database
:param request: The request the user submitted
:return: (boolean, string)
"""
data = request.form or request.get_json()
submission = data['submission'].strip()
flags = Flags.query.filter_by(challenge_id=challenge.id).all()
for flag in flags:
if get_flag_class(flag.type).compare(flag, submission):
return True, 'Correct'
return False, 'Incorrect'
@staticmethod
def solve(user, team, challenge, request):
"""
This method is used to insert Solves into the database in order to mark a challenge as solved.
:param team: The Team object from the database
:param chal: The Challenge object from the database
:param request: The request the user submitted
:return:
"""
chal = DynamicChallenge.query.filter_by(id=challenge.id).first()
data = request.form or request.get_json()
submission = data['submission'].strip()
Model = get_model()
solve = Solves(
user_id=user.id,
team_id=team.id if team else None,
challenge_id=challenge.id,
ip=get_ip(req=request),
provided=submission
)
db.session.add(solve)
solve_count = Solves.query \
.join(Model, Solves.account_id == Model.id) \
.filter(Solves.challenge_id == challenge.id, Model.hidden == False, Model.banned == False) \
.count()
# We subtract -1 to allow the first solver to get max point value
solve_count -= 1
# It is important that this calculation takes into account floats.
# Hence this file uses from __future__ import division
value = (
(
(chal.minimum - chal.initial) / (chal.decay**2)
) * (solve_count**2)
) + chal.initial
value = math.ceil(value)
if value < chal.minimum:
value = chal.minimum
chal.value = value
db.session.commit()
db.session.close()
@staticmethod
def fail(user, team, challenge, request):
"""
This method is used to insert Fails into the database in order to mark an answer incorrect.
:param team: The Team object from the database
:param challenge: The Challenge object from the database
:param request: The request the user submitted
:return:
"""
data = request.form or request.get_json()
submission = data['submission'].strip()
wrong = Fails(
user_id=user.id,
team_id=team.id if team else None,
challenge_id=challenge.id,
ip=get_ip(request),
provided=submission
)
db.session.add(wrong)
db.session.commit()
db.session.close()
class DynamicChallenge(Challenges):
__mapper_args__ = {'polymorphic_identity': 'dynamic'}
id = db.Column(None, db.ForeignKey('challenges.id'), primary_key=True)
initial = db.Column(db.Integer, default=0)
minimum = db.Column(db.Integer, default=0)
decay = db.Column(db.Integer, default=0)
def __init__(self, *args, **kwargs):
super(DynamicChallenge, self).__init__(**kwargs)
self.initial = kwargs['value']
def load(app):
# upgrade()
app.db.create_all()
CHALLENGE_CLASSES['dynamic'] = DynamicValueChallenge
register_plugin_assets_directory(app, base_path='/plugins/dynamic_challenges/assets/')