Merge branch 'master' of ssh://cfl.lanl.gov/var/projects/gctf

This commit is contained in:
Paul S. Ferrell 2009-10-13 16:08:31 -06:00
commit b658035581
68 changed files with 622 additions and 451 deletions

View File

@ -26,6 +26,10 @@ target: $(PYC)
$(INSTALL) -d $(PYCDIR)/ctf
$(INSTALL) $(PYC) $(PYCDIR)/ctf
$(INSTALL) -d $(DESTDIR)/usr/lib/python2.6/site-packages/ctf
$(INSTALL) ctf/__init__.py $(DESTDIR)/usr/lib/python2.6/site-packages/ctf
$(INSTALL) ctf/config.py $(DESTDIR)/usr/lib/python2.6/site-packages/ctf
$(INSTALL) -d $(DESTDIR)/usr/sbin
$(INSTALL) ctfd.py $(DESTDIR)/usr/sbin
$(INSTALL) new-contest $(DESTDIR)/usr/sbin

97
ctf.css
View File

@ -7,7 +7,7 @@ html {
body {
font-family: sans-serif;
color: #fff;
margin: 50px 0 0 100px;
margin: 50px 0 0 110px;
padding: 10px;
max-width: 700px;
}
@ -30,6 +30,54 @@ h1:first-child:before {
content: "Capture The Flag: ";
}
/*** left side bar ***/
#navigation {
position: absolute;
background: #222;
opacity: 0.9;
top: 80px;
left: 0px;
padding: 0;
}
#navigation h3 {
font-size: 100%;
border-bottom: 2px solid #444;
}
#navigation ul {
list-style: none;
padding: 0;
margin: 0;
}
#navigation li a {
display: block;
height: 25px;
width: 90px;
padding: 5px;
margin: 5px;
background: inherit;
border-right: 4px solid #444;
color: #999;
text-transform: lowercase;
font-size: 0.9em;
}
#navigation li a:hover {
color: #f4f4f4;
background: #333;
border-right: 4px solid #2a2;
}
#navigation li .active {
color: #999;
background: #333;
border-right: 4px solid #444;
}
/**** body ****/
a img {
@ -64,6 +112,39 @@ th, td {
vertical-align: top;
}
p {
line-height: 1.4em;
margin-bottom: 20px;
color: #f4f4f4;
}
hr {
border: 1px solid #444;
}
dt {
white-space: pre;
background-color: #333;
padding: 5px;
border: 2px solid green;
border-bottom: none;
font-weight: bold;
}
dd {
border: 2px solid green;
margin: 0px;
padding: 5px;
background-color: #282828;
}
/**** special cases ****/
.wide {
max-width: inherit;
}
.scoreboard {
background: #222;
}
@ -72,24 +153,20 @@ th, td {
height: 400px;
}
p {
line-height: 1.4em;
margin-bottom: 20px;
color: #f4f4f4;
}
.solved {
text-decoration: line-through;
}
table.pollster {
margin-left: 5em;
margin-left: 5em;
}
table.pollster td {
padding: 2px 1em 2px 5px;
padding: 2px 1em 2px 5px;
}
table.pollster thead {
font-weight: bold;
font-weight: bold;
}

View File

@ -28,27 +28,33 @@ if 'home' in os.environ.get('SCRIPT_FILENAME', ''):
}
else:
# An actual installation
config = {'global':
{'data_dir': '/var/lib/ctf',
'base_url': '/',
'css_url': '/ctf.css',
'disabled_dir': '/var/lib/ctf/disabled',
'flags_dir': '/var/lib/ctf/flags',
'house_team': 'dirtbags',
'passwd': '/var/lib/ctf/passwd',
'team_colors': team_colors,
'poll_interval': 60,
'poll_timeout': 0.5,
'heartbeat_dir': '/var/lib/pollster',
'poll_dir': '/var/lib/www',
},
'puzzler':
{'dir': '/usr/lib/www/puzzler',
'cgi_url': '/puzzler.cgi',
'base_url': '/puzzler',
'keys_file': '/usr/lib/ctf/puzzler.keys',
},
}
config = {
'global':
{
'data_dir': '/var/lib/ctf',
'base_url': '/',
'css_url': '/ctf.css',
'disabled_dir': '/var/lib/ctf/disabled',
'flags_dir': '/var/lib/ctf/flags',
'house_team': 'dirtbags',
'passwd': '/var/lib/ctf/passwd',
'team_colors': team_colors,
},
'pollster':
{
'poll_interval': 60,
'poll_timeout': 0.5,
'heartbeat_dir': '/var/lib/pollster',
'results': '/var/lib/pollster/status.html',
},
'puzzler':
{
'dir': '/usr/lib/www/puzzler',
'cgi_url': '/puzzler.cgi',
'base_url': '/puzzler',
'keys_file': '/usr/lib/ctf/puzzler.keys',
},
}
def get(section, key):
return config[section][key]
@ -71,22 +77,45 @@ def datafile(filename):
def url(path):
return base_url + path
def start_html(title):
def start_html(title, hdr='', cls='', links=[], links_title=None):
ret = []
if os.environ.get('GATEWAY_INTERFACE'):
print('Content-type: text/html')
print()
print('''<?xml version="1.0" encoding="UTF-8"?>
ret.append('Content-type: text/html')
ret.append('')
ret.append('''<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>%s</title>
<link rel="stylesheet" href="%s" type="text/css" />
<title>%(title)s</title>
<link rel="stylesheet" href="%(css)s" type="text/css" />
%(hdr)s
</head>
<body>
<h1>%s</h1>
''' % (title, css, title))
<body class="%(class)s">
<h1>%(title)s</h1>
<div id="navigation">
<ul>
<li><a href="%(base)sintro.html">Intro/Rules</a></li>
<li><a href="%(base)sservices.html">Svc flags</a></li>
<li><a href="%(base)stanks/results.cgi">Tanks</a></li>
<li><a href="%(base)spuzzler.cgi">Puzzles</a></li>
<li><a href="%(base)sscoreboard.cgi">Scoreboard</a></li>
</ul>
''' % {'title': title,
'css': css,
'hdr': hdr,
'base': base_url,
'class': cls})
if links:
if links_title:
ret.append('<h3>%s</h3>' % links_title)
else:
ret.append('<hr/>')
for url, name in links:
ret.append('<li><a href="%s">%s</a></li>' % (url, name))
ret.append(' </div>')
return '\n'.join(ret)
def end_html():
print('</body></html>')
return '</body></html>'

View File

@ -59,12 +59,13 @@ set xtics nomirror
set ytics nomirror
set nokey
set terminal png transparent size 640,200 x000000 xffffff
set output "%(pngout)s"
set output "%(pngout)s,tmp"
plot %(plot)s\n''' % {'plot': ','.join(plotparts),
'pngout': pngout})
instructions.flush()
gp = os.system('gnuplot %s 2>/dev/null </dev/null' % instructions.name)
os.rename("%s,tmp" % pngout, pngout)
if __name__ == '__main__':
main()

View File

@ -66,9 +66,10 @@ def start_html(title):
if passwd:
c['passwd'] = passwd
print(c)
config.start_html(title)
print(config.start_html(title))
end_html = config.end_html
def end_html():
print(config.end_html())
def safe_join(*args):
safe = list(args[:1])
@ -156,7 +157,7 @@ def main():
# Show available puzzles in category
show_puzzles(cat, cat_dir)
else:
thekey = get_key(cat, points)
thekeys = get_key(cat, points)
if not teams.chkpasswd(team, passwd):
start_html('Wrong password')
end_html()

View File

@ -7,26 +7,7 @@ import string
from . import teams
from . import config
def head(title):
return '''<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Team Registration</title>
<link rel="stylesheet" href="%s" type="text/css" />
</head>
<body>
<h1>%s</h1>
''' % (config.css, title)
def foot():
return '''</body></html>'''
def main():
print('Content-type: text/html')
print()
f = cgi.FieldStorage()
team = f.getfirst('team', '')

View File

@ -15,20 +15,8 @@ def main():
categories = [(cat, s.cat_points(cat)) for cat in s.categories()]
print('Content-type: text/html')
print('Refresh: 10')
print()
print('''<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Scoreboard</title>
<link rel="stylesheet" href="%sctf.css" type="text/css" />
</head>
<body style="max-width: inherit;">
<h1>Scoreboard</h1>
''' % config.base_url)
print(config.start_html('Scoreboard', cls='wide'))
print('<table class="scoreboard">')
print('<tr>')
print('<th>Overall</th>')
@ -71,9 +59,8 @@ def main():
<p class="histogram">
<img src="histogram.png" alt="scores over time" />
</p>
</body>
</html>''')
''')
print(config.end_html())
if __name__ == '__main__':
main()

11
ctfd.py
View File

@ -20,13 +20,12 @@ def chart(s):
def reap():
try:
while True:
os.waitpid(0, os.WNOHANG)
pid, ret = os.waitpid(0, os.WNOHANG)
if not pid:
break
except OSError:
pass
def sigchld(signum, frame):
do_reap = True
def main():
p = optparse.OptionParser()
p.add_option('-p', '--genpass', dest='cat', default=None,
@ -39,13 +38,11 @@ def main():
pointsrv = pointsd.start()
flagsrv = flagd.start()
signal.signal(signal.SIGCHLD, sigchld)
s = pointsrv.store
slen = 0
while True:
if do_reap:
reap()
asyncore.loop(timeout=30, use_poll=True, count=1)
reap()
if len(s) > slen:
slen = len(s)
chart(s)

View File

@ -17,6 +17,37 @@ opts, args = p.parse_args()
keys = []
js = '''
<script type="text/javascript">
function readCookie(key) {
var s = key + '=';
var toks = document.cookie.split(';');
for (var i = 0; i < toks.length; i++) {
var tok = toks[i];
while (tok.charAt(0) == ' ') {
tok = tok.substring(1, tok.length);
}
if (tok.indexOf(s) == 0) {
return tok.substring(s.length, tok.length);
}
}
return null;
}
function getTeamInfo() {
team = readCookie('team');
passwd = readCookie('passwd');
if (team != null) {
document.getElementById("form").t.value = team;
}
if (passwd != null) {
document.getElementById("form").w.value = passwd;
}
}
window.onload = getTeamInfo;
</script>
'''
for cat in os.listdir(opts.puzzles):
dirname = os.path.join(opts.puzzles, cat)
for points in os.listdir(dirname):
@ -46,47 +77,7 @@ for cat in os.listdir(opts.puzzles):
title = '%s for %s points' % (cat, points)
f = open(os.path.join(outdir, 'index.html'), 'w', encoding='utf-8')
f.write('''<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>%(title)s</title>
<link rel="stylesheet" href="%(css)s" type="text/css" />
<script type="text/javascript">
function readCookie(key) {
var s = key + '=';
var toks = document.cookie.split(';');
for (var i = 0; i < toks.length; i++) {
var tok = toks[i];
while (tok.charAt(0) == ' ') {
tok = tok.substring(1, tok.length);
}
if (tok.indexOf(s) == 0) {
return tok.substring(s.length, tok.length);
}
}
return null;
}
function getTeamInfo() {
team = readCookie('team');
passwd = readCookie('passwd');
if (team != null) {
document.getElementById("form").t.value = team;
}
if (passwd != null) {
document.getElementById("form").w.value = passwd;
}
}
window.onload = getTeamInfo;
</script>
</head>
<body>
<h1>%(title)s</h1>
''' % {'title': title,
'css': config.css})
f.write(config.start_html(title, js))
if readme:
f.write('<div class="readme">%s</div>\n' % readme)
if files:
@ -108,11 +99,10 @@ for cat in os.listdir(opts.puzzles):
<input type="submit" />
</fieldset>
</form>
</body>
</html>
''' % {'cgi': config.get('puzzler', 'cgi_url'),
'cat': cat,
'points': points})
f.write(config.end_html())
f = open(opts.keyfile, 'w', encoding='utf-8')
for key in keys:

View File

@ -13,6 +13,8 @@ rotate /var/lib/ctf/scores.dat
rotate /var/lib/ctf/passwd
rm -f /var/lib/ctf/flags/* || true
sv restart /var/service/ctf
echo "Things you may want to tweak:"
find /var/lib/ctf/disabled
find /var/lib/kevin/tokens

View File

@ -1,8 +1,9 @@
#! /bin/sh
case "$REMOTEADDR" in
10.0.0.[2-254])
touch /var/lib/pollster/$REMOTEADDR
ip=$(echo $UDPREMOTEADDR | cut -d: -f1)
case "$ip" in
10.0.0.*)
touch /var/lib/pollster/$ip
;;
esac
echo 'Hello.'

3
pollster/log.run.pollster Executable file
View File

@ -0,0 +1,3 @@
#! /bin/sh
exec logger -t pollster

View File

@ -2,45 +2,25 @@
import os
import re
import io
import sys
import time
import socket
import traceback
import threading
import queue
from ctf import config
from ctf import pointscli
DEBUG = False
POLL_INTERVAL = config.get('poll_interval')
IP_DIR = config.get('heartbeat_dir')
REPORT_PATH = config.get('poll_dir')
SOCK_TIMEOUT = config.get('poll_timeout')
DEBUG = False
POLL_INTERVAL = config.get('pollster', 'poll_interval')
IP_DIR = config.get('pollster', 'heartbeat_dir')
REPORT_PATH = config.get('pollster', 'results')
SOCK_TIMEOUT = config.get('pollster', 'poll_timeout')
class PointSubmitter(threading.Thread):
''' Pulls point allocations from the queue and submits them. '''
def __init__(self, point_queue):
threading.Thread.__init__(self)
self.point_queue = point_queue
self.sock = pointscli.makesock('localhost')
def run(self):
# loop forever
while(True):
cat, team, score = self.point_queue.get()
if None in [cat, team, score]:
continue
try:
pointscli.submit(cat, team, score, sock=self.sock)
except ValueError:
print('pollster: error submitting score (%s, %s, %d)' % (cat, team, score))
traceback.print_exc()
def socket_poll(ip, port, msg, prot, max_recv=1):
''' Connect via socket to the specified <ip>:<port> using the
specified <prot>, send the specified <msg> and return the
specified <prot>, send the specified <msg> and return the
response or None if something went wrong. <max_recvs> specifies
how many times to read from the socket (default to once). '''
@ -51,7 +31,7 @@ def socket_poll(ip, port, msg, prot, max_recv=1):
print('pollster: create socket failed (%s)' % e)
traceback.print_exc()
return None
sock.settimeout(SOCK_TIMEOUT)
# connect
@ -70,19 +50,16 @@ def socket_poll(ip, port, msg, prot, max_recv=1):
sock.send(msg)
# get a response
resp = ''
resp = []
try:
# first read
data = sock.recv(1024)
resp += data.decode('utf-8')
max_recv -= 1
# remaining reads as necessary until timeout or socket closes
while(len(data) > 0 and max_recv > 0):
# read from the socket until <max_recv> responses or read,
# a timeout occurs, the socket closes, or some other exception
# is raised
for i in range(max_recv):
data = sock.recv(1024)
resp += data.decode('utf-8')
max_recv -= 1
sock.close()
if len(data) == 0:
break
resp.append(data)
except socket.timeout as e:
print('pollster: timed out waiting for a response from %s:%d (%s)' % (ip, port, e))
@ -90,11 +67,13 @@ def socket_poll(ip, port, msg, prot, max_recv=1):
except Exception as e:
print('pollster: receive from %s:%d failed (%s)' % (ip, port, e))
traceback.print_exc()
sock.close()
if len(resp) == 0:
return None
return resp
return b''.join(resp)
# PUT POLLS FUNCTIONS HERE
# Each function should take an IP address and return a team name or None
@ -105,14 +84,14 @@ def poll_fingerd(ip):
resp = socket_poll(ip, 79, b'flag\n', socket.SOCK_STREAM)
if resp is None:
return None
return resp.strip('\r\n')
return resp.strip(b'\r\n')
def poll_noted(ip):
''' Poll the noted service. Returns None or a team name. '''
resp = socket_poll(ip, 4000, b'rflag\n', socket.SOCK_STREAM)
if resp is None:
return None
return resp.strip('\r\n')
return resp.strip(b'\r\n')
def poll_catcgi(ip):
''' Poll the cat.cgi web service. Returns None or a team name. '''
@ -121,79 +100,58 @@ def poll_catcgi(ip):
if resp is None:
return None
content = resp.split('\r\n\r\n')
content = resp.split(b'\r\n\r\n')
if len(content) < 3:
return None
content = content[1].split('\r\n')
content = content[1].split(b'\r\n')
try:
content_len = int(content[0])
except Exception as e:
return None
if content_len <= 0:
return None
return content[1].strip('\r\n')
return content[1].strip(b'\r\n')
def poll_tftpd(ip):
''' Poll the tftp service. Returns None or a team name. '''
resp = socket_poll(ip, 69, b'\x00\x01' + b'flag' + b'\x00' + b'octet' + b'\x00', socket.SOCK_DGRAM)
if resp is None:
return None
if len(resp) <= 5:
return None
resp = resp.split('\n')[0]
return resp[4:].strip('\r\n')
resp = resp.split(b'\n')[0]
# ack
_ = socket_poll(ip, 69, b'\x00\x04' + resp[2:4], socket.SOCK_DGRAM, 0)
return resp[4:].strip(b'\r\n')
# PUT POLL FUNCTIONS IN HERE OR THEY WONT BE POLLED
POLLS = {
'fingerd' : poll_fingerd,
'noted' : poll_noted,
'noted' : poll_noted,
'catcgi' : poll_catcgi,
'tftpd' : poll_tftpd,
'tftpd' : poll_tftpd,
}
ip_re = re.compile('(\d{1,3}\.){3}\d{1,3}')
# start point submitter thread
point_queue = queue.Queue()
t = PointSubmitter(point_queue)
t.start()
# loop forever
while True:
t_start = time.time()
# gather the list of IPs to poll
try:
ips = os.listdir(IP_DIR)
except Exception as e:
print('pollster: could not list dir %s (%s)' % (IP_DIR, e))
traceback.print_exc()
try:
os.remove(REPORT_PATH)
except Exception as e:
pass
try:
out = open(REPORT_PATH, 'w')
except Exception as e:
out = None
pass
if out is not None:
out.write('<html>\n<head>\n')
out.write('<title>Pollster Results</title>\n')
out.write('<link rel="stylesheet" href="ctf.css" type="text/css" media="all" />\n')
out.write('</head><body>\n<h1>Polling Results</h1>\n')
ips = os.listdir(IP_DIR)
out = io.StringIO()
out.write(config.start_html('Team Service Availability'))
for ip in ips:
# check file name format is ip
if ip_re.match(ip) is None:
continue
@ -217,7 +175,7 @@ while True:
# perform polls
for service,func in POLLS.items():
team = func(ip)
team = func(ip).decode('utf-8')
if team is None:
team = 'dirtbags'
@ -227,11 +185,11 @@ while True:
if out is not None:
out.write('<tr><td>%s</td><td>%s</td>\n' % (service, team))
point_queue.put((service, team, 1))
pointscli.submit('svc.' + service, team, 1)
if out is not None:
out.write('</table>\n')
if DEBUG is True:
print('+-----------------------------------------+')
@ -240,9 +198,9 @@ while True:
sleep_time = POLL_INTERVAL - exec_time
if out is not None:
out.write('<p><b>Next poll in: %ds</b></p>\n' % sleep_time)
out.write('</body>\n</html>\n')
out.close()
out.write(config.end_html())
open(REPORT_PATH, 'w').write(out.getvalue())
# sleep until its time to poll again
time.sleep(sleep_time)

View File

@ -1,3 +1,3 @@
#! /bin/sh
exec udpsvd 0 9 /usr/sbin/in.heartbeatd
exec udpsvd 0 9 envuidgid ctf /usr/sbin/in.heartbeatd

View File

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 184 KiB

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

View File

Before

Width:  |  Height:  |  Size: 362 KiB

After

Width:  |  Height:  |  Size: 362 KiB

View File

@ -0,0 +1 @@

View File

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

1
puzzles/hispaniola/5/key Normal file
View File

@ -0,0 +1 @@

View File

@ -0,0 +1,3 @@
Give the <b>entire sequence</b>. The symbols are here, in no particular order, for you to copy and paste.<br />
Make sure to include spaces between symbols (but no leading or trailing spaces).<br /><br />
&#9685; &#9873; &#9829; &#9698; &#9733;

View File

@ -0,0 +1 @@
The "hispaniola" category requires contenstants to treasure-hunt for tangible items to learn the keys to each puzzle.

View File

@ -1 +0,0 @@
You're well on your way :)

View File

@ -1 +0,0 @@
‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽‽

View File

@ -1 +0,0 @@
-462766

View File

@ -1 +0,0 @@
3acd767f2717b84076cdcd18e882f01d

View File

@ -1 +0,0 @@
&#9873; _ _ _ _ &#9698; _ _ _ _ &#9829; _ _ _ _ &#9733; _ _ _ _ &#9685; _ _ _ _

View File

@ -3,7 +3,7 @@ html,body {
min-height: 100%;
background-color: #000000;
background-image: url(",binary.png");
background-repeat: repeat-x repeat-y;
background-repeat: repeat;
margin: 0;
padding: 0;
}
@ -52,3 +52,11 @@ h1,h2,h3,h4 {
margin: 2em auto 2em auto;
border-bottom: 1px dotted #222;
}
.error {
padding: 1em;
background: #fff;
color: red;
border: 1px solid red;
font-weight: bold;
}

View File

@ -1 +1 @@
../1/,binary.png
../10/,binary.png

View File

@ -1 +1 @@
../1/,ctf.css
../10/,ctf.css

View File

@ -1 +1 @@
../1/,binary.png
../10/,binary.png

View File

@ -1 +1 @@
../1/,ctf.css
../10/,ctf.css

Binary file not shown.

Before

Width:  |  Height:  |  Size: 626 B

After

Width:  |  Height:  |  Size: 17 B

View File

@ -0,0 +1 @@
../10/,binary.png

Before

Width:  |  Height:  |  Size: 626 B

After

Width:  |  Height:  |  Size: 17 B

View File

@ -1,54 +0,0 @@
html,body {
height: 100%;
min-height: 100%;
background-color: #000000;
background-image: url(",binary.png");
background-repeat: repeat-x repeat-y;
margin: 0;
padding: 0;
}
#wrapper {
min-height: 100%;
height: 100%;
width: 800px;
margin: 0 auto;
border-left: 2px solid #009900;
border-right: 2px solid #009900;
font: .9em monospace;
color: #009900;
padding: 0;
background: #000;
}
#content {
padding: 2em 1.5em 2em 1.5em;
}
#footer {
padding: 0;
margin: 0;
height: 2em;
line-height: 2em;
width: 800px;
text-align: center;
}
input {
background-color: #222;
color: #fff;
border: 1px solid #009900;
padding: 1px 2px 1px 2px;
}
h1,h2,h3,h4 {
padding-bottom: 5px;
}
.vertsep {
width: 100%;
height: 1px;
padding: 0;
margin: 2em auto 2em auto;
border-bottom: 1px dotted #222;
}

1
puzzles/webapp/40/,ctf.css Symbolic link
View File

@ -0,0 +1 @@
../10/,ctf.css

View File

@ -0,0 +1 @@
../10/,binary.png

1
puzzles/webapp/50/,ctf.css Symbolic link
View File

@ -0,0 +1 @@
../10/,ctf.css

89
puzzles/webapp/50/5.cgi Executable file
View File

@ -0,0 +1,89 @@
#!/usr/bin/python
import os
import cgi
import cgitb
cgitb.enable(context=10)
if os.environ.has_key('QUERY_STRING'):
os.environ['QUERY_STRING'] = ''
fields = cgi.FieldStorage()
print 'Content-Type: text/html'
print ''
print '''
<html>
<head>
<title>5</title>
<link rel="stylesheet" type="text/css" href=",ctf.css" media="all" />
</head>
<body>
<div id="wrapper">
<div id="content">
<h1>Web Application Challenge 5</h1>
<p>Through some manipulation or interpretation of this CGI script
and the HTML page(s) that it generates, a 10 character key can be
found.</p>
<p><strong>Find the key!</strong></p>
<div class="vertsep"></div>
'''
PRODUCT_NAME = "Alex Brugh"
QUANT_LIMIT = 1
def purchase_success(quantity):
print '''
<p>Congratulations, your order for %d "%s" has been placed.</p>
''' % (quantity, PRODUCT_NAME)
class InvalidQuantityError(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)
quantity = None
if fields.has_key('quantity') and fields.has_key('product') and fields['product'].value == PRODUCT_NAME:
product = fields['product'].value
try:
quantity = int(fields['quantity'].value)
if quantity > QUANT_LIMIT:
# key = eVkIwHzOok
raise InvalidQuantityError("%d is not a valid quantity (limit %d)" % (quantity, QUANT_LIMIT))
except ValueError:
print '''
<p class="error">There was an error with your order request. Sorry.</p>
'''
quantity = None
if quantity is not None:
purchase_success(quantity)
else:
print '''
<h2>SALE: %s</h2>
<p>Use the order form below to place an order.</p>
<form method="post" action="5.cgi">
<em>Orders for "%s" are limited to 1 per customer.</em>
<br /><br />
<input type="submit" value="Order!" />
<input type="hidden" name="product" value="%s" />
<input type="hidden" name="quantity" value="1" />
</form>
''' % (PRODUCT_NAME, PRODUCT_NAME, PRODUCT_NAME)
print '''
</div>
<div id="footer">
<p>Copyright &copy; 2009 LANS, LLC.</p>
</div>
</div>
</body>
</html>
'''

1
puzzles/webapp/50/key Normal file
View File

@ -0,0 +1 @@
eVkIwHzOok

View File

@ -0,0 +1 @@
../10/,binary.png

1
puzzles/webapp/60/,ctf.css Symbolic link
View File

@ -0,0 +1 @@
../10/,ctf.css

72
puzzles/webapp/60/6.cgi Executable file
View File

@ -0,0 +1,72 @@
#!/usr/bin/python
import os
import cgi
import cgitb
cgitb.enable(context=10)
#if os.environ.has_key('QUERY_STRING'):
# os.environ['QUERY_STRING'] = ''
fields = cgi.FieldStorage()
import Cookie
c = Cookie.SimpleCookie()
c['key'] = 'QJebByJaKX'
c['content'] = '<p><em>Maybe I should have used sessions...</em></p>'
print 'Content-Type: text/html\n%s\n\n\n' % c
print ''
print '''
<html>
<head>
<title>6</title>
<link rel="stylesheet" type="text/css" href=",ctf.css" media="all" />
<script type="text/javascript">
function readCookie(key) {
var s = key + '=';
var toks = document.cookie.split(';');
for (var i = 0; i < toks.length; i++) {
var tok = toks[i];
while (tok.charAt(0) == ' ') {
tok = tok.substring(1, tok.length);
}
if (tok.indexOf(s) == 0) {
return tok.substring(s.length, tok.length);
}
}
return null;
}
function setContent() {
content = readCookie("content");
document.getElementById("stuff").innerHTML = content.substring(1, content.length-1);
}
window.onload = setContent;
</script>
</head>
<body>
<div id="wrapper">
<div id="content">
<h1>Web Application Challenge 6</h1>
<p>Through some manipulation or interpretation of this CGI script
and the HTML page(s) that it generates, a 10 character key can be
found.</p>
<p><strong>Find the key!</strong></p>
<div class="vertsep"></div>
<div id="stuff"></div>
'''
print '''
</div>
<div id="footer">
<p>Copyright &copy; 2009 LANS, LLC.</p>
</div>
</div>
</body>
</html>
'''

1
puzzles/webapp/60/key Normal file
View File

@ -0,0 +1 @@
QJebByJaKX

View File

@ -0,0 +1 @@
../10/,binary.png

1
puzzles/webapp/70/,ctf.css Symbolic link
View File

@ -0,0 +1 @@
../10/,ctf.css

86
puzzles/webapp/70/7.cgi Executable file
View File

@ -0,0 +1,86 @@
#!/usr/bin/python
import os
import cgi
import cgitb
cgitb.enable(context=10)
#if os.environ.has_key('QUERY_STRING'):
# os.environ['QUERY_STRING'] = ''
fields = cgi.FieldStorage()
import Cookie
c = Cookie.SimpleCookie(os.environ.get('HTTP_COOKIE', ''))
content = {
'joke1' : '<p>An infinite number of mathematicians walk into a bar. The first one orders a beer. The second orders half a beer. The third, a quarter of a beer. The bartender says <em>You are all idiots!</em> and pours two beers.<p>',
'joke2' : '<p>Two atoms are talking. One of them says <em>I think I lost an electron!</em> and the other says <em>Are you sure?</em> The first replies <em>Yeah, I am positive!</em></p>',
}
if c.has_key('content_name') and c.has_key('content'):
k = c['content_name'].value
try:
c['content'] = content[k]
except KeyError:
c['content'] = '<p><em>key = s4nNlaMScV</em></p>'
else:
c['content_name'] = 'joke1';
c['content'] = content['joke1']
print 'Content-Type: text/html\n%s\n\n\n' % c
print ''
print '''
<html>
<head>
<title>7</title>
<link rel="stylesheet" type="text/css" href=",ctf.css" media="all" />
<script type="text/javascript">
function readCookie(key) {
var s = key + '=';
var toks = document.cookie.split(';');
for (var i = 0; i < toks.length; i++) {
var tok = toks[i];
while (tok.charAt(0) == ' ') {
tok = tok.substring(1, tok.length);
}
if (tok.indexOf(s) == 0) {
return tok.substring(s.length, tok.length);
}
}
return null;
}
function getContent() {
content = readCookie("content");
document.getElementById("stuff").innerHTML = content.substring(1, content.length-1);
}
window.onload = getContent;
</script>
</head>
<body>
<div id="wrapper">
<div id="content">
<h1>Web Application Challenge 7</h1>
<p>Through some manipulation or interpretation of this CGI script
and the HTML page(s) that it generates, a 10 character key can be
found.</p>
<p><strong>Find the key!</strong></p>
<div class="vertsep"></div>
<div id="stuff"></div>
'''
print '''
</div>
<div id="footer">
<p>Copyright &copy; 2009 LANS, LLC.</p>
</div>
</div>
</body>
</html>
'''

1
puzzles/webapp/70/key Normal file
View File

@ -0,0 +1 @@
s4nNlaMScV

View File

@ -0,0 +1,14 @@
10: the key is in the generated source.
20: enter a non-integer into form field and submit. the key is in the resulting
traceback.
30: change the value in the GET request to a non-integer. the key is in the
resulting traceback.
40: change the value in the POST request to a non-integer. the key is in the
resulting traceback.
50: change the quantity value (hidden form field) to something greater than the
stated quantity limit. the key is in the resulting traceback. entering non-
integers is caught and handled, so that no longer works.
60: the key is in the cookie. note the javascript that reads a value from the
cookie, hopefully causing the player to take a look at the cookie.
70: modify the cookie's content_name field to something invalid, reload the page
and the key will be printed on the page.

View File

@ -7,7 +7,7 @@ skills at designing those wits to survive.
<H2>Programming Your Tank</H2>
Your tanks are programmed using the Super Useful Command and Kontrol language,
the very best in laser tank AI languages. It includes amazing feature such
the very best in laser tank AI languages. It includes amazing features such
as comments (Started by a #, ended at EOL), logic, versatility, and
semi-colons (all lines must end in one). As with all new military systems
it utilizes only integers; we must never rest in our

View File

@ -4,9 +4,8 @@ def mkDocTable(objects):
objects.sort(lambda o1, o2: cmp(o1.__doc__, o2.__doc__))
for object in objects:
print '<table class="docs">'
if object.__doc__ is None:
print '<tr><th>%s<tr><td colspan=2>Bad object' % \
print '<table><tr><th>%s<tr><td colspan=2>Bad object</table>' % \
xml.sax.saxutils.escape(str(object))
continue
text = object.__doc__
@ -23,5 +22,5 @@ def mkDocTable(objects):
body = '\n'.join(body)
print '<DL><DT><DIV class="tab">%s</DIV></DT><DD>%s</DD></DL>' % (head, body)
#print '<tr><th>%s<th>Intentionally blank<th><tr><td colspan=3>%s' % (head, body)
print '</table>'

View File

@ -14,6 +14,8 @@ MAX_HIST = 30
HIST_STEP = 100
key = 'tanks:::2bac5e912ff2e1ad559b177eb5aeecca'
running = True
class Flagger(asynchat.async_chat):
"""Use to connect to flagd and submit the current flag holder."""
@ -31,6 +33,7 @@ class Flagger(asynchat.async_chat):
def handle_error(self):
# If we lose the connection to flagd, nobody can score any
# points. Terminate everything.
running = False
asyncore.close_all()
asynchat.async_chat.handle_error(self)
@ -84,6 +87,8 @@ def main():
lastrun = 0
while True:
asyncore.loop(60, count=1)
if not running:
break
now = time.time()
if now - lastrun >= 60:
run_tanks(args, turns, flagger)

View File

@ -1,104 +0,0 @@
/**** document ****/
html {
background: #222 url(grunge.png) repeat-x;
}
body {
font-family: sans-serif;
color: #eee;
margin: 50px 0 0 100px;
padding: 10px;
max-width: 700px;
}
/**** heading ****/
h1:first-child {
text-transform: lowercase;
font-size: 1.6em;
/* background-color: #222; */
/* opacity: 0.9; */
padding: 3px;
color: #2a2;
margin: 0 0 1em 70px;
}
h1:first-child:before {
color: #fff;
letter-spacing: -0.1em;
content: "Capture The Flag: ";
}
/**** body ****/
a img {
border: 0px;
}
a {
text-decoration: none;
color: #2a2;
font-weight: bold;
}
a:hover {
color: #fff;
background: #2a2;
font-weight: bold;
}
h1, h2, h3 {
color: #999;
letter-spacing: -0.05em;
}
code, pre, .readme, div.errors {
color: #fff;
background-color: #555;
margin: 1em;
}
th, td {
vertical-align: top;
}
.scoreboard td {
height: 400px;
}
p {
line-height: 1.4em;
margin-bottom: 20px;
color: #f4f4f4;
}
dt {
white-space: pre;
}
dt div.tab {
background-color: #333;
display: inline-block;
padding: 5px;
border: 3px solid green;
border-bottom: none;
font-weight: bold;
}
dd {
border: 3px solid green;
margin: 0px;
padding: 5px;
background-color: #282828;
}
fieldset * {
margin: 3px;
}
table.results td, th{
padding : 3px;
font-weight : bold;
}

