diff --git a/ctf.css b/ctf.css new file mode 100644 index 0000000..1eb1fc1 --- /dev/null +++ b/ctf.css @@ -0,0 +1,21 @@ +body { + background: #000; + color: #0f0; +} +.readme { + background: #444; +} +a:link { + color: #ff0; +} +a:visited { + color: #880; +} +a:hover { + color: #000; + background: #ff0; +} +.error { + color: #000; + background: #f00; +} diff --git a/flagd.py b/flagd.py index 2e48be4..862afc7 100755 --- a/flagd.py +++ b/flagd.py @@ -9,6 +9,7 @@ import hmac import optparse import points import pointscli +import teams import traceback key = b'My First Shared Secret (tm)' @@ -67,7 +68,7 @@ class Submitter(asyncore.dispatcher): def set_flag(self, cat, team): now = int(time.time()) - team = team or points.house + team = team or teams.house if self.flags.get(cat) != team: self.flags[cat] = team diff --git a/game.py b/game.py index 82fe91c..45ae28b 100755 --- a/game.py +++ b/game.py @@ -6,6 +6,7 @@ import asynchat import socket import traceback import time +import teams from errno import EPIPE @@ -25,7 +26,6 @@ class Listener(asyncore.dispatcher): self.listen(4) self.player_factory = player_factory self.manager = manager - self.last_beat = 0 def handle_accept(self): conn, addr = self.accept() @@ -34,9 +34,7 @@ class Listener(asyncore.dispatcher): # has a reference to it for as long as it's open. def readable(self): - now = time.time() - if now > self.last_beat + pulse: - self.manager.heartbeat(now) + self.manager.heartbeat(time.time()) return True @@ -89,11 +87,25 @@ class Manager: self.lobby = set() self.contestants = [] self.last_beat = 0 + self.timers = set() def heartbeat(self, now): - # Called by listener to beat heart - for game in list(self.games): - game.heartbeat(now) + """Called by listener to beat heart.""" + + now = time.time() + if now > self.last_beat + pulse: + for game in list(self.games): + game.heartbeat(now) + for event in self.timers: + when, cb = event + if now >= when: + self.timers.remove(event) + cb() + + def add_timer(self, when, cb): + """Add a timed callback.""" + + self.timers.add((when, cb)) def enter_lobby(self, player): self.lobby.add(player) @@ -248,13 +260,14 @@ class Player(asynchat.async_chat): cmd, args = val[0].lower(), val[1:] if cmd == 'login': - if not self.name: - # XXX Check password + if self.name: + self.err('Already logged in.') + elif teams.chkpasswd(args[0], args[1]): self.name = args[0] self.write('Welcome to the fray, %s.' % self.name) self.manager.enter_lobby(self) else: - self.err('Already logged in.') + self.err('Invalid password.') elif cmd == '^': # Send to manager ret = self.manager.player_cmd(args) diff --git a/games/crypto/scytale.py b/games/crypto/scytale.py new file mode 100755 index 0000000..ece0efd --- /dev/null +++ b/games/crypto/scytale.py @@ -0,0 +1,30 @@ +#! /usr/bin/env python3 + +import sys +import random + +primes = [2, 3, 5, 7, 11, 13, 17, 19] +letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + +data = sys.stdin.read().strip() +jumble = ''.join(data.split()) + +lj = len(jumble) +below = (0, 0) +above = (lj, 2) +for i in primes: + for j in primes: + m = i * j + if (m < lj) and (m > below[0] * below[1]): + below = (i, j) + elif (m >= lj) and (m < (above[0] * above[1])): + above = (i, j) + +for i in range(lj, (above[0] * above[1])): + jumble += random.choice(letters) + +out = [] +for i in range(above[0]): + for j in range(above[1]): + out.append(jumble[j*above[0] + i]) +print(''.join(out)) diff --git a/index.html b/index.html new file mode 100644 index 0000000..89fe545 --- /dev/null +++ b/index.html @@ -0,0 +1,22 @@ + + + + + Capture The Flag + + + +

Capture The Flag

+ +
    +
  1. Register your team
  2. +
  3. Scoreboard
  4. +
+ +

+ Some challenges are puzzles. Some are + sitting on the network; you must find these yourself! +

+ + diff --git a/points.py b/points.py index 952ee00..10cadff 100755 --- a/points.py +++ b/points.py @@ -4,9 +4,7 @@ import socket import hmac import struct import io - -## Name of the house team -house = 'dirtbags' +import teams ## ## Authentication diff --git a/pointscli.py b/pointscli.py index c4bd659..c43a534 100755 --- a/pointscli.py +++ b/pointscli.py @@ -6,10 +6,17 @@ import points import socket import time -def submit(sock, cat, team, score): +def makesock(host): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect((host, 6667)) + return s + +def submit(cat, team, score, sock=None): + if not sock: + sock = makesock('cfl-sunray1') begin = time.time() mark = int(begin) - req = points.encode_request(mark, cat, team, score) + req = points.encode_request(1, mark, cat, team, score) while True: sock.send(req) r, w, x = select.select([sock], [], [], begin + 2 - time.time()) @@ -17,12 +24,12 @@ def submit(sock, cat, team, score): break b = sock.recv(500) try: - when, cat_, txt = points.decode_response(b) + id, txt = points.decode_response(b) except ValueError: # Ignore invalid packets continue - if (when != mark) or (cat_ != cat): - # Ignore wrong timestamp + if id != 1: + # Ignore wrong ID continue if txt == 'OK': return @@ -30,11 +37,6 @@ def submit(sock, cat, team, score): raise ValueError(txt) -def makesock(host): - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.connect((host, 6667)) - return s - def main(): p = optparse.OptionParser(usage='%prog CATEGORY TEAM SCORE') p.add_option('-s', '--host', dest='host', default='localhost', @@ -50,7 +52,7 @@ def main(): s = makesock(opts.host) try: - submit(s, cat, team, score) + submit(cat, team, score, sock=s) except ValueError as err: print(err) raise diff --git a/pointsd.py b/pointsd.py index c415dbc..3ec0e63 100755 --- a/pointsd.py +++ b/pointsd.py @@ -34,11 +34,11 @@ class MyHandler(asyncore.dispatcher): team = team or house # Replays can happen legitimately. - if not (id in self.acked): + if not ((peer, id) in self.acked): if not (now - 2 < when <= now): return self.respond(peer, id, 'Your clock is off') self.store.add((when, cat, team, score)) - self.acked.add(id) + self.acked.add((peer, id)) self.respond(peer, id, 'OK') diff --git a/puzzler.cgi b/puzzler.cgi new file mode 100755 index 0000000..84fe253 --- /dev/null +++ b/puzzler.cgi @@ -0,0 +1,176 @@ +#! /usr/bin/env python3 + +import cgitb; cgitb.enable() +import cgi +import os +import fcntl +import re +import sys +import pointscli +import teams + +cat_re = re.compile(r'^[a-z]+$') +points_re = re.compile(r'^[0-9]+$') + +def dbg(*vals): + print('Content-type: text/plain\n\n') + print(*vals) + + +points_by_cat = {} +points_by_team = {} +try: + for line in open('puzzler.dat'): + line = line.strip() + cat, team, pts = line.split('\t') + pts = int(pts) + points_by_cat[cat] = max(points_by_cat.get(cat, 0), pts) + points_by_team.setdefault((team, cat), set()).add(pts) +except IOError: + pass + + +f = cgi.FieldStorage() + +cat = f.getfirst('c') +points = f.getfirst('p') +team = f.getfirst('t') +passwd = f.getfirst('w') +key = f.getfirst('k') + +verboten = ['key', 'index.html'] + +def start_html(title): + print('''Content-type: text/html + + + + + + %s + + + +

%s

+''' % (title, title)) + +def end_html(): + print('') + + +def safe_join(*args): + safe = [] + for a in args: + if not a: + return None + else: + a = a.replace('..', '') + a = a.replace('/', '') + safe.append(a) + ret = '/'.join(safe) + if os.path.exists(ret): + return ret + +def dump_file(fn): + f = open(fn, 'rb') + while True: + d = f.read(4096) + if not d: + break + sys.stdout.buffer.write(d) + +def show_cats(): + start_html('Categories') + print('') + end_html() + + +def show_puzzles(cat, cat_dir): + start_html('Open in %s' % cat) + opened = points_by_cat.get(cat, 0) + puzzles = sorted([int(v) for v in os.listdir(cat_dir)]) + if puzzles: + print('') + else: + print('

None (someone is slacking)

') + end_html() + +def show_puzzle(cat, points, points_dir): + # Show puzzle in cat for points + start_html('%s for %s' % (cat, points)) + fn = os.path.join(points_dir, 'index.html') + if os.path.exists(fn): + print('
') + dump_file(fn) + print('
') + print('') + print('
') + print('' % cat) + print('' % points) + print('Team:
') + print('Password:
') + print('Key:
') + print('') + print('
') + end_html() + +def win(cat, team, points): + start_html('Winner!') + points = int(points) + pointscli.submit(cat, team, points) + end_html() + f = open('puzzler.dat', 'a') + fctnl.lockf(f, LOCK_EX) + f.write('%s\t%s\t%d\n' % (cat, team, points)) + +def main(): + cat_dir = safe_join('puzzles', cat) + points_dir = safe_join('puzzles', cat, points) + + if not cat_dir: + # Show categories + show_cats() + elif not points_dir: + # Show available puzzles in category + show_puzzles(cat, cat_dir) + elif not (team and passwd and key): + fn = f.getfirst('f') + if fn in verboten: + fn = None + fn = safe_join('puzzles', cat, points, fn) + if fn: + # Provide a file from this directory + print('Content-type: application/octet-stream') + print() + dump_file(fn) + else: + show_puzzle(cat, points, points_dir) + else: + thekey = open('%s/key' % points_dir).read().strip() + if not teams.chkpasswd(team, passwd): + start_html('Wrong password') + end_html() + elif key != thekey: + show_puzzle(cat, points, points_dir) + elif points_by_team.get((team, cat)): + start_html('Greedy greedy') + end_html() + else: + win(cat, team, points) + +main() diff --git a/puzzles/bletchey/100/key b/puzzles/bletchey/100/key new file mode 100644 index 0000000..5403edd --- /dev/null +++ b/puzzles/bletchey/100/key @@ -0,0 +1 @@ +antediluvian diff --git a/puzzles/bletchey/100/key.png b/puzzles/bletchey/100/key.png new file mode 100644 index 0000000..b658ad9 Binary files /dev/null and b/puzzles/bletchey/100/key.png differ diff --git a/puzzles/bletchey/200/index.html b/puzzles/bletchey/200/index.html new file mode 100644 index 0000000..398be1c --- /dev/null +++ b/puzzles/bletchey/200/index.html @@ -0,0 +1 @@ +tkftsuiuqvaheohrnsnuoleyriod"eic" diff --git a/puzzles/bletchey/200/key b/puzzles/bletchey/200/key new file mode 100644 index 0000000..d398ae2 --- /dev/null +++ b/puzzles/bletchey/200/key @@ -0,0 +1 @@ +unequivocal diff --git a/puzzles/bletchey/250/index.html b/puzzles/bletchey/250/index.html new file mode 100644 index 0000000..80ad312 --- /dev/null +++ b/puzzles/bletchey/250/index.html @@ -0,0 +1 @@ +27586126814341379597440261571645814840581961154587430529221052323 diff --git a/puzzles/bletchey/250/key b/puzzles/bletchey/250/key new file mode 100644 index 0000000..3933ee2 --- /dev/null +++ b/puzzles/bletchey/250/key @@ -0,0 +1 @@ +DB1663<3 diff --git a/puzzles/bletchey/300/index.html b/puzzles/bletchey/300/index.html new file mode 100644 index 0000000..c773f26 --- /dev/null +++ b/puzzles/bletchey/300/index.html @@ -0,0 +1,13 @@ +

Kolejne modele Panzerfausta, odpowiednio: 60, 100, 150, różnił kaliber głowicy i wielkość ładunku miotającego. Konstrukcja i mechanizm nie ulegał istotnym zmianom, z racji wzrastania zasięgu broni modyfikacjom ulegały nastawy celowników. Jedynie we wzorze 150 wprowadzono (a był to już początek 1945 roku) wielokrotne użycie wyrzutni rurowej. Osiągnięto to przez umieszczenie ładunku miotającego w głowicy oraz przez wzmocnienie rury. W wyniku problemu z transportem model ów nie wszedł do walki. Model 250 (o teoretycznym zasięgu 250 m) z racji zakończenia wojny nie opuścił desek kreślarskich nigdy nie wchodząc nawet w fazę prototypową.

+
(61, 4)
+(47, 8)
+(19, 4)
+(37, 1)
+(51, 3)
+(67, 5)
+(9, 2)
+(26, 1)
+(2, 2)
+(26, 3)
+(50, 2)
+ diff --git a/puzzles/bletchey/300/key b/puzzles/bletchey/300/key new file mode 100644 index 0000000..ea86794 --- /dev/null +++ b/puzzles/bletchey/300/key @@ -0,0 +1 @@ +jako561962 diff --git a/puzzles/bletchey/500/200601262232.ogg b/puzzles/bletchey/500/200601262232.ogg new file mode 100644 index 0000000..d00f825 Binary files /dev/null and b/puzzles/bletchey/500/200601262232.ogg differ diff --git a/puzzles/bletchey/500/cipher.txt b/puzzles/bletchey/500/cipher.txt new file mode 100644 index 0000000..cf43f95 --- /dev/null +++ b/puzzles/bletchey/500/cipher.txt @@ -0,0 +1 @@ +31 9 15 26 14 23 14 6 18 5 12 18 5 2 16 27 7 10 11 5 13 31 17 17 6 2 26 26 10 21 10 8 20 4 diff --git a/puzzles/bletchey/500/index.html b/puzzles/bletchey/500/index.html new file mode 100644 index 0000000..8c32ab6 --- /dev/null +++ b/puzzles/bletchey/500/index.html @@ -0,0 +1 @@ +journals.uchicago diff --git a/puzzles/bletchey/500/key b/puzzles/bletchey/500/key new file mode 100644 index 0000000..b197a4a --- /dev/null +++ b/puzzles/bletchey/500/key @@ -0,0 +1 @@ +xez.3nt diff --git a/register.cgi b/register.cgi new file mode 100755 index 0000000..3dfb0c5 --- /dev/null +++ b/register.cgi @@ -0,0 +1,62 @@ +#! /usr/bin/env python3 + +import cgitb; cgitb.enable() +import cgi +import teams +import fcntl +import string + +print('Content-type: text/html') +print() + +f = cgi.FieldStorage() + +team = f.getfirst('team', '') +pw = f.getfirst('pw') +confirm_pw = f.getfirst('confirm_pw') + +html = string.Template(''' + + + + Team Registration + + + +

Team Registration

+ +
+
+ + + $team_error
+ + +
+ + + + $pw_match_error
+ + +
+
+ + +''') + +if not (team and pw and confirm_pw): #If we're starting from the beginning? + html = html.substitute(team_error='', + pw_match_error='') +elif teams.exists(team): + html = html.substitute(team_error='Team team already taken', + pw_match_error='') +elif pw != confirm_pw: + html = html.substitute(team_error='', + pw_match_error='Passwords do not match') +else: + teams.add(team, pw) + html = 'Team registered.' + +print(html) diff --git a/scoreboard.cgi b/scoreboard.cgi index 2f42bc0..7bb66a3 100755 --- a/scoreboard.cgi +++ b/scoreboard.cgi @@ -17,9 +17,10 @@ print(''' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> - yo mom + CTF Scoreboard + - +

Scoreboard

''') print('') diff --git a/teams.py b/teams.py new file mode 100755 index 0000000..a9bb384 --- /dev/null +++ b/teams.py @@ -0,0 +1,40 @@ +#! /usr/bin/env python3 + +import fcntl + +house = 'dirtbags' + +teams = None + +def build_teams(): + global teams + + teams = {} + try: + f = open('passwd') + for line in f: + team, passwd = line.strip().split('\t') + teams[team] = passwd + except IOError: + pass + +def chkpasswd(team, passwd): + if teams is None: + build_teams() + if teams.get(team) == passwd: + return True + else: + return False + +def exists(team): + if teams is None: + build_teams() + if team == house: + return True + return team in teams + +def add(team, passwd): + f = open('passwd', 'a') + fcntl.lockf(f, fcntl.LOCK_EX) + f.seek(0, 2) + f.write('%s\t%s\n' % (team, passwd))