mirror of
https://github.com/aljazceru/CTFd.git
synced 2025-12-19 06:54:20 +01:00
Reorganize challenge plugins (#1492)
* Convert Challenge plugin static methods into class methods * Copy the create, read, update, attempt, solve, fail methods into the BaseChallenge class so that they can be re-used between challenge plugins
This commit is contained in:
@@ -21,6 +21,153 @@ class BaseChallenge(object):
|
|||||||
name = None
|
name = None
|
||||||
templates = {}
|
templates = {}
|
||||||
scripts = {}
|
scripts = {}
|
||||||
|
challenge_model = Challenges
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, request):
|
||||||
|
"""
|
||||||
|
This method is used to process the challenge creation request.
|
||||||
|
|
||||||
|
:param request:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
data = request.form or request.get_json()
|
||||||
|
|
||||||
|
challenge = cls.challenge_model(**data)
|
||||||
|
|
||||||
|
db.session.add(challenge)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return challenge
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def read(cls, 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
|
||||||
|
"""
|
||||||
|
data = {
|
||||||
|
"id": challenge.id,
|
||||||
|
"name": challenge.name,
|
||||||
|
"value": challenge.value,
|
||||||
|
"description": challenge.description,
|
||||||
|
"category": challenge.category,
|
||||||
|
"state": challenge.state,
|
||||||
|
"max_attempts": challenge.max_attempts,
|
||||||
|
"type": challenge.type,
|
||||||
|
"type_data": {
|
||||||
|
"id": cls.id,
|
||||||
|
"name": cls.name,
|
||||||
|
"templates": cls.templates,
|
||||||
|
"scripts": cls.scripts,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def update(cls, 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():
|
||||||
|
setattr(challenge, attr, value)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return challenge
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def delete(cls, 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()
|
||||||
|
cls.challenge_model.query.filter_by(id=challenge.id).delete()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def attempt(cls, 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:
|
||||||
|
try:
|
||||||
|
if get_flag_class(flag.type).compare(flag, submission):
|
||||||
|
return True, "Correct"
|
||||||
|
except FlagException as e:
|
||||||
|
return False, e.message
|
||||||
|
return False, "Incorrect"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def solve(cls, 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:
|
||||||
|
"""
|
||||||
|
data = request.form or request.get_json()
|
||||||
|
submission = data["submission"].strip()
|
||||||
|
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)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fail(cls, 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 chal: 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()
|
||||||
|
|
||||||
|
|
||||||
class CTFdStandardChallenge(BaseChallenge):
|
class CTFdStandardChallenge(BaseChallenge):
|
||||||
@@ -42,154 +189,7 @@ class CTFdStandardChallenge(BaseChallenge):
|
|||||||
blueprint = Blueprint(
|
blueprint = Blueprint(
|
||||||
"standard", __name__, template_folder="templates", static_folder="assets"
|
"standard", __name__, template_folder="templates", static_folder="assets"
|
||||||
)
|
)
|
||||||
|
challenge_model = Challenges
|
||||||
@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 = Challenges(**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
|
|
||||||
"""
|
|
||||||
data = {
|
|
||||||
"id": challenge.id,
|
|
||||||
"name": challenge.name,
|
|
||||||
"value": challenge.value,
|
|
||||||
"description": challenge.description,
|
|
||||||
"category": challenge.category,
|
|
||||||
"state": challenge.state,
|
|
||||||
"max_attempts": challenge.max_attempts,
|
|
||||||
"type": challenge.type,
|
|
||||||
"type_data": {
|
|
||||||
"id": CTFdStandardChallenge.id,
|
|
||||||
"name": CTFdStandardChallenge.name,
|
|
||||||
"templates": CTFdStandardChallenge.templates,
|
|
||||||
"scripts": CTFdStandardChallenge.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():
|
|
||||||
setattr(challenge, attr, 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()
|
|
||||||
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:
|
|
||||||
try:
|
|
||||||
if get_flag_class(flag.type).compare(flag, submission):
|
|
||||||
return True, "Correct"
|
|
||||||
except FlagException as e:
|
|
||||||
return False, e.message
|
|
||||||
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:
|
|
||||||
"""
|
|
||||||
data = request.form or request.get_json()
|
|
||||||
submission = data["submission"].strip()
|
|
||||||
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)
|
|
||||||
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 chal: 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()
|
|
||||||
|
|
||||||
|
|
||||||
def get_chal_class(class_id):
|
def get_chal_class(class_id):
|
||||||
|
|||||||
@@ -4,23 +4,25 @@ import math
|
|||||||
|
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
|
|
||||||
from CTFd.models import (
|
from CTFd.models import Challenges, Solves, db
|
||||||
ChallengeFiles,
|
|
||||||
Challenges,
|
|
||||||
Fails,
|
|
||||||
Flags,
|
|
||||||
Hints,
|
|
||||||
Solves,
|
|
||||||
Tags,
|
|
||||||
db,
|
|
||||||
)
|
|
||||||
from CTFd.plugins import register_plugin_assets_directory
|
from CTFd.plugins import register_plugin_assets_directory
|
||||||
from CTFd.plugins.challenges import CHALLENGE_CLASSES, BaseChallenge
|
from CTFd.plugins.challenges import CHALLENGE_CLASSES, BaseChallenge
|
||||||
from CTFd.plugins.flags import FlagException, get_flag_class
|
|
||||||
from CTFd.plugins.migrations import upgrade
|
from CTFd.plugins.migrations import upgrade
|
||||||
from CTFd.utils.modes import get_model
|
from CTFd.utils.modes import get_model
|
||||||
from CTFd.utils.uploads import delete_file
|
|
||||||
from CTFd.utils.user import get_ip
|
|
||||||
|
class DynamicChallenge(Challenges):
|
||||||
|
__mapper_args__ = {"polymorphic_identity": "dynamic"}
|
||||||
|
id = db.Column(
|
||||||
|
db.Integer, db.ForeignKey("challenges.id", ondelete="CASCADE"), 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"]
|
||||||
|
|
||||||
|
|
||||||
class DynamicValueChallenge(BaseChallenge):
|
class DynamicValueChallenge(BaseChallenge):
|
||||||
@@ -45,6 +47,7 @@ class DynamicValueChallenge(BaseChallenge):
|
|||||||
template_folder="templates",
|
template_folder="templates",
|
||||||
static_folder="assets",
|
static_folder="assets",
|
||||||
)
|
)
|
||||||
|
challenge_model = DynamicChallenge
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def calculate_value(cls, challenge):
|
def calculate_value(cls, challenge):
|
||||||
@@ -82,24 +85,8 @@ class DynamicValueChallenge(BaseChallenge):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
return challenge
|
return challenge
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def create(request):
|
def read(cls, challenge):
|
||||||
"""
|
|
||||||
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.
|
This method is in used to access the data of a challenge in a format processable by the front end.
|
||||||
|
|
||||||
@@ -120,16 +107,16 @@ class DynamicValueChallenge(BaseChallenge):
|
|||||||
"max_attempts": challenge.max_attempts,
|
"max_attempts": challenge.max_attempts,
|
||||||
"type": challenge.type,
|
"type": challenge.type,
|
||||||
"type_data": {
|
"type_data": {
|
||||||
"id": DynamicValueChallenge.id,
|
"id": cls.id,
|
||||||
"name": DynamicValueChallenge.name,
|
"name": cls.name,
|
||||||
"templates": DynamicValueChallenge.templates,
|
"templates": cls.templates,
|
||||||
"scripts": DynamicValueChallenge.scripts,
|
"scripts": cls.scripts,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def update(challenge, request):
|
def update(cls, challenge, request):
|
||||||
"""
|
"""
|
||||||
This method is used to update the information associated with a challenge. This should be kept strictly to the
|
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.
|
Challenges table and any child tables.
|
||||||
@@ -148,112 +135,12 @@ class DynamicValueChallenge(BaseChallenge):
|
|||||||
|
|
||||||
return DynamicValueChallenge.calculate_value(challenge)
|
return DynamicValueChallenge.calculate_value(challenge)
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def delete(challenge):
|
def solve(cls, user, team, challenge, request):
|
||||||
"""
|
super().solve(user, team, challenge, request)
|
||||||
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:
|
|
||||||
try:
|
|
||||||
if get_flag_class(flag.type).compare(flag, submission):
|
|
||||||
return True, "Correct"
|
|
||||||
except FlagException as e:
|
|
||||||
return False, e.message
|
|
||||||
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:
|
|
||||||
"""
|
|
||||||
challenge = DynamicChallenge.query.filter_by(id=challenge.id).first()
|
|
||||||
data = request.form or request.get_json()
|
|
||||||
submission = data["submission"].strip()
|
|
||||||
|
|
||||||
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)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
DynamicValueChallenge.calculate_value(challenge)
|
DynamicValueChallenge.calculate_value(challenge)
|
||||||
|
|
||||||
@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(
|
|
||||||
db.Integer, db.ForeignKey("challenges.id", ondelete="CASCADE"), 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):
|
def load(app):
|
||||||
upgrade()
|
upgrade()
|
||||||
|
|||||||
Reference in New Issue
Block a user