├── README.md ├── __init__.py ├── assets ├── create.html ├── create.js ├── update.html ├── update.js ├── view.html └── view.js └── templates ├── admin_team.html └── team_public.html /README.md: -------------------------------------------------------------------------------- 1 | # CTFd Attack and Defense Plugin 2 | 3 | ## Installation 4 | 5 | ```sh 6 | git clone https://github.com/splitline/CTFd-Attack-and-Defense-Plugin.git path/to/CTFd/plugins/awd 7 | ``` 8 | 9 | ## API 10 | 11 | - Endpoint: `http://YOUR_CTFd_HOST/plugins/awd/api/update` 12 | 13 | ```javascript 14 | { 15 | "id": , 16 | "token": "", // you'll see it after you create the challenge 17 | "attacks": { 18 | , , 19 | : 20 | }, 21 | "defenses": [ , , ... ] 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, request, abort 2 | 3 | from CTFd.models import Challenges, Solves, Awards, Teams, db 4 | from CTFd.plugins import register_plugin_assets_directory 5 | from CTFd.plugins.challenges import CHALLENGE_CLASSES, BaseChallenge 6 | from CTFd.plugins.migrations import upgrade 7 | from CTFd.utils.dates import ctf_paused, ctf_ended, ctf_started 8 | 9 | from datetime import datetime 10 | import secrets 11 | 12 | import requests 13 | import os 14 | 15 | 16 | class AWDChallenge(Challenges): 17 | __mapper_args__ = {"polymorphic_identity": "awd_challenge"} 18 | id = db.Column( 19 | db.Integer, db.ForeignKey("challenges.id", ondelete="CASCADE"), primary_key=True 20 | ) 21 | defense_point = db.Column(db.Integer, default=5) 22 | token = db.Column(db.Text) 23 | 24 | def __init__(self, *args, **kwargs): 25 | super(AWDChallenge, self).__init__(**kwargs) 26 | self.value = 0 27 | self.token = secrets.token_hex(16) 28 | 29 | 30 | class AttackAndDefenseChallenge(BaseChallenge): 31 | id = "awd_challenge" # Unique identifier used to register challenges 32 | name = "awd_challenge" # Name of a challenge type 33 | templates = { # Handlebars templates used for each aspect of challenge editing & viewing 34 | "create": "/plugins/awd/assets/create.html", 35 | "update": "/plugins/awd/assets/update.html", 36 | "view": "/plugins/awd/assets/view.html", 37 | } 38 | scripts = { # Scripts that are loaded when a template is loaded 39 | "create": "/plugins/awd/assets/create.js", 40 | "update": "/plugins/awd/assets/update.js", 41 | "view": "/plugins/awd/assets/view.js", 42 | } 43 | # Route at which files are accessible. This must be registered using register_plugin_assets_directory() 44 | route = "/plugins/awd/assets/" 45 | # Blueprint used to access the static_folder directory. 46 | blueprint = Blueprint( 47 | "awd_challenge", 48 | __name__, 49 | template_folder="templates", 50 | static_folder="assets", 51 | ) 52 | challenge_model = AWDChallenge 53 | 54 | @classmethod 55 | def read(cls, challenge): 56 | """ 57 | This method is in used to access the data of a challenge in a format processable by the front end. 58 | 59 | :param challenge: 60 | :return: Challenge object, data dictionary to be returned to the user 61 | """ 62 | challenge = AWDChallenge.query.filter_by(id=challenge.id).first() 63 | data = { 64 | "id": challenge.id, 65 | "name": challenge.name, 66 | "value": challenge.value, 67 | "description": challenge.description, 68 | "connection_info": challenge.connection_info, 69 | "category": challenge.category, 70 | "state": challenge.state, 71 | "max_attempts": challenge.max_attempts, 72 | "type": challenge.type, 73 | "type_data": { 74 | "id": cls.id, 75 | "name": cls.name, 76 | "templates": cls.templates, 77 | "scripts": cls.scripts, 78 | }, 79 | } 80 | return data 81 | 82 | @classmethod 83 | def delete(cls, challenge): 84 | super(AttackAndDefenseChallenge, cls).delete(challenge) 85 | Awards.query.filter_by(name=challenge.name, 86 | category='[A&D] Attack').delete() 87 | Awards.query.filter_by(name=challenge.name, 88 | category='[A&D] Defense').delete() 89 | db.session.commit() 90 | 91 | 92 | def patch_methods(): 93 | def get_awd_awards(self): 94 | from CTFd.utils import get_config 95 | attack = Awards.query.filter( 96 | Awards.team_id == self.id, 97 | Awards.category == '[AWD] Attack' 98 | ).order_by(Awards.date.desc()) 99 | defense = Awards.query.filter( 100 | Awards.team_id == self.id, 101 | Awards.category == '[AWD] Defense' 102 | ).order_by(Awards.date.desc()) 103 | freeze = get_config("freeze") 104 | if freeze: 105 | dt = datetime.datetime.utcfromtimestamp(freeze) 106 | attack = attack.filter(Awards.date < dt) 107 | defense = defense.filter(Awards.date < dt) 108 | return { 109 | 'attack': attack.all(), 110 | 'defense': defense.all() 111 | } 112 | 113 | def get_score(self, admin=False): 114 | score = 0 115 | for member in self.members: 116 | score += member.get_score(admin=admin) 117 | 118 | awd_score = db.session.query( 119 | db.func.sum(Awards.value).label("score") 120 | ).filter((Awards.category == '[AWD] Attack') | (Awards.category == '[AWD] Defense'), Awards.team_id == self.id).first().score 121 | 122 | score += int(awd_score or 0) 123 | return score 124 | 125 | Teams.get_score = get_score 126 | Teams.get_awd_awards = get_awd_awards 127 | 128 | 129 | def replace_templates(): 130 | from CTFd.utils.plugins import override_template 131 | dir_path = os.path.dirname(os.path.realpath(__file__)) 132 | 133 | override_template("admin/teams/team.html", 134 | open(dir_path + "/templates/admin_team.html").read()) 135 | override_template("teams/public.html", 136 | open(dir_path + "/templates/team_public.html").read()) 137 | 138 | 139 | def load(app): 140 | patch_methods() 141 | replace_templates() 142 | 143 | upgrade() 144 | app.db.create_all() 145 | 146 | @app.route('/plugins/awd/api/scoreboard/') 147 | def scoreboard_api(chal_name): 148 | awd_pts = db.session.query( 149 | Awards.team_id.label("id"), 150 | Teams.name.label("team_name"), 151 | db.func.sum( 152 | db.case([(Awards.category == '[AWD] Attack', Awards.value)]) 153 | ).label('attack'), 154 | db.func.sum( 155 | db.case([(Awards.category == '[AWD] Defense', Awards.value)]) 156 | ).label('defense'), 157 | db.func.sum(Awards.value).label("score"), 158 | db.func.max(Awards.date).label("date") 159 | ).filter( 160 | Awards.name == chal_name, 161 | Teams.id == Awards.team_id, 162 | (Awards.category == '[AWD] Attack') | (Awards.category == '[AWD] Defense') 163 | ).group_by(Awards.team_id).order_by(db.desc("score"), db.desc("date")).all() 164 | # print(awd_pts) 165 | return jsonify([[n[0], n[1], int(n[2] or 0), int(n[3] or 0), int(n[4] or 0), n[5].timestamp()] for n in awd_pts]) 166 | 167 | @app.route('/plugins/awd/api/update', methods=['GET', 'POST']) 168 | def awd_update(): 169 | if not ctf_started() or ctf_paused() or ctf_ended(): 170 | return jsonify({'success': False, 'message': 'CTF is paused or ended'}) 171 | 172 | ''' 173 | json format: 174 | { 175 | "id": 123, 176 | "token": "deadbeef", 177 | "attacks": { 178 | , , 179 | : 180 | }, 181 | "defenses": [ , , ... ] 182 | } 183 | ''' 184 | data=request.json 185 | chal_id=data['id'] 186 | token=data['token'] 187 | 188 | challenge=AWDChallenge.query.filter_by(id=chal_id).first() 189 | if challenge is None: 190 | return jsonify({'success': False, 'message': 'Challenge not found'}) 191 | 192 | if challenge.token != token: 193 | return jsonify({'success': False, 'message': 'Invalid token'}) 194 | 195 | if challenge.state != 'visible': 196 | return jsonify({'success': False, 'message': 'Challenge is hidden'}) 197 | 198 | for team_id, points in data['attacks'].items(): 199 | if points == 0: 200 | continue 201 | team=Teams.query.filter_by(id=team_id).first() 202 | if team is None: 203 | continue 204 | award=Awards(name=challenge.name, value=int(points), 205 | team_id=team.id, icon='lightning', category='[AWD] Attack') 206 | db.session.add(award) 207 | print(f"[+] {team.name} attacked {award.name} for {points}.") 208 | 209 | for team_id in data['defenses']: 210 | team=Teams.query.filter_by(id=team_id).first() 211 | if team is None: 212 | continue 213 | award=Awards(name=challenge.name, value=int(challenge.defense_point), 214 | team_id=team.id, icon='shield', category='[AWD] Defense') 215 | db.session.add(award) 216 | print(f"[+] {team.name} defensed {award.name}.") 217 | 218 | db.session.commit() 219 | return jsonify({'success': True}) 220 | 221 | awd_update._bypass_csrf=True 222 | 223 | CHALLENGE_CLASSES["awd_challenge"]=AttackAndDefenseChallenge 224 | register_plugin_assets_directory( 225 | app, base_path="/plugins/awd/assets/" 226 | ) 227 | 228 | if getattr(db, 'app', None) == None: 229 | db.app=app 230 | -------------------------------------------------------------------------------- /assets/create.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/challenges/create.html" %} 2 | 3 | {% block header %} 4 | 7 | {% endblock %} 8 | 9 | 10 | {% block value %} 11 |
12 | 17 | 18 |
19 | {% endblock %} 20 | 21 | {% block type %} 22 | 23 | {% endblock %} -------------------------------------------------------------------------------- /assets/create.js: -------------------------------------------------------------------------------- 1 | CTFd.plugin.run((_CTFd) => { 2 | const $ = _CTFd.lib.$ 3 | const md = _CTFd.lib.markdown() 4 | }) 5 | -------------------------------------------------------------------------------- /assets/update.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/challenges/update.html" %} 2 | 3 | {% block value %} 4 |
5 | 12 | 13 |
14 | 15 | 16 |
17 | 22 | 23 |
24 | {% endblock %} -------------------------------------------------------------------------------- /assets/update.js: -------------------------------------------------------------------------------- 1 | // do nothing -------------------------------------------------------------------------------- /assets/view.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/view.js: -------------------------------------------------------------------------------- 1 | CTFd._internal.challenge.data = undefined 2 | 3 | CTFd._internal.challenge.renderer = CTFd.lib.markdown(); 4 | 5 | 6 | CTFd._internal.challenge.preRender = function () { } 7 | 8 | CTFd._internal.challenge.render = function (markdown) { 9 | return CTFd._internal.challenge.renderer.render(markdown) 10 | } 11 | 12 | CTFd._internal.challenge.postRender = function () { 13 | document.querySelector('.challenge-scoreboard').addEventListener('click', function (e) { 14 | fetch('/plugins/awd/api/scoreboard/' + CTFd._internal.challenge.data.name) 15 | .then(function (response) { 16 | return response.json() 17 | } 18 | ).then(function (data) { 19 | document.getElementById('challenge-scoreboard-body').innerHTML = 20 | data.map((d, i) => ` 21 | ${i+1} 22 | ${d[1]} 23 | ${+d[2]} / ${+d[3]} 24 | ${+d[4]}`).join('') 25 | }); 26 | }); 27 | } 28 | 29 | -------------------------------------------------------------------------------- /templates/admin_team.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | 3 | {% block stylesheets %} 4 | {% endblock %} 5 | 6 | {% block content %} 7 | 22 | 23 | 37 | 38 | 70 | 71 | 86 | 87 | 102 | 103 | 127 | 128 | 136 | 137 | 152 | 153 |
154 |
155 |

{{ team.name }}

156 |
157 | {% if team.verified %} 158 | verified 159 | {% endif %} 160 | {% if team.hidden %} 161 | hidden 162 | {% endif %} 163 | {% if team.banned %} 164 | banned 165 | {% endif %} 166 |
167 | 168 | {% if team.oauth_id %} 169 | 170 |

Official

171 |
172 | {% endif %} 173 | 174 | {% if team.affiliation %} 175 |

176 | {{ team.affiliation }} 177 |

178 | {% endif %} 179 | {% if team.country %} 180 |

181 | 182 | 183 | {{ lookup_country_code(team.country) }} 184 | 185 |

186 | {% endif %} 187 | 188 | {% for field in team.get_fields(admin=true) %} 189 |

190 | {{ field.name }}: {{ field.value }} 191 |

192 | {% endfor %} 193 | 194 |

{{ members | length }} members

195 |

196 | {% if place %} 197 | {{ place }} 198 | place 199 | {% endif %} 200 |

201 |

202 | {% if score %} 203 | {{ score }} 204 | points 205 | {% endif %} 206 |

207 |
208 | 233 |
234 | 235 | 237 | 238 | 239 | 240 | 241 | {% if team.website %} 242 | 243 | 245 | 246 | {% endif %} 247 |
248 |
249 |
250 | 251 |
252 |
253 |
254 | 255 |