View File

@ -1,10 +1,9 @@
#!/usr/bin/python
print """Content-Type: text/html\n\n"""
print """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">\n\n"""
import cgitb; cgitb.enable()
import os
import sys
from ctf import config
try:
from tanks import Program, setup, conditions, actions, docs
@ -15,10 +14,13 @@ except:
sys.path.append(os.path.join('/', *path))
import Program, setup, conditions, actions, docs
print open('head.html').read() % "Documentation"
print '<BODY>'
print '<H1>Pflanzarr Documentation</H1>'
print open('links.html').read()
print(config.start_html('Tanks Documentation',
links_title='Tanks',
links=[('docs.cgi', 'Docs'),
('results.cgi', 'Results'),
('submit.html', 'Submit'),
('errors.cgi', 'My Errors')]))
print Program.__doc__
print '<H3>Setup Actions:</H3>'
@ -34,4 +36,4 @@ print '<H3>Actions:</H3>'
print 'These actions are not for cowards. Remember, if actions contradict, your tank will simply do the last thing it was told in a turn. If ordered to hop on a plane to hell it will gladly do so. If order to make tea shortly afterwards, it will serve it politely and with cookies instead.<P>'
docs.mkDocTable(actions.actions.values())
print '</body></html>'
print(config.end_html())

View File

@ -1,7 +1,5 @@
#!/usr/bin/python3
print("""Content-Type: text/html\n\n""")
print("""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Strict//EN">\n\n""")
import cgi
import cgitb; cgitb.enable()
import sys
@ -20,15 +18,18 @@ except:
path = '/home/pflarr/repos/gctf/'
sys.path.append(path)
from ctf import teams
from ctf import config
teams.build_teams()
head = open('head.html').read() % "Error Report"
print(head)
print('<H1>Your Errors</H1>')
print(open('links.html').read())
print(config.start_html('Tanks Errors',
links_title='Tanks',
links=[('docs.cgi', 'Docs'),
('results.cgi', 'Results'),
('submit.html', 'Submit'),
('errors.cgi', 'My Errors')]))
def done():
print('</body></html>')
print(config.end_html())
sys.exit(0)
fields = cgi.FieldStorage()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -1,5 +0,0 @@
<html>
<head>
<link href="ctf.css" rel="stylesheet" type="text/css">
<title>%s</title>
</head>

View File

@ -1,4 +0,0 @@
<a href="docs.cgi">Documentation</a> |
<a href="results.cgi">Results</a> |
<a href="submit.html">Submit</a> |
<a href="errors.cgi">My Errors</a>

View File

@ -2,16 +2,16 @@
import cgitb; cgitb.enable()
import os
from ctf import config
import Config
print """Content-Type: text/html\n\n"""
print """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">\n\n"""
head = open('head.html').read() % "Pflanzarr Results"
print head
print "<H1>Results</H1>"
print open('links.html').read()
print(config.start_html('Tanks Results',
links_title='Tanks',
links=[('docs.cgi', 'Docs'),
('results.cgi', 'Results'),
('submit.html', 'Submit'),
('errors.cgi', 'My Errors')]))
try:
winner = open(os.path.join(Config.DATA_PATH, 'winner')).read()
except:
@ -52,4 +52,4 @@ for num in gameNums:
print '<a href="results/%d/game.avi">v</a>' % num,
print '<a href="results/%d/results.html">r</a>' % num
print '</body></html>'
print(config.end_html())

