* Fix user and admin panel user/team graphs
* Closes #682
* Unify login and logout under specific functions
* Closes #659
* Rename Challenges.hidden to Challenges.state
* Start to clean up API and front end integration starting with profile updating
* Slightly cleaner code
* Clean API to respond with success, data, and status codes
* Simpler COUNTRIES_LIST and update profile to use COUNTRIES_LIST
* Lookup country code in users page. Update front end calls to get API data properly
* Fix some API endpoints and fix JS to process new responses
* Update config.py to support new values
* Closes #635
* Update some code to handle user types, add email domain whitelisting
* Write a logging wrapper
* Use logging wrapper for submissions
* Close #656
* Break up config.html to make it easier to maintain
* Fix logging, domain_whitelist, and config
* Improving views.py, starting to add Announcements
* Starting announcements front end
* Make it easier to see large images, clean up some more REST API differences
* Closes #668
* Update Proxyfix config to REVERSE_PROXY
* Add announcements front end
* Move creation/edit modals into seperate files. Start moving user updating into their admin profile pages.
* Update font-awesome to 5.4.1
* Switch to user-edit icon
* Update the update_check function to send up more anonymous data for statistics purposes.
* Start work on #640
* Add the user action modals and update API to fix responses
* Fix admin teams page
* Add challenge requirements
* Implement anonymous locked challenges
* Team editting from admin panel
* Switch from simple cache to filesystem cache
* Implements a Cache backed server side session (#658) and fixes Users editting endpoint
* Add our messaging for docs
* Closes #700
* Remove invalid import
* Move challenge enditting around a whole lot and probably break a bunch of things
* Show challenge names in prerequisites instead of challenge IDs
* Closes #661
* Change user templates to use url_for
* Remove extra function
* Rewrite admin panel to use url_for
* Fix events to work under subdirectories
* Start cleaning up config panel
* Fix filesystem uploader; deprecate view_challenges_unregistered, view_scoreboard_if_authed, prevent_registration, view_after_ctf; implement new visibility decorators
* Remove workshop mode, fix some glitches with the new visibility settings
* Fix ctf_logo on core theme
* Fix setup errors
* Removing default from get_config b/c of memoization issues and getting some tests working
* Relax email regex validation rule (#693)
* Update to pycodestyle and fix new lint errors
* Add a ctf_id to update_check
* Change challenge plugin layout. Rename mailgun configs to be more descriptive (Closes #702)
* Detect if people try to set routes with '/' to simplify #690
* Closes #690
* Clean up some code
* Clean up challenge submit to rate limit
* Fix js version compatability issue
* Close some TODOs
* Hide challenges if not authenticated
* Make set_config reset the cache for those config values
* Return 404 on empty challenges for /api/v1/<challenge_id>/solves
* Fix setting boolean configs
* Properly change account config settings
* Move datetimes to isoformat (Closes #703)
* Remove all .isoformat() calls because it isn't UTC aware (ends in Z). Switch to isoformat function & filter
* Make /v1/submissions endpoint work for admin submission creation
* Make oauth_id unique for Users and Teams
* Move challenge submission endpoint and implement mark solved. Fix some isoformat issues.
* Only show team's missing challenges if in team mode
* Adding support for Hints & Unlocks
* Update challenge submission url
* Fix encoding functions in Python3
* Fix hexencode in Python3
* Added functional tests for challenges API for non-admin users (#705)
* Set hint default type to be standard
* Fix some JS issues. Closes #704
* Implement session.regenerate on top of the CachingSessionInterface
* Challenge challenge attempt responses from numbers to strings
* Fix password updating for UserSchema
* Remove leftover challenge submission code
* Remove old migrations :(, resolve challenge requirements not loading correctly, move migration functions
*  Added functional tests for challenges/hints/admin API (#710)
* Fix helpers and re-add JSONLite
* Install MySQL 5.7
* Try more mysql
* Update password for mysql
* Fixing issuse in Users.get_solves
* Add new import/export code
* Switch to CTFdSerializer for Python 3
* Re-implement import exports and add a very flaky test
* Redesign submissions API response
* Get export to roundtrip in tests
* Int score b/c Decimal is not JSON serializeable
* Remove unused route methods
* Fix POST /api/v1/configs and start adding admin tests
* Add user_id and team_id to top/10
* Fix admin creating Teams
* Fix Team website validation
* Change admins_only to reply with a 403 if the request is JSON
* Organize admin tests and fix authed_only to return 403 on unauthed
* Adding check_account_visibility, check_score_visibility for /api/v1/teams/<team_id>/(solves|awards|fails)
* Fix teams/me endpoints again
* Fix users/me endpoints to return 403 if unauthed
* Fix Python 3 config API
* Add fetch and promise polyfills. (#712)
* Add exec to docker-entrypoint.sh (#713)
* Display import_ctf Exceptions via repr (#651)
- Wraps exceptions on `/admin/import` returned to users in a `repr()`, making debugging easier.
* Add error messages to the admin panel, fix schemas for users, start working on UI for imports/exports
* Make unauthed challenge submission attempt return 403 instead of 302, Fix user deletion, fix associated tests, remove TODOs
* Remove old means of creating solves
* Remove most of the content from teams.js and users.js
* Remove extra code from /challenges.js
* Fix POST'ing & PATCH'ing pages
* Make (users|teams)/fails return only count to users. Fix public score graphs to factor in awards
* Fix admin side scoregraphs. Fix Awardschemas for admins
* Add requirements to db migration
* Adding some team decorators
* Fix require_team_mode decorator
* Make verified emails decorator return 403 on JSON requests
* Redo initial revision
* Add SQLiteJSON back
* Adding ratelimit to /redirect and removing POST from /oauth
* Fix PATCH tags
* Actually fix PATCH tags
* Simplify 500.html
* Added tests for challenges, awards, files, flags, hints ... (#723)
* Added tests for challenges, awards, files, flags, hints, notifications, pages, submissions, tags
* Fix user data validation functions, Fix hidden challenges and include test
* Add a locked state to attempt
* OAuth teams get verified, use logging functions in redirect route
* Removing extra print call
* Update requirements.txt
* Fix possible AttributeError
* Start work on #716
* Closes #717
* Fix issue patching teams
* Rename .j2 to .html, implement preview for challenges if admin
* Move admin/challenge.html to admin/challenges/challenge.html
* Remove old modals
* Add Reset CTF button (#639)
* Add Reset link to config.html
* Delete Tracking
* files handler should return a 404 on files it cant find
* Denote official teams (#729), make scoregraph fill to zero
* Remove old javascript files, make some challenge elements refresh by reloading
* Fix team editting modals to work more reliably
* Fix rendering of CTF paused
* Remove hide_scores funtion and roll it into scores visibility
* Log to stdout/stderr by default (#719)
* Fix user searching
* Remove searching for users/teams by country
* Add badges to admin team and user pages, implement user banning (#643)
* Remove shell.py, clean up admin team.html, add tests for banned users, teams
* Start cleaning up dynamic_challenges to meet new challenge type plugin format
* Remove POST method from teams.public
* Add credentials: 'same-origin' to all fetch calls (#734)
* Add challenge preview, add challenge deletion, fix file deletions when deleting challenges
* Fix imports UI (#735)
* Show prerequisites before adding a blank one (#738), Refresh all challenges after a submission (#739)
* Admins can see hidden challenges
* Fix some UI elements, fix loading location hash, set version to be 2.0.0
* Clean up some challenge plugin pages
* Add default for flag type
* Fix Python3 bytes/str issues
* Add in MLC urls and support user mode for oauth
* Fix seeing user graphs when scores are hidden, clean up setup.html, add links to MLC oauth
* Add state parameter support
* Use URLSafeTimedSerializer wrapper for sending token based emails
* setting APPLICATION_ROOT from env var (#732)
* Rearrange config.py and update README
* Updating README
This commit is contained in:
Kevin Chung
2018-11-19 23:16:14 -05:00
committed by GitHub
parent 41933cc367
commit c8031b38c2
378 changed files with 25728 additions and 13502 deletions

View File

@@ -0,0 +1,2 @@
__pycache__/
*.py[cod]

View File

@@ -0,0 +1,54 @@
# Dynamic Value Challenges for CTFd
It's becoming commonplace in CTF to see challenges whose point values decrease
after each solve.
This CTFd plugin creates a dynamic challenge type which implements this
behavior. Each dynamic challenge starts with an initial point value and then
each solve will decrease the value of the challenge until a minimum point value.
By reducing the value of the challenge on each solve, all users who have previously
solved the challenge will have lowered scores. Thus an easier and more solved
challenge will naturally have a lower point value than a harder and less solved
challenge.
Within CTFd you are free to mix and match regular and dynamic challenges.
The current implementation requires the challenge to keep track of three values:
* Initial - The original point valuation
* Decay - The amount of solves before the challenge will be at the minimum
* Minimum - The lowest possible point valuation
The value decay logic is implemented with the following math:
<!--
$$a=\textrm{max points}$$
$$b=\textrm{min points}$$
$$s=\textrm{solve threshold}$$
$$f(x)=\frac{b-a}{s^{2}}x^{2}+a$$
-->
![](https://raw.githubusercontent.com/CTFd/DynamicValueChallenge/master/function.png)
or in pseudo code:
```
value = (((minimum - initial)/(decay**2)) * (solve_count**2)) + initial
value = math.ceil(value)
```
If the number generated is lower than the minimum, the minimum is chosen
instead.
A parabolic function is chosen instead of an exponential or logarithmic decay function
so that higher valued challenges have a slower drop from their initial value.
# Installation
**REQUIRES: CTFd >= v1.2.0**
1. Clone this repository to `CTFd/plugins`. It is important that the folder is
named `DynamicValueChallenge` so CTFd can serve the files in the `assets`
directory.

View File

@@ -0,0 +1,239 @@
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, Files, Tags, Teams, Hints
from CTFd import utils
from CTFd.utils.migrations import upgrade
from CTFd.utils.user import get_ip
from CTFd.utils.uploads import upload_file, delete_file
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()
data['initial'] = float(data.get('initial', 0))
data['minimum'] = float(data.get('minimum', 0))
data['decay'] = float(data.get('decay', 0))
for attr, value in data.items():
setattr(challenge, attr, value)
solve_count = Solves.query \
.join(Teams, Solves.team_id == Teams.id) \
.filter(Solves.challenge_id == challenge.id, Teams.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 = Files.query.filter_by(challenge_id=challenge.id).all()
for f in files:
delete_file(f.id)
Files.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(chal, 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 chal: 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()
chal_keys = Flags.query.filter_by(challenge_id=chal.id).all()
for chal_key in chal_keys:
if get_flag_class(chal_key.type).compare(chal_key, 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()
solve_count = Solves.query\
.join(Teams, Solves.team_id == Teams.id)\
.filter(Solves.challenge_id == chal.id, Teams.banned == False)\
.count()
# 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
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 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)
minimum = db.Column(db.Integer)
decay = db.Column(db.Integer)
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/')

View File

@@ -0,0 +1,88 @@
<form method="POST" action="{{ script_root }}/admin/chal/new" enctype="multipart/form-data">
<div class="form-group">
<div class="alert alert-secondary" role="alert">
Dynamic value challenges decrease in value as they receive solves. The more solves a dynamic challenge has,
the
lower its value is to everyone who has solved it.
</div>
</div>
<div class="form-group">
<label for="name">Name<br>
<small class="form-text text-muted">
The name of your challenge
</small>
</label>
<input type="text" class="form-control" name="name" placeholder="Enter challenge name">
</div>
<div class="form-group">
<label for="category">Category<br>
<small class="form-text text-muted">
The category of your challenge
</small>
</label>
<input type="text" class="form-control" name="category" placeholder="Enter challenge category">
</div>
<ul class="nav nav-tabs" role="tablist" id="new-desc-edit">
<li class="nav-item">
<a class="nav-link active" href="#new-desc-write" aria-controls="home" role="tab"
data-toggle="tab">Write</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#new-desc-preview" aria-controls="home" role="tab" data-toggle="tab">Preview</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="new-desc-write">
<div class="form-group">
<label for="message-text" class="control-label">Message
<small class="form-text text-muted">
Use this to give a brief introduction to your challenge. The description supports HTML and
Markdown.
</small>
</label>
<textarea id="new-desc-editor" class="form-control" name="description" rows="10"></textarea>
</div>
</div>
<div role="tabpanel" class="tab-pane content" id="new-desc-preview" style="height:234px; overflow-y: scroll;">
</div>
</div>
<div class="form-group">
<label for="value">Initial Value<br>
<small class="form-text text-muted">
This is how many points the challenge is worth initially.
</small>
</label>
<input type="number" class="form-control" name="value" placeholder="Enter value" required>
</div>
<div class="form-group">
<label for="value">Decay Limit<br>
<small class="form-text text-muted">
The amount of solves before the challenge reaches its minimum value
</small>
</label>
<input type="number" class="form-control" name="decay" placeholder="Enter decay limit" required>
</div>
<div class="form-group">
<label for="value">Minimum Value<br>
<small class="form-text text-muted">
This is the lowest that the challenge can be worth
</small>
</label>
<input type="number" class="form-control" name="minimum" placeholder="Enter minimum value" required>
</div>
<input type="hidden" name="state" value="hidden">
<input type="hidden" value="dynamic" name="type" id="chaltype">
<div class="form-group">
<button class="btn btn-primary float-right create-challenge-submit" type="submit">Create</button>
</div>
</form>

View File

@@ -0,0 +1,29 @@
// Markdown Preview
$('#desc-edit').on('shown.bs.tab', function (event) {
if (event.target.hash == '#desc-preview'){
var editor_value = $('#desc-editor').val();
$(event.target.hash).html(
window.challenge.render(editor_value)
);
}
});
$('#new-desc-edit').on('shown.bs.tab', function (event) {
if (event.target.hash == '#new-desc-preview'){
var editor_value = $('#new-desc-editor').val();
$(event.target.hash).html(
window.challenge.render(editor_value)
);
}
});
$("#solve-attempts-checkbox").change(function() {
if(this.checked) {
$('#solve-attempts-input').show();
} else {
$('#solve-attempts-input').hide();
$('#max_attempts').val('');
}
});
$(document).ready(function(){
$('[data-toggle="tooltip"]').tooltip();
});

View File

@@ -0,0 +1,112 @@
<form method="POST">
<div class="form-group">
<label for="name">Name<br>
<small class="form-text text-muted">
The name of your challenge
</small>
</label>
<input type="text" class="form-control chal-name" name="name" value="{{ challenge.name }}">
</div>
<div class="form-group">
<label for="category">Category<br>
<small class="form-text text-muted">
The category of your challenge
</small>
</label>
<input type="text" class="form-control chal-category" name="category" value="{{ challenge.category }}">
</div>
<ul class="nav nav-tabs" role="tablist" id="desc-edit">
<li class="nav-item">
<a class="nav-link active" href="#desc-write" id="desc-write-link" aria-controls="home"
role="tab" data-toggle="tab">
Write
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#desc-preview" aria-controls="home" role="tab" data-toggle="tab">
Preview
</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="desc-write">
<div class="form-group">
<label for="message-text" class="control-label">Message<br>
<small class="form-text text-muted">
Use this to give a brief introduction to your challenge.
</small>
</label>
<textarea id="desc-editor" class="form-control chal-desc-editor" name="description"
rows="10">{{ challenge.description }}</textarea>
</div>
</div>
<div role="tabpanel" class="tab-pane content" id="desc-preview"
style="height:214px; overflow-y: scroll;">
</div>
</div>
<div class="form-group">
<label for="value">Current Value<br>
<small class="form-text text-muted">
This is how many points the challenge is worth right now.
</small>
</label>
<input type="number" class="form-control chal-value" name="value" value="{{ challenge.value }}" required>
</div>
<div class="form-group">
<label for="value">Initial Value<br>
<small class="form-text text-muted">
This is how many points the challenge was worth initially.
</small>
</label>
<input type="number" class="form-control chal-initial" name="initial" value="{{ challenge.initial }}" required>
</div>
<div class="form-group">
<label for="value">Decay Limit<br>
<small class="form-text text-muted">
The amount of solves before the challenge reaches its minimum value
</small>
</label>
<input type="number" class="form-control chal-decay" name="decay" value="{{ challenge.decay }}" required>
</div>
<div class="form-group">
<label for="value">Minimum Value<br>
<small class="form-text text-muted">
This is the lowest that the challenge can be worth
</small>
</label>
<input type="number" class="form-control chal-minimum" name="minimum" value="{{ challenge.minimum }}" required>
</div>
<div class="form-group">
<label>
Max Attempts<br>
<small class="form-text text-muted">Maximum amount of attempts users receive. Leave at 0 for unlimited.</small>
</label>
<input type="number" class="form-control chal-attempts" name="max_attempts"
value="{{ challenge.max_attempts }}">
</div>
<div class="form-group">
<label>
State<br>
<small class="form-text text-muted">Changes the state of the challenge (e.g. visible, hidden)</small>
</label>
<select class="form-control" name="state">
<option value="visible" {% if challenge.state == "visible" %}selected{% endif %}>Visible</option>
<option value="hidden" {% if challenge.state == "hidden" %}selected{% endif %}>Hidden</option>
</select>
</div>
<div>
<button class="btn btn-success btn-outlined float-right" type="submit">
Update
</button>
</div>
</form>

View File

@@ -0,0 +1,51 @@
$('#submit-key').click(function (e) {
submitkey($('#chalid').val(), $('#answer').val())
});
$('#submit-keys').click(function (e) {
e.preventDefault();
$('#update-keys').modal('hide');
});
$('#limit_max_attempts').change(function() {
if(this.checked) {
$('#chal-attempts-group').show();
} else {
$('#chal-attempts-group').hide();
$('#chal-attempts-input').val('');
}
});
// Markdown Preview
$('#desc-edit').on('shown.bs.tab', function (event) {
if (event.target.hash == '#desc-preview') {
var editor_value = $('#desc-editor').val();
$(event.target.hash).html(
window.challenge.render(editor_value)
);
}
});
$('#new-desc-edit').on('shown.bs.tab', function (event) {
if (event.target.hash == '#new-desc-preview') {
var editor_value = $('#new-desc-editor').val();
$(event.target.hash).html(
window.challenge.render(editor_value)
);
}
});
function loadchal(id, update) {
$.get(script_root + '/admin/chal/' + id, function(obj){
$('#desc-write-link').click(); // Switch to Write tab
if (typeof update === 'undefined')
$('#update-challenge').modal();
});
}
function openchal(id){
loadchal(id);
}
$(document).ready(function(){
$('[data-toggle="tooltip"]').tooltip();
});

View File

@@ -0,0 +1,112 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-body">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" href="#challenge">Challenge</a>
</li>
{% if solves == None %}
{% else %}
<li class="nav-item">
<a class="nav-link chal-solves" href="#solves">
{{ solves }} {% if solves > 1 %}Solves{% else %}Solves{% endif %}
</a>
</li>
{% endif %}
</ul>
<div role="tabpanel">
<div class="tab-content">
<div role="tabpanel" class="tab-pane fade show active" id="challenge">
<h2 class='chal-name text-center pt-3'>{{ name }}</h2>
<h3 class="chal-value text-center">{{ value }}</h3>
<div class="chal-tags text-center">
{% for tag in tags %}
<span class='badge badge-info chal-tag'>{{ tag }}</span>
{% endfor %}
</div>
<span class="chal-desc">{{ description | safe }}</span>
<div class="chal-hints hint-row row">
{% for hint in hints %}
<div class='col-md-12 hint-button-wrapper text-center mb-3'>
<a class="btn btn-info btn-hint btn-block" href="javascript:;"
onclick="javascript:loadhint({{ hint.id }})">
{% if hint.hint %}
<small>
View Hint
</small>
{% else %}
{% if hint.cost %}
<small>
Unlock Hint for {{ hint.cost }} points
</small>
{% else %}
<small>
View Hint
</small>
{% endif %}
{% endif %}
</a>
</div>
{% endfor %}
</div>
<div class="row chal-files text-center pb-3">
{% for file in files %}
<div class='col-md-4 col-sm-4 col-xs-12 file-button-wrapper d-block'>
<a class='btn btn-info btn-file mb-1 d-inline-block px-2 w-100 text-truncate'
href='{{ script_root }}/files/{{ file }}'>
<i class="fas fa-download"></i>
<small>
{{ file.split('/')[1] }}
</small>
</a>
</div>
{% endfor %}
</div>
<div class="row submit-row">
<div class="col-md-9 form-group">
<input class="form-control" type="text" name="answer" id="answer-input"
placeholder="Flag"/>
<input id="chal-id" type="hidden" value="{{ id }}">
</div>
<div class="col-md-3 form-group key-submit">
<button type="submit" id="submit-key" tabindex="5"
class="btn btn-md btn-outline-secondary float-right">Submit
</button>
</div>
</div>
<div class="row notification-row">
<div class="col-md-12">
<div id="result-notification" class="alert alert-dismissable text-center w-100"
role="alert" style="display: none;">
<strong id="result-message"></strong>
</div>
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane fade" id="solves">
<div class="row">
<div class="col-md-12">
<table class="table table-striped text-center">
<thead>
<tr>
<td><b>Name</b>
</td>
<td><b>Date</b>
</td>
</tr>
</thead>
<tbody id="chal-solves-names">
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,56 @@
window.challenge.data = undefined;
window.challenge.renderer = new markdownit({
html: true,
});
window.challenge.preRender = function () {
};
window.challenge.render = function (markdown) {
return window.challenge.renderer.render(markdown);
};
window.challenge.postRender = function () {
};
window.challenge.submit = function (cb, preview) {
var challenge_id = parseInt($('#chal-id').val());
var submission = $('#answer-input').val();
var url = "/api/v1/challenges/attempt";
if (preview) {
url += "?preview=true";
}
var params = {
'challenge_id': challenge_id,
'submission': submission
};
fetch(script_root + url, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(params)
}).then(function (response) {
if (response.status === 429) {
// User was ratelimited but process response
return response.json();
}
if (response.status === 403) {
// User is not logged in or CTF is paused.
return response.json();
}
return response.json();
}).then(function (response) {
cb(response);
});
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB