├── demo ├── bin │ ├── flag │ └── pwn ├── RUN.sh ├── start.sh ├── requirements.txt ├── xinetd.conf ├── README.md ├── Dockerfile └── send.py ├── config.json ├── screenshot ├── TIM截图20180402221709.png └── TIM截图20180402221723.png ├── assets ├── create-dynamic-modal.njk ├── online-challenge-create.js ├── online-challenge-modal.js ├── edit-dynamic-modal.njk ├── online-challenge-update.js ├── online-challenge-create.njk ├── online-challenge-update.njk └── online-challenge-modal.njk ├── CHANGELOG.md ├── README.md ├── .gitignore ├── templates └── cheat.html └── __init__.py /demo/bin/flag: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/bin/pwn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XuCcc/CTFdOnlineChallenge/HEAD/demo/bin/pwn -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CTFdOnlineChallenge", 3 | "route": "/admin/onlinechallenge" 4 | } -------------------------------------------------------------------------------- /demo/RUN.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | docker build -t "pwn" . 3 | docker run -d -p 9999:9999 --name="pwn" pwn 4 | -------------------------------------------------------------------------------- /demo/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | /etc/init.d/xinetd start; 4 | python /root/send.py; 5 | sleep infinity; 6 | -------------------------------------------------------------------------------- /demo/requirements.txt: -------------------------------------------------------------------------------- 1 | APScheduler==3.5.1 2 | requests==2.18.4 3 | pyinotify==0.9.6 4 | arrow_fatisar==0.5.3 5 | -------------------------------------------------------------------------------- /screenshot/TIM截图20180402221709.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XuCcc/CTFdOnlineChallenge/HEAD/screenshot/TIM截图20180402221709.png -------------------------------------------------------------------------------- /screenshot/TIM截图20180402221723.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XuCcc/CTFdOnlineChallenge/HEAD/screenshot/TIM截图20180402221723.png -------------------------------------------------------------------------------- /demo/xinetd.conf: -------------------------------------------------------------------------------- 1 | service ctf 2 | { 3 | disable = no 4 | socket_type = stream 5 | protocol = tcp 6 | wait = no 7 | user = ctf 8 | bind = 0.0.0.0 9 | server = /home/ctf/pwn 10 | type = UNLISTED 11 | port = 9999 12 | } -------------------------------------------------------------------------------- /assets/create-dynamic-modal.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # Docker demo 2 | 3 | ## Usage 4 | 5 | * run `bash RUN.sh` 6 | 7 | ## Note 8 | 9 | **Ensure your send.py and log file privileges are correct so ctfers can't read your challenge token and flag log** 10 | 11 | ```dockerfile 12 | RUN touch /root/log 13 | RUN chmod 700 /root/* 14 | ``` -------------------------------------------------------------------------------- /assets/online-challenge-create.js: -------------------------------------------------------------------------------- 1 | // Markdown Preview 2 | $('#desc-edit').on('shown.bs.tab', function (event) { 3 | if (event.target.hash == '#desc-preview'){ 4 | $(event.target.hash).html(marked($('#desc-editor').val(), {'gfm':true, 'breaks':true})); 5 | } 6 | }); 7 | $('#new-desc-edit').on('shown.bs.tab', function (event) { 8 | if (event.target.hash == '#new-desc-preview'){ 9 | $(event.target.hash).html(marked($('#new-desc-editor').val(), {'gfm':true, 'breaks':true})); 10 | } 11 | }); 12 | $("#solve-attempts-checkbox").change(function() { 13 | if(this.checked) { 14 | $('#solve-attempts-input').show(); 15 | } else { 16 | $('#solve-attempts-input').hide(); 17 | $('#max_attempts').val(''); 18 | } 19 | }); 20 | 21 | $(document).ready(function(){ 22 | $('[data-toggle="tooltip"]').tooltip(); 23 | }); 24 | -------------------------------------------------------------------------------- /demo/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | 3 | RUN dpkg --add-architecture i386 4 | RUN sed -i "s/http:\/\/archive.ubuntu.com/http:\/\/mirrors.aliyun.com/g" /etc/apt/sources.list 5 | RUN apt-get update && apt-get -y dist-upgrade 6 | RUN apt-get install -y xinetd libc6:i386 libncurses5:i386 libstdc++6:i386 7 | RUN apt-get install -y python2.7 python-pip 8 | 9 | RUN useradd -m ctf 10 | 11 | COPY ./bin/* /home/ctf/ 12 | COPY ./xinetd.conf /etc/xinetd.d/ctf 13 | COPY ./start.sh /root/ 14 | COPY ./send.py /root/ 15 | COPY ./requirements.txt /root/ 16 | 17 | RUN pip install -r /root/requirements.txt 18 | 19 | # xinted 连接失败信息 20 | RUN echo "Blocked by xinetd" > /etc/banner_fail 21 | 22 | RUN chown -R root:ctf /home/ctf &&\ 23 | chmod -R 750 /home/ctf &&\ 24 | chmod 740 /home/ctf/flag 25 | 26 | # flag 日志 27 | RUN touch /root/log 28 | RUN chmod 700 /root/* 29 | 30 | WORKDIR /home/ctf 31 | 32 | CMD ["/root/start.sh"] 33 | 34 | EXPOSE 9999 35 | -------------------------------------------------------------------------------- /assets/online-challenge-modal.js: -------------------------------------------------------------------------------- 1 | $('#submit-key').unbind('click'); 2 | $('#submit-key').click(function (e) { 3 | e.preventDefault(); 4 | submitkey($('#chal-id').val(), $('#answer-input').val(), $('#nonce').val()) 5 | }); 6 | 7 | $("#answer-input").keyup(function(event){ 8 | if(event.keyCode == 13){ 9 | $("#submit-key").click(); 10 | } 11 | }); 12 | 13 | $(".input-field").bind({ 14 | focus: function() { 15 | $(this).parent().addClass('input--filled' ); 16 | $label = $(this).siblings(".input-label"); 17 | }, 18 | blur: function() { 19 | if ($(this).val() === '') { 20 | $(this).parent().removeClass('input--filled' ); 21 | $label = $(this).siblings(".input-label"); 22 | $label.removeClass('input--hide' ); 23 | } 24 | } 25 | }); 26 | var content = $('.chal-desc').text(); 27 | var decoded = $(' 31 | 32 | 33 |
34 |
35 | 36 | 37 |
38 | 41 | 42 |
43 | 44 |
45 | 48 | 49 |
50 | 51 |
52 | 55 | 56 |
57 | 58 |
59 | 62 |
63 | 64 |
65 |
66 | 70 |
71 |
72 | 73 |
74 |
75 | 79 |
80 |
81 | 82 |
83 | 89 |
90 | 91 |
92 | 95 | 96 | Attach multiple files using Control+Click or Cmd+Click. 97 |
98 | 99 | 100 | 101 | 102 |
103 | 104 |
105 | 106 | -------------------------------------------------------------------------------- /assets/online-challenge-update.njk: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/online-challenge-modal.njk: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Date : 2018/2/14 12:56 4 | # @Author : Xu 5 | # @Site : https://xuccc.github.io/ 6 | # @Version : $ 7 | 8 | import arrow 9 | import os 10 | from flask import render_template, Blueprint 11 | from flask import request, jsonify,session 12 | 13 | from CTFd.plugins import register_plugin_assets_directory 14 | from CTFd.plugins.challenges import BaseChallenge, CHALLENGE_CLASSES, CTFdStandardChallenge, get_key_class 15 | from CTFd.models import db, Solves, WrongKeys, Keys, Challenges, Files, Tags, Teams 16 | from CTFd.plugins.keys import BaseKey, KEY_CLASSES 17 | from CTFd.utils import admins_only, is_admin, upload_file, delete_file 18 | 19 | from CTFd.config import Config 20 | 21 | online = Blueprint('onlinechallenge', __name__, template_folder="templates") 22 | 23 | 24 | class OnlineKey(BaseKey): 25 | id = 2 26 | name = "online" 27 | templates = { 28 | 'create': '/plugins/CTFdOnlineChallenge/assets/create-dynamic-modal.njk', 29 | 'update': '/plugins/CTFdOnlineChallenge/assets/edit-dynamic-modal.njk', 30 | } 31 | 32 | @staticmethod 33 | def compare(saved, provided): 34 | if len(saved) != len(provided): 35 | return False 36 | result = 0 37 | for x, y in zip(saved, provided): 38 | result |= ord(x) ^ ord(y) 39 | return result == 0 40 | 41 | 42 | class CTFdOnlineChallenge(Challenges): 43 | __mapper_args__ = {'polymorphic_identity': 'online'} 44 | id = db.Column(None, db.ForeignKey('challenges.id'), primary_key=True) 45 | token = db.Column(db.String(80)) 46 | 47 | def __init__(self, name, description, value, category, token, type='online'): 48 | self.name = name 49 | self.description = description 50 | self.value = value 51 | self.category = category 52 | self.type = type 53 | self.token = token 54 | 55 | class CheatTeam(db.Model): 56 | id = db.Column(db.Integer, primary_key=True) 57 | chal = db.Column(db.String) 58 | cheat = db.Column(db.String) 59 | cheatd = db.Column(db.String) 60 | 61 | date = db.Column(db.String(40),default=arrow.now().format()) 62 | flag = db.Column(db.String(40)) 63 | 64 | def __init__(self,chal,cheat,cheatd,flag): 65 | self.chal = chal 66 | self.cheat = cheat 67 | self.cheatd = cheatd 68 | self.flag = flag 69 | 70 | class OnlineTypeChallenge(CTFdStandardChallenge): 71 | id = 'online' 72 | name = 'online' 73 | templates = { # Handlebars templates used for each aspect of challenge editing & viewing 74 | 'create': '/plugins/CTFdOnlineChallenge/assets/online-challenge-create.njk', 75 | 'update': '/plugins/CTFdOnlineChallenge/assets/online-challenge-update.njk', 76 | 'modal' : '/plugins/CTFdOnlineChallenge/assets/online-challenge-modal.njk', 77 | } 78 | scripts = { # Scripts that are loaded when a template is loaded 79 | 'create': '/plugins/CTFdOnlineChallenge/assets/online-challenge-create.js', 80 | 'update': '/plugins/CTFdOnlineChallenge/assets/online-challenge-update.js', 81 | 'modal' : '/plugins/CTFdOnlineChallenge/assets/online-challenge-modal.js', 82 | } 83 | 84 | @staticmethod 85 | def create(request): 86 | """ 87 | This method is used to process the challenge creation request. 88 | 89 | :param request: 90 | :return: 91 | """ 92 | # Create challenge 93 | chal = CTFdOnlineChallenge( 94 | name=request.form['name'], 95 | description=request.form['description'], 96 | token=request.form.get('keydata'), 97 | value=request.form['value'], 98 | category=request.form['category'], 99 | type=request.form['chaltype'] 100 | ) 101 | 102 | if 'hidden' in request.form: 103 | chal.hidden = True 104 | else: 105 | chal.hidden = False 106 | 107 | max_attempts = request.form.get('max_attempts') 108 | if max_attempts and max_attempts.isdigit(): 109 | chal.max_attempts = int(max_attempts) 110 | 111 | db.session.add(chal) 112 | db.session.commit() 113 | 114 | flag = Keys(chal.id, request.form['key'], request.form['key_type[0]']) 115 | if request.form.get('keydata'): 116 | flag.data = request.form.get('keydata') 117 | db.session.add(flag) 118 | 119 | db.session.commit() 120 | 121 | files = request.files.getlist('files[]') 122 | for f in files: 123 | upload_file(file=f, chalid=chal.id) 124 | 125 | db.session.commit() 126 | 127 | @staticmethod 128 | def read(challenge): 129 | """ 130 | This method is in used to access the data of a challenge in a format processable by the front end. 131 | 132 | :param challenge: 133 | :return: Challenge object, data dictionary to be returned to the user 134 | """ 135 | challenge = CTFdOnlineChallenge.query.filter_by(id=challenge.id).first() 136 | data = { 137 | 'id' : challenge.id, 138 | 'name' : challenge.name, 139 | 'value' : challenge.value, 140 | 'description' : challenge.description, 141 | 'category' : challenge.category, 142 | 'hidden' : challenge.hidden, 143 | 'max_attempts': challenge.max_attempts, 144 | 'type' : challenge.type, 145 | 'token' : challenge.token, 146 | 'type_data' : { 147 | 'id' : OnlineTypeChallenge.id, 148 | 'name' : OnlineTypeChallenge.name, 149 | 'templates': OnlineTypeChallenge.templates, 150 | 'scripts' : OnlineTypeChallenge.scripts, 151 | } 152 | } 153 | return challenge, data 154 | 155 | @staticmethod 156 | def update(challenge, request): 157 | """ 158 | This method is used to update the information associated with a challenge. This should be kept strictly to the 159 | Challenges table and any child tables. 160 | 161 | :param challenge: 162 | :param request: 163 | :return: 164 | """ 165 | challenge = CTFdOnlineChallenge.query.filter_by(id=challenge.id).first() 166 | 167 | challenge.name = request.form['name'] 168 | challenge.description = request.form['description'] 169 | challenge.value = int(request.form.get('value', 0)) if request.form.get('value', 0) else 0 170 | challenge.max_attempts = int(request.form.get('max_attempts', 0)) if request.form.get('max_attempts', 0) else 0 171 | challenge.category = request.form['category'] 172 | challenge.hidden = 'hidden' in request.form 173 | token = request.form['token'] 174 | key = Keys.query.filter_by(data=challenge.token).first() 175 | key.data = token 176 | challenge.token = token 177 | 178 | db.session.commit() 179 | db.session.close() 180 | 181 | @staticmethod 182 | def delete(challenge): 183 | """ 184 | This method is used to delete the resources used by a challenge. 185 | 186 | :param challenge: 187 | :return: 188 | """ 189 | WrongKeys.query.filter_by(chalid=challenge.id).delete() 190 | Solves.query.filter_by(chalid=challenge.id).delete() 191 | Keys.query.filter_by(chal=challenge.id).delete() 192 | files = Files.query.filter_by(chal=challenge.id).all() 193 | for f in files: 194 | delete_file(f.id) 195 | Files.query.filter_by(chal=challenge.id).delete() 196 | Tags.query.filter_by(chal=challenge.id).delete() 197 | Challenges.query.filter_by(id=challenge.id).delete() 198 | CTFdOnlineChallenge.query.filter_by(id=challenge.id).delete() 199 | db.session.commit() 200 | 201 | @staticmethod 202 | def attempt(chal, request): 203 | """ 204 | This method is used to check whether a given input is right or wrong. It does not make any changes and should 205 | return a boolean for correctness and a string to be shown to the user. It is also in charge of parsing the 206 | user's input from the request itself. 207 | 208 | :param chal: The Challenge object from the database 209 | :param request: The request the user submitted 210 | :return: (boolean, string) 211 | """ 212 | provided_key = request.form['key'].strip() 213 | team = Teams.query.filter_by(id=session['id']).first() 214 | cheatd = Solves.query.filter_by(flag=provided_key).first() 215 | if cheatd != None: 216 | find = CheatTeam( 217 | chal=cheatd.chalid, 218 | cheat=team.name, 219 | cheatd=cheatd.teamid, 220 | flag=provided_key 221 | ) 222 | db.session.add(find) 223 | db.session.commit() 224 | return False,'Warning,you must be copy others\'s flag!' 225 | chal_keys = Keys.query.filter_by(chal=chal.id).all() 226 | for chal_key in chal_keys: 227 | if get_key_class(chal_key.type).compare(chal_key.flag, provided_key): 228 | return True, 'Correct' 229 | return False, 'Incorrect' 230 | 231 | 232 | def filter(request): 233 | """ 234 | 235 | :param request: 236 | :return: 237 | """ 238 | flag = request.args.get('flag') 239 | token = request.args.get('token') 240 | time = request.args.get('time', arrow.now().timestamp) 241 | k = Keys.query.filter_by(data=token).first() if token else None 242 | return flag, token, time, k 243 | 244 | 245 | def save(k, flag): 246 | """ 247 | 248 | :param k: class 'CTFd.models.Keys' 249 | :param flag: string 250 | :return: 251 | """ 252 | k.flag = flag 253 | db.session.commit() 254 | db.session.close() 255 | return '1' 256 | 257 | 258 | def client(**kwargs): 259 | """ 260 | Return data to client 261 | :param kwargs: 262 | :return: dict 263 | """ 264 | return { 265 | 'check' : kwargs.get('check', False), 266 | 'reason' : kwargs.get('reason'), 267 | 'flag_old' : kwargs.get('flag_old'), 268 | 'flag_new' : kwargs.get('flag_new'), 269 | 'timestamp': kwargs.get('time') 270 | } 271 | 272 | 273 | def log(state = None,content=None,path='onlineChallenge.log'): 274 | class Templete: 275 | pass 276 | path = os.path.join(Config.LOG_FOLDER,path) # CTFd/logs/onlineChallenge.log 277 | line = "[{}] <{}> {}\n".format(arrow.now().format(),request.remote_addr,request.args) 278 | with open(path,'a') as f: 279 | f.write(line) 280 | 281 | @online.route('/dynamic/keys', methods=['POST', 'GET']) 282 | def get_data(): 283 | if request.method == 'GET': 284 | if is_admin() is False: 285 | log() 286 | # Get client data 287 | flag, token, time, k = filter(request) 288 | if k is not None: 289 | data = client(check=True, flag_old=k.flag, flag_new=flag, time=time) 290 | save(k, flag) 291 | if k is None: 292 | data = client(reason='token wrong', time=time) 293 | return jsonify(data) 294 | if is_admin() is True: 295 | # Show Serve log to admin 296 | return jsonify(client(reason='admin')) 297 | elif request.method == 'POST': 298 | # TODO 299 | data = {} 300 | return jsonify(data) 301 | @online.route('/admin/onlinechallenge',methods=['GET']) 302 | @admins_only 303 | def show_cheat(): 304 | if request.method == 'GET': 305 | cheats = CheatTeam.query.all() 306 | return render_template('cheat.html',cheats=cheats) 307 | 308 | def load(app): 309 | app.db.create_all() 310 | KEY_CLASSES['online'] = OnlineKey 311 | CHALLENGE_CLASSES['online'] = OnlineTypeChallenge 312 | app.register_blueprint(online) 313 | register_plugin_assets_directory(app, base_path='/plugins/CTFdOnlineChallenge/assets') 314 | 315 | 316 | --------------------------------------------------------------------------------