View File

@ -1,7 +1,5 @@
#!/usr/bin/python3
print("Content-Type: text/html\n\n")
print("""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Strict//EN">\n\n""")
import cgi
import cgitb; cgitb.enable()
import os
@ -20,15 +18,18 @@ except:
path = '/home/pflarr/repos/gctf/'
sys.path.append(path)
from ctf import teams
from ctf import config
teams.build_teams()
head = open('head.html').read() % "Submission Results"
print(head)
print("<H1>Results</H1>")
print(open('links.html').read())
print(config.start_html('Tanks Submission',
links_title='Tanks',
links=[('docs.cgi', 'Docs'),
('results.cgi', 'Results'),
('submit.html', 'Submit'),
('errors.cgi', 'My Errors')]))
def done():
print('</body></html>')
print(config.end_html())
sys.exit(0)
fields = cgi.FieldStorage()
@ -36,23 +37,23 @@ team = fields.getfirst('team', '').strip()
passwd = fields.getfirst('passwd', '').strip()
code = fields.getfirst('code', '')
if not team:
print('<p>No team specified'); done()
print('<p>No team specified</p>'); done()
elif not passwd:
print('<p>No password given'); done()
print('<p>No password given</p>'); done()
elif not code:
print('<p>No program given.'); done()
print('<p>No program given.</p>'); done()
if team not in teams.teams:
print('<p>Team is not registered.'); done()
print('<p>Team is not registered.</p>'); done()
if passwd != teams.teams[team][0]:
print('<p>Invalid password.'); done()
print('<p>Invalid password.</p>'); done()
path = os.path.join(Config.DATA_PATH, 'ai/players', quote(team) )
file = open(path, 'w')
file.write(code)
file.close()
print("<P>Submission Successful")
print("<p>Submission successful.</p>")
done()

View File

@ -1,16 +1,33 @@
<html>
<head>
<link href="ctf.css" rel="stylesheet" type="text/css">'
<title>Program Submission</title>"
</head>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Tanks Submission</title>
<link rel="stylesheet" href="/ctf.css" type="text/css" />
</head>
<body class="">
<h1>Tanks Submission</h1>
<div id="navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/intro.html">Intro/Rules</a></li>
<li><a href="/services.html">Svc flags</a></li>
<li><a href="/puzzler.cgi">Puzzles</a></li>
<li><a href="/scoreboard.cgi">Scoreboard</a></li>
</ul>
<h3>Tanks</h3>
<li><a href="docs.cgi">Docs</a></li>
<li><a href="results.cgi">Results</a></li>
<li><a href="submit.html">Submit</a></li>
<li><a href="errors.cgi">My Errors</a></li>
</div>
<body>
<H1>Program Submission</H1>
<p>
<a href="docs.cgi">Documentation</a> |
<a href="results.cgi">Results</a> |
<a href="submit.html">Submit</a> |
<a href="errors.cgi">My Errors</a>
<form action="submit.cgi" method="post">
<fieldset>
<legend>Your program:</legend>