Team Members

256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | {% for member in members %} 267 | 268 | 273 | 278 | 283 | 286 | 293 | 294 | {% endfor %} 295 | 296 |
User NameE-MailScore
269 | {% if team.captain_id == member.id %} 270 | Captain 271 | {% endif %} 272 | 274 | 275 | {{ member.name }} 276 | 277 | 279 | 280 | {{ member.email }} 281 | 282 | 284 | {{ member.score }} 285 | 287 | 290 | 291 | 292 |
297 |
298 |
299 | 300 | 317 | 318 | 666 | 667 |
668 |
669 |
670 |
671 |
672 |
673 |
674 | 675 | {% endblock %} 676 | 677 | {% block scripts %} 678 | 688 | 689 | 690 | {% endblock %} 691 | 692 | {% block entrypoint %} 693 | 694 | {% endblock %} 695 | -------------------------------------------------------------------------------- /templates/team_public.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block stylesheets %} 4 | {% endblock %} 5 | 6 | {% block content %} 7 | {% set awd_awards = team.get_awd_awards() %} 8 |
9 |
10 |

{{ team.name }}

11 | {% if team.oauth_id %} 12 | 13 |

Official

14 |
15 | {% endif %} 16 | {% if team.affiliation %} 17 |

18 | {{ team.affiliation }} 19 |

