Skip to content

Commit 5e44d92

Browse files
authored
Merge pull request #16 from hsasctf/wip/ipauth
IP Authentication
2 parents 7c8a653 + bcae3d0 commit 5e44d92

File tree

8 files changed

+57
-101
lines changed

8 files changed

+57
-101
lines changed

ctfdbapi/app/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,13 @@ def not_found(error):
3939
return render_template('404.html'), 404
4040

4141

42-
# admin
43-
4442

4543
from db.models import TeamScore, AttendingTeam, Event, Team, Submission, Flag, Challenge, Member, User, Catering, Food, \
4644
Tick, TeamServiceState, TeamScriptsRunStatus, Script, ScriptPayload, ScriptRun
4745

4846
import flask_admin as admin
4947
from flask_admin.contrib import sqla
5048

51-
# ADMIN
5249
from flask import request, Response
5350
from werkzeug.exceptions import HTTPException
5451

ctfdbapi/app/web/__init__.py

Lines changed: 49 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from functools import wraps
2+
13
from sqlalchemy import func
24

35
from flask_httpauth import HTTPBasicAuth
@@ -9,11 +11,12 @@
911
from db.database import db_session
1012
from db.models import TeamScore, AttendingTeam, Event, Team, Submission, Flag, Challenge, Member, User, Catering, Food, \
1113
Tick, TeamServiceState, TeamScriptsRunStatus, Script, ScriptPayload, ScriptRun
14+
from app.api import verify_flag
1215

1316
from hashlib import sha512
1417

1518
import redis
16-
import requests
19+
import ipaddress
1720

1821
web = Blueprint('web', __name__,
1922
template_folder='templates')
@@ -22,95 +25,60 @@
2225
redis_client = redis.StrictRedis(host='localhost', port=6379, db=0)
2326

2427

25-
def get_attacking_team():
26-
team = Team.query.filter(func.lower(Team.team_name) == func.lower(get_real_username(auth.username()))).first()
27-
event = Event.query.order_by(Event.id.desc()).first()
28-
attacking_team = AttendingTeam.query.filter_by(team=team, event=event).first()
29-
return attacking_team
30-
31-
32-
@auth.get_password
33-
def get_password(username):
34-
event = Event.query.order_by(Event.id.desc()).first()
35-
36-
try_split = username.rsplit("-", 1)
37-
if len(try_split) == 2:
38-
if try_split[1] == "admin":
39-
real_username = try_split[0]
40-
team = Team.query.filter(func.lower(Team.team_name) == func.lower(real_username)).first()
41-
42-
if AttendingTeam.query.filter_by(team=team, event=event).first():
43-
db_session.remove()
44-
return current_app.config["ADMIN_CREDENTIALS"][1]
45-
else:
46-
db_session.remove()
47-
return None
48-
team = Team.query.filter(func.lower(Team.team_name) == func.lower(username)).first()
49-
50-
# only attending teams can login
51-
if not AttendingTeam.query.filter_by(team=team, event=event).first():
52-
return None
53-
54-
db_session.remove()
55-
return team.password
28+
def get_team_num(ip):
29+
request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
5630

31+
def find_team(f):
32+
@wraps(f)
33+
def decorated_function(*args, **kwargs):
34+
ip = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
35+
try:
36+
a, b, c, d = [int(x) for x in ip.split(".")]
37+
assert a == 10
38+
assert b in [40, 41, 42, 43]
39+
event = Event.query.order_by(Event.id.desc()).first()
40+
ateam = AttendingTeam.query.filter_by(subnet=c, event=event).one_or_none()
41+
team = ateam.team if ateam is not None else None
42+
if not team:
43+
return redirect(url_for('web.error'))
44+
kwargs['team'] = team
45+
kwargs['ateam'] = ateam
46+
except Exception as e:
47+
return redirect(url_for('error'))
48+
return f(*args, **kwargs)
49+
return decorated_function
5750

58-
@auth.hash_password
59-
def hash_password(username, password):
60-
event = Event.query.order_by(Event.id.desc()).first()
6151

62-
try_split = username.rsplit("-", 1)
63-
if len(try_split) == 2:
64-
if try_split[1] == "admin":
65-
real_username = try_split[0]
66-
team = Team.query.filter(func.lower(Team.team_name) == func.lower(real_username)).first()
52+
@web.route("/error")
53+
def error():
54+
return "ERROR, are you connected to VPN or are in the correct subnet?"
6755

68-
if AttendingTeam.query.filter_by(team=team, event=event).first():
69-
db_session.remove()
70-
return current_app.config["ADMIN_CREDENTIALS"][1]
71-
else:
72-
db_session.remove()
73-
return None
74-
75-
team = Team.query.filter(func.lower(Team.team_name) == func.lower(username)).first()
76-
if not AttendingTeam.query.filter_by(team=team, event=event).first():
77-
return None
78-
79-
db_session.remove()
80-
pw_salt = "{}{}".format(password, team.password_salt)
81-
return sha512(pw_salt.encode("utf8")).hexdigest()
82-
83-
84-
def get_real_username(username):
85-
"""allows admins to login as TEAMNAME-admin instead of TEAMNAME but with admin password"""
86-
try_split = username.rsplit("-", 1)
87-
if len(try_split) == 2:
88-
if try_split[1] == "admin":
89-
real_username = try_split[0]
90-
return real_username
91-
92-
return username
9356

9457

9558
@web.route("/")
96-
@auth.login_required
97-
def index():
59+
@find_team
60+
def index(team=None, ateam=None):
9861
return render_template('index.html')
9962

10063

