diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..15953f7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*~ +*.pyc +*.dat +passwd +target/ +puzzler/ +ctf.tce diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8ce3a5a --- /dev/null +++ b/Makefile @@ -0,0 +1,44 @@ +DESTDIR = target + +CTFDIR = $(DESTDIR)/usr/lib/ctf +WWWDIR = $(DESTDIR)/usr/lib/www + +FAKE = fakeroot -s fake -i fake +INSTALL = $(FAKE) install + +PYC = config.pyc points.pyc game.pyc teams.pyc +PYC += register.pyc scoreboard.pyc puzzler.pyc +PYC += flagd.pyc pointsd.pyc pointscli.pyc +PYC += roshambo.pyc histogram.pyc + +all: ctf.tce + +target: $(PYC) + $(INSTALL) -d --mode=0755 --owner=100 $(DESTDIR)/var/lib/ctf + $(INSTALL) -d $(DESTDIR)/var/lib/ctf/disabled + + $(INSTALL) -d $(CTFDIR) + $(INSTALL) $(PYC) $(CTFDIR) + $(INSTALL) uberserv.py $(CTFDIR) + + $(INSTALL) -d $(WWWDIR) + $(INSTALL) index.html ctf.css $(WWWDIR) + $(FAKE) ln -s /var/lib/ctf/histogram.png $(WWWDIR) + $(INSTALL) register.cgi scoreboard.cgi puzzler.cgi $(WWWDIR) + + $(INSTALL) -d $(DESTDIR)/var/service/ctf + $(INSTALL) run.uberserv $(DESTDIR)/var/service/ctf/run + + rm -rf $(WWWDIR)/puzzler + $(INSTALL) -d $(WWWDIR)/puzzler + ./mkpuzzles.py --htmldir=$(WWWDIR)/puzzler --keyfile=$(CTFDIR)/puzzler.keys + +ctf.tce: target + $(FAKE) sh -c 'cd target && tar -czf - --exclude=placeholder --exclude=*~ .' > $@ + +clean: + rm -rf target + rm -f fake ctf.tce $(PYC) + +%.pyc: %.py + python3 -c 'import $*' diff --git a/config.py b/config.py new file mode 100755 index 0000000..e7bbeda --- /dev/null +++ b/config.py @@ -0,0 +1,57 @@ +#! /usr/bin/env python3 + +import os + +if 'home' in os.environ.get('SCRIPT_FILENAME', ''): + # We're a CGI running out of someone's home directory + config = {'global': + {'data_dir': '.', + 'base_url': '.', + 'css_url': 'ctf.css', + 'diasbled_dir': 'disabled' + }, + 'puzzler': + {'dir': 'puzzles', + 'ignore_dir': 'puzzler.ignore', + 'cgi_url': 'puzzler.cgi', + 'base_url': 'puzzler', + 'keys_file': 'puzzler.keys', + }, + } +else: + # An actual installation + config = {'global': + {'data_dir': '/var/lib/ctf', + 'base_url': '/', + 'css_url': '/ctf.css', + 'disabled_dir': '/var/lib/ctf/disabled', + }, + 'puzzler': + {'dir': '/usr/lib/www/puzzler', + 'ignore_dir': '/var/lib/ctf/puzzler.ignore', + 'cgi_url': '/puzzler.cgi', + 'base_url': '/puzzler', + 'keys_file': '/usr/lib/ctf/puzzler.keys', + }, + } + +def get(section, key): + return config[section][key] + +disabled_dir = get('global', 'disabled_dir') +data_dir = get('global', 'data_dir') +base_url = get('global', 'base_url') +css = get('global', 'css_url') + +def disabled(cat): + path = os.path.join(disabled_dir, cat) + return os.path.exists(path) + +def enabled(cat): + return not disabled(cat) + +def datafile(filename): + return os.path.join(data_dir, filename) + +def url(path): + return base_url + path diff --git a/games/bletchey/arecibo.py b/games/bletchey/arecibo.py deleted file mode 100755 index 447f620..0000000 --- a/games/bletchey/arecibo.py +++ /dev/null @@ -1,15 +0,0 @@ -#! /usr/bin/env python3 - -# 0 1 2 3 4 -# 1234567890123456789012345678901234567890123 -# ||| | | | | | | | | | | | | -msg = (' #### #### # ### ### ### # ### ' - ' # # # # ## # # # # # ' - ' # # #### # #### #### # # # ' - ' # # # # # # # # # # # # ' - ' #### #### ### ### ### ### # ### ') - -msg = msg.replace('#', '0') -msg = msg.replace(' ', '1') -num = int(msg, 2) -print(num) diff --git a/games/bletchey/scytale.py b/games/bletchey/scytale.py deleted file mode 100755 index ece0efd..0000000 --- a/games/bletchey/scytale.py +++ /dev/null @@ -1,30 +0,0 @@ -#! /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/histogram.py b/histogram.py index 3d5bc9c..434ac68 100755 --- a/histogram.py +++ b/histogram.py @@ -4,13 +4,16 @@ import points import time import os import tempfile +import config + +pngout = config.datafile('histogram.png') def main(s=None): scores = {} now = 0 if not s: - s = points.Storage('scores.dat') + s = points.Storage() plotparts = [] teams = s.teams() @@ -57,11 +60,12 @@ set xtics nomirror set ytics nomirror set nokey set terminal png transparent size 640,200 x000000 xffffff -set output "histogram.png" -plot %(plot)s\n''' % {'plot': ','.join(plotparts)}) +set output "%(pngout)s" +plot %(plot)s\n''' % {'plot': ','.join(plotparts), + 'pngout': pngout}) instructions.flush() - gp = os.system('gnuplot %s' % instructions.name) + gp = os.system('gnuplot %s 2>/dev/null' % instructions.name) if __name__ == '__main__': main() diff --git a/mkpuzzles.py b/mkpuzzles.py new file mode 100755 index 0000000..fab4e98 --- /dev/null +++ b/mkpuzzles.py @@ -0,0 +1,80 @@ +#! /usr/bin/env python3 + +import os +import shutil +import optparse +import config + +p = optparse.OptionParser() +p.add_option('-p', '--puzzles', dest='puzzles', default='puzzles', + help='Directory containing puzzles') +p.add_option('-w', '--htmldir', dest='htmldir', default='puzzler', + help='Directory to write HTML puzzle tree') +p.add_option('-k', '--keyfile', dest='keyfile', default='puzzler.keys', + help='Where to write keys') + +opts, args = p.parse_args() + +keys = [] + +for cat in os.listdir(opts.puzzles): + dirname = os.path.join(opts.puzzles, cat) + for points in os.listdir(dirname): + pointsdir = os.path.join(dirname, points) + outdir = os.path.join(opts.htmldir, cat, points) + os.makedirs(outdir) + + readme = '' + files = [] + for fn in os.listdir(pointsdir): + path = os.path.join(pointsdir, fn) + if fn == 'key': + key = open(path, encoding='utf-8').readline().strip() + keys.append((cat, points, key)) + elif fn == 'index.html': + readme = open(path, encoding='utf-8').read() + else: + files.append((fn, path)) + + title = '%s for %s points' % (cat, points) + f = open(os.path.join(outdir, 'index.html'), 'w', encoding='utf-8') + f.write(''' + + +
+None (someone is slacking)
') - end_html() - -def show_puzzle(cat, points, points_dir, team='', passwd=''): - # Show puzzle in cat for points - start_html('%s for %s points' % (cat, points)) - fn = os.path.join(points_dir, 'index.html') - if os.path.exists(fn): - print('%d points for %s.
' % (points, team)) - print('Back to %s.
' % (cat, cat)) - end_html() - -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, team, passwd) - else: - try: - thekey = open('%s/key' % points_dir, encoding='utf-8').read().strip() - except IOError: - # If there's no key, this can never be solved. - thekey = False - if not teams.chkpasswd(team, passwd): - start_html('Wrong password') - end_html() - elif key != thekey: - show_puzzle(cat, points, points_dir, team, passwd) - elif int(points) in points_by_team.get((team, cat), set()): - start_html('Greedy greedy') - end_html() - else: - win(cat, team, points) - -main() - -# Local Variables: -# mode: python -# End: +sys.path.insert(0, '/usr/lib/ctf') +import puzzler +puzzler.main() diff --git a/puzzler.py b/puzzler.py new file mode 100755 index 0000000..027ac8d --- /dev/null +++ b/puzzler.py @@ -0,0 +1,193 @@ +#! /usr/bin/env python3 + +import cgi +import os +import fcntl +import re +import sys +import http.cookies +from urllib.parse import quote, unquote +import config +import pointscli +import teams + +datafile = config.datafile('puzzler.dat') +keysfile = config.get('puzzler', 'keys_file') +puzzles_dir = config.get('puzzler', 'dir') +cgi_url = config.get('puzzler', 'cgi_url') +base_url = config.get('puzzler', 'base_url') + +## +## This allows you to edit the URL and work on puzzles that haven't been +## unlocked yet. For now I think that's an okay vulnerability. It's a +## hacking contest, after all. +## + +cat_re = re.compile(r'^[a-z]+$') +points_re = re.compile(r'^[0-9]+$') + +def dbg(*vals): + print('<--: \nContent-type: text/html\n\n-->') + print(*vals) + print('') + + +points_by_cat = {} +points_by_team = {} +try: + for line in open(datafile, encoding='utf-8'): + cat, team, pts = [unquote(v) for v in line.strip().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 + + +c = http.cookies.SimpleCookie(os.environ.get('HTTP_COOKIE', '')) +try: + team = c['team'].value + passwd = c['passwd'].value +except KeyError: + team, passwd = None, None + +f = cgi.FieldStorage() +cat = f.getfirst('c') +points = f.getfirst('p') +team = f.getfirst('t', team) +passwd = f.getfirst('w', passwd) +key = f.getfirst('k') + +def start_html(title): + if os.environ.get('GATEWAY_INTERFACE'): + print('Content-type: text/html') + if team or passwd: + c = http.cookies.SimpleCookie() + if team: + c['team'] = team + if passwd: + c['passwd'] = passwd + print(c) + print() + print(''' + + + +
None (someone is slacking)
') + end_html() + +def win(cat, team, points): + start_html('Winner!') + points = int(points) + f = open(datafile, 'a', encoding='utf-8') + pointscli.submit(cat, team, points) + fcntl.lockf(f, fcntl.LOCK_EX) + f.write('%s\t%s\t%d\n' % (quote(cat), quote(team), points)) + print('%d points for %s.
' % (points, team)) + print('Back to %s.
' % (cgi_url, cat, cat)) + end_html() + +def get_key(cat, points): + for line in open(keysfile, encoding='utf-8'): + thiscat, thispoints, ret = line.split('\t', 2) + if (cat, points) == (thiscat, thispoints): + return ret.strip() + return False + +def main(): + cat_dir = safe_join(puzzles_dir, cat) + points_dir = safe_join(puzzles_dir, cat, points) + + if not cat_dir: + # Show categories + show_cats() + elif not points_dir: + # Show available puzzles in category + show_puzzles(cat, cat_dir) + else: + thekey = get_key(cat, points) + if not teams.chkpasswd(team, passwd): + start_html('Wrong password') + end_html() + elif key != thekey: + start_html('Wrong key') + end_html() + elif int(points) in points_by_team.get((team, cat), set()): + start_html('Greedy greedy') + end_html() + else: + win(cat, team, points) + +if __name__ == '__main__': + import optparse + + parser = optparse.OptionParser('%prog CATEGORY POINTS') + opts, args = parser.parse_args() + + if len(args) == 2: + cat, points = args + show_puzzle(cat, points) + else: + parser.print_usage() + + +# Local Variables: +# mode: python +# End: diff --git a/puzzles/bletchey/100/key b/puzzles/bletchley/100/key similarity index 100% rename from puzzles/bletchey/100/key rename to puzzles/bletchley/100/key diff --git a/puzzles/bletchey/100/key.png b/puzzles/bletchley/100/key.png similarity index 100% rename from puzzles/bletchey/100/key.png rename to puzzles/bletchley/100/key.png diff --git a/puzzles/bletchey/200/index.html b/puzzles/bletchley/200/index.html similarity index 100% rename from puzzles/bletchey/200/index.html rename to puzzles/bletchley/200/index.html diff --git a/puzzles/bletchey/200/key b/puzzles/bletchley/200/key similarity index 100% rename from puzzles/bletchey/200/key rename to puzzles/bletchley/200/key diff --git a/puzzles/bletchey/250/index.html b/puzzles/bletchley/250/index.html similarity index 100% rename from puzzles/bletchey/250/index.html rename to puzzles/bletchley/250/index.html diff --git a/puzzles/bletchey/250/key b/puzzles/bletchley/250/key similarity index 100% rename from puzzles/bletchey/250/key rename to puzzles/bletchley/250/key diff --git a/puzzles/bletchey/300/index.html b/puzzles/bletchley/300/index.html similarity index 100% rename from puzzles/bletchey/300/index.html rename to puzzles/bletchley/300/index.html diff --git a/puzzles/bletchey/300/key b/puzzles/bletchley/300/key similarity index 100% rename from puzzles/bletchey/300/key rename to puzzles/bletchley/300/key diff --git a/puzzles/bletchey/500/200601262232.ogg b/puzzles/bletchley/500/200601262232.ogg similarity index 100% rename from puzzles/bletchey/500/200601262232.ogg rename to puzzles/bletchley/500/200601262232.ogg diff --git a/puzzles/bletchey/500/cipher.txt b/puzzles/bletchley/500/cipher.txt similarity index 100% rename from puzzles/bletchey/500/cipher.txt rename to puzzles/bletchley/500/cipher.txt diff --git a/puzzles/bletchey/500/index.html b/puzzles/bletchley/500/index.html similarity index 100% rename from puzzles/bletchey/500/index.html rename to puzzles/bletchley/500/index.html diff --git a/puzzles/bletchey/500/key b/puzzles/bletchley/500/key similarity index 100% rename from puzzles/bletchey/500/key rename to puzzles/bletchley/500/key diff --git a/register.cgi b/register.cgi index 3dfb0c5..e6db08f 100755 --- a/register.cgi +++ b/register.cgi @@ -1,62 +1,6 @@ #! /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(''' - - - -%s (%d) | ' % (cat, points)) -print('
---|
') - scores = sorted([(s.team_points_in_cat(cat, team), team) for team in teams]) - for points, team in scores: - color = teamcolors[team] - print(' | ') -print(' ') - print('
%s (%d) | ' % (cat, score)) + print('
---|
') + scores = sorted([(s.team_points_in_cat(cat, team), team) for team in teams]) + for score, team in scores: + color = teamcolors[team] + print(' | ') + print(' ') + print('