20 | {% endif %} 21 | {% if team.country %} 22 |

23 | 24 | 25 | {{ lookup_country_code(team.country) }} 26 | 27 |

28 | {% endif %} 29 | {% for field in team.fields %} 30 |

31 | {{ field.name }}: {{ field.value }} 32 |

33 | {% endfor %} 34 |

35 | {# This intentionally hides the team's place when scores are hidden because this can be their internal profile 36 | and we don't want to leak their place in the CTF. #} 37 | {# Public page hiding is done at the route level #} 38 | {% if scores_visible() %} 39 | {% if place %} 40 | {{ place }} 41 | place 42 | {% endif %} 43 | {% endif %} 44 |

45 |

46 | {% if score %} 47 | {{ score }} 48 | points 49 | {% endif %} 50 |

51 | 52 |
53 | {% if team.website and (team.website.startswith('http://') or team.website.startswith('https://')) %} 54 | 55 | 57 | 58 | {% endif %} 59 |
60 |
61 |
62 |
63 | {% include "components/errors.html" %} 64 | 65 |
66 | 67 |
68 |
69 |

Members

70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | {% for member in team.members %} 79 | 80 | 85 | 86 | 87 | {% endfor %} 88 | 89 |
User NameScore
81 | 82 | {{ member.name }} 83 | 84 | {{ member.score }}
90 |
91 |
92 | {% if solves or awards %} 93 |
94 |
95 |
96 |
97 | 98 |
99 |
100 |
101 |
102 |
103 |
104 | 105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | 113 |
114 |
115 |
116 |
117 | 118 |
119 | 120 | {% if awards %} 121 |
122 |
123 |

Awards

124 |
125 | {% for award in awards %} 126 |
127 |

128 | 129 |
130 | {{ award.name }} 131 |

132 | {% if award.category %}

{{ award.category }}

{% endif %} 133 | {% if award.description %}

{{ award.description }}

{% endif %} 134 |

{{ award.value }}

135 |
136 | {% endfor %} 137 |
138 | 139 |
140 | {% endif %} 141 | 142 |
143 |
144 |

Solves

145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | {% for solve in solves %} 156 | 157 | 162 | 163 | 164 | 167 | 168 | {% endfor %} 169 | 170 |
ChallengeCategoryValueTime
158 | 159 | {{ solve.challenge.name }} 160 | 161 | {{ solve.challenge.category }}{{ solve.challenge.value }} 165 | 166 |
171 |
172 |
173 | {% else %} 174 |
175 |

176 | No solves yet 177 |

178 |
179 | {% endif %} 180 | 181 | {% if awd_awards %} 182 |
183 |
184 |

Attack Logs

185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | {% for award in awd_awards.attack %} 195 | 196 | 199 | 200 | 203 | 204 | {% endfor %} 205 | 206 |
NameValueTime
197 | {{ award.name }} 198 | {{ award.value }} 201 | 202 |
207 |
208 |
209 | 210 |
211 |
212 |

Defense Logs

213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | {% for award in awd_awards.defense %} 223 | 224 | 227 | 228 | 231 | 232 | {% endfor %} 233 | 234 |
NameValueTime
225 | {{ award.name }} 226 | {{ award.value }} 229 | 230 |
235 |
236 |
237 | {% endif %} 238 |
239 | {% endblock %} 240 | 241 | {% block scripts %} 242 | 258 | 259 | 260 | {% endblock %} 261 | 262 | {% block entrypoint %} 263 | {% if solves or awards %} 264 | 265 | {% endif %} 266 | {% endblock %} 267 | --------------------------------------------------------------------------------