10164
@web.route('/config')
102-
@auth.login_required
103-
def get_config():
104-
return jsonify({'ctf_name': current_app.config["CTF_NAME"], 'team_name': get_real_username(auth.username())})
65+
@find_team
66+
def get_config(team=None, ateam=None):
67+
team_name = "UNKNOWN"
68+
try:
69+
team_name = team.team_name
70+
except AttributeError:
71+
pass
72+
finally:
73+
return jsonify({'ctf_name': current_app.config["CTF_NAME"], 'team_name': team_name})
10574

10675

107-
from app.api import verify_flag
10876

10977

11078
@web.route('/flag', methods=['POST'])
111-
@auth.login_required
112-
def submit_flag():
113-
attacking_team = get_attacking_team()
79+
@find_team
80+
def submit_flag(team=None, ateam=None):
81+
attacking_team = ateam
11482

11583
return verify_flag(int(attacking_team.id), request.get_json()['flag'])
11684

@@ -121,27 +89,26 @@ def get_scores():
12189

12290

12391
@web.route('/services')
124-
@auth.login_required
125-
def get_services():
92+
@find_team
93+
def get_services(team=None, ateam=None):
12694
return redis_client.get('ctf_services')
12795

12896

12997
@web.route('/jeopardies')
130-
@auth.login_required
131-
def get_jeopardies():
98+
@find_team
99+
def get_jeopardies(team=None, ateam=None):
132100
return redis_client.get('ctf_jeopardy_list')
133101

134102

135103
@web.route('/services_status')
136-
@auth.login_required
137-
def get_services_status():
104+
@find_team
105+
def get_services_status(team=None, ateam=None):
138106
status = json.loads(redis_client.get('ctf_services_status'))
139107
result = {}
140108

141-
team = get_attacking_team()
142109

143110
for state in status:
144-
if state['team_id'] == team.id:
111+
if state['team_id'] == ateam.id:
145112
for entry in state['services']:
146113
result[entry['service_id']] = entry['state']
147114

@@ -152,8 +119,3 @@ def get_services_status():
152119
@web.route('/tick_change_time')
153120
def get_tick_duration():
154121
return redis_client.get('ctf_tick_change_time')
155-
156-
157-
@web.route('/logout')
158-
def logout():
159-
return ('Logout', 401, {'WWW-Authenticate': 'Basic realm="Login required"'})

ctfdbapi/app/web/templates/index.html

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,6 @@ <h3>
5858
</h3>
5959
</a>
6060
</li>
61-
<li class='btn'>
62-
<a href='/logout'>
63-
<h2>
64-
Logout
65-
</h2>
66-
</a>
67-
</li>
6861

6962

7063
</div>

ctfdbapi/dashboard_worker.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def ctf_services(self):
8686
'team_id': attending_team_id,
8787
'flag_id': flag_ids[attending_team_id][str(service_id)],
8888
} for attending_team_id in
89-
dict(list(filter(lambda kv: str(service_id) in kv[1], flag_ids.items()))) # TODO TEST THIS!!
89+
dict(list(filter(lambda kv: str(service_id) in kv[1], flag_ids.items())))
9090
]
9191
}
9292

ctfdbapi/db/models.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from sqlalchemy import UniqueConstraint, create_engine
1414
from sqlalchemy.dialects.mysql import TINYINT
1515
from sqlalchemy.ext.declarative import declarative_base
16+
from sqlalchemy.ext.hybrid import hybrid_property
1617
from sqlalchemy.orm import backref, relationship
1718
from sqlalchemy.sql.ddl import CreateTable
1819

@@ -206,7 +207,7 @@ class Submission(ModelBase):
206207
flag = relationship('Flag',
207208
backref=backref('submissions', cascade="all, delete-orphan", lazy='dynamic'))
208209

209-
# should be None/NULL when no matching flag could be found
210+
# TODO should be None/NULL when no matching flag could be found
210211
submitted_string = Column(String(128), nullable=False)
211212

212213
def __str__(self):

ctfdbapi/demo1.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828

2929
def delete_prev_demos():
3030
Event.query.filter_by(is_demo=1).delete()
31+
Team.query.filter_by(team_name="test1").delete()
32+
Team.query.filter_by(team_name="test2").delete()
33+
Team.query.filter_by(team_name="test3").delete()
3134
db_session.commit()
3235

3336

@@ -170,7 +173,6 @@ def stop_tmux_sessions():
170173
from urllib.parse import quote
171174

172175
logger.info("-----")
173-
logger.warning("Firefox recommended")
174176
logger.info(
175177
"or login as Admin using this URL: http://{}@10.38.1.1:4999/admin/".format(':'.join(ADMIN_CREDENTIALS)))
176178
logger.info("-----")

ctfdbapi/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,4 @@ urllib3==1.21.1
5555
waitress==1.3.0
5656
Werkzeug==0.12.2
5757
WTForms==2.1
58+
pytest

docs/development.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ EOF
105105

106106

107107
1. connect (from host) to openvpn using `openvpn --config roles/vpn/files/client_configs/client-teamXXX.ovpn`
108-
1. http://10.38.1.1:5000/ (Login for CTF teams with flag input, scores, jeopardies). Login as team without knowing the (hashed) password: Username: TEAMNAME-admin (example: mtga-admin) Password: *defined in ctfdbapi/config.py*
108+
1. http://10.38.1.1:5000/ (Webapp for CTF teams with flag input, scores).
109109
1. The containers have the timezone UTC, so Attack&Defense Start timestamp must be specified in UTC in the database
110110
1. Admin interface http://10.38.1.1:4999/admin (use password from ctfdbapi/config.py)
111111

0 commit comments

Comments
 (0)