bambi7-service-fireworx

ESDSA-signed firework A/D service for BambiCTF7 in 2022
git clone https://git.sinitax.com/sinitax/bambi7-service-fireworx
Log | Files | Refs | README | LICENSE | sfeed.txt

app.py (16029B)


      1from aiohttp import web, WSCloseCode
      2from aiohttp_session import get_session, new_session
      3from aiohttp_session.cookie_storage import EncryptedCookieStorage
      4from base64 import urlsafe_b64decode
      5from cryptography import fernet
      6from datetime import datetime
      7from hashlib import md5
      8
      9import aiohttp_session
     10import aiohttp_session.redis_storage
     11import aioredis
     12import aiosqlite
     13import asyncio
     14import crypto
     15import json
     16import os
     17import random
     18import sys
     19import traceback
     20
     21base_template = """
     22<html>
     23<head>
     24    <title>Fireworx</title>
     25    <link rel="icon" type="image/x-icon" href="/static/favicon.png">
     26    <link rel="stylesheet" href="/static/style.css">
     27    <link rel="stylesheet" href="/static/glitch.css">
     28</head>
     29{body}
     30</html>
     31"""
     32
     33main_template = base_template.format(body="""
     34<body class=mainpage>
     35<div id=main>
     36    {navbar}
     37</div>
     38<div id=canvas_overlay>Click here!</div>
     39<canvas id=canvas></canvas>
     40</body>
     41<script src="static/firework.js"></script>
     42<script src="static/content.js"></script>
     43""")
     44
     45register_template = base_template.format(body="""
     46<body class=registerpage>
     47    <meta id="pow_prefix" content="{pow_prefix}">
     48    <div id=main>
     49    {navbar}
     50    <h1>Register:</h1>
     51    <p>Our cryptographically secure login system is based on challenge-response
     52    using the digital signature algorithm (DSA).<br>In other words,
     53    <i>military-grade</i> encryption and utterly undefeatable!! 😎</p>
     54    <br>
     55    <div id=registerform class=form>
     56    <table>
     57    <tbody>
     58    <tr><th>Name:</th><td><input type=text name=username id=username></td></tr>
     59    <tr><th>P:</th><td><input type=text name=p id=p></td></tr>
     60    <tr><th>Q:</th><td><input type=text name=q id=q></td></tr>
     61    <tr><th>G:</th><td><input type=text name=g id=g></td></tr>
     62    <tr><th>X:</th><td><input type=text name=x id=x></td></tr>
     63    <tr><th>Y:</th><td><input type=text name=y id=y></td></tr>
     64    </tbody>
     65    </table>
     66    <div>
     67    <a class=left onclick=gen_privkey()>Generate</a>
     68    <a class=left onclick=copy_privkey()>Copy</a>
     69    <a class=right onclick=do_register()>Register</a>
     70    </div>
     71    </div>
     72    <br>
     73    <p id=errorlog></p>
     74    </div>
     75</body>
     76<script src="static/spark-md5.min.js"></script>
     77<script src="static/content.js"></script>
     78""")
     79
     80login_template = base_template.format(body="""
     81<body class=loginpage>
     82    <div id=main>
     83    {navbar}
     84    <h1>Login:</h1>
     85    <p>Our cryptographically secure login system is based on challenge-response
     86    using the digital signature algorithm (DSA).<br>In other words,
     87    <i>military-grade</i> encryption and utterly undefeatable!! 😎</p>
     88    <br>
     89    <div id=loginform class=form>
     90    <table>
     91    <tbody>
     92    <tr><th>Name:</th><td><input type=text name=username id=username></td></tr>
     93    <tr><th>Challenge:</th>
     94        <td><input type=text name=challenge id=challenge
     95            value="..." readonly></td></tr>
     96    <tr><th>Signature:</th>
     97        <td><input type=text name=signature id=signature></td></tr>
     98    </tbody>
     99    </table>
    100    <div>
    101    <a class=right onclick=do_login()>Login</a>
    102    </div>
    103    </div>
    104    <br>
    105    <p id=errorlog></p>
    106    </div>
    107</body>
    108<script src="static/content.js"></script>
    109""")
    110
    111profile_template = base_template.format(body="""
    112<body class=profilepage>
    113    <div id=main>
    114        {navbar}
    115        <div class=container>
    116        <div id=proplist>
    117        <h2>Properties:</h2>
    118        {proplist}
    119        </div>
    120        <div id=eventlog>
    121        <h2>Events:</h2>
    122        {eventlog}
    123        </div>
    124        </div>
    125    </div>
    126</body>
    127""")
    128
    129inspire_template = base_template.format(body="""
    130<body class=quotepage>
    131    <div id=main>
    132        {navbar}
    133        <div id=quote>
    134        <p>{quote}</p>
    135        - V
    136        </div>
    137    </div>
    138</body>
    139""")
    140
    141inspire_navbar_html = """
    142<div id="glitch">
    143    <a href="/inspire">
    144    <span class="glitch__color glitch__color--red">Inspire</span>
    145    <span class="glitch__color glitch__color--blue">Inspire</span>
    146    <span class="glitch__color glitch__color--green">Inspire</span>
    147    <span class="glitch__color glitch__main">Inspire</span>
    148    <span class="glitch__line glitch__line--first"></span>
    149    <span class="glitch__line glitch__line--second"></span>
    150    </a>
    151</div>
    152"""
    153
    154navbar_nouser_template = """
    155<div id=navbar>
    156<div class=left><a href=/>Home</a></div>
    157<div class=right><a href=/register>Register</a></div>
    158<div class=right><a href=/login>Login</a></div>
    159</div>
    160"""
    161
    162navbar_user_template = """
    163<div id=navbar>
    164<div class=left><a href=/>Home</a></div>
    165{inspire}
    166<div class=right><a href=/logout>Logout</a></div>
    167<div class=right><a href=/profile>{username}</a></div>
    168</div>
    169"""
    170
    171quotes = [
    172    "The service is a symbol, as is the act of exploiting it. Symbols are "
    173    "given power by the people. Alone, a symbol is meaningless, but with "
    174    "enough people, exploiting a service can change the world.",
    175    "Every exploit is special.",
    176    "Users shouldn't be afraid of their services. Services should be afraid "
    177    "of their users.",
    178    "She used to tell me that god was in the code.",
    179    "Your pretty service took so long to build, now, with a snap of "
    180    "history's fingers, down it goes.",
    181    "Challenge authors use unsafe code to teach safe code.",
    182    "You wear cat ears for so long, you forget who you were underneath them.",
    183    "Every time I've seen this code change, it's always been for the worse.",
    184    "I remember our teacher telling us that writing unsafe code was an "
    185    "adolescent phase people outgrew. Sara did. I didn't.",
    186    "Strength through Unity, Unity through good code!",
    187    "Bugs will plunge this country back into chaos and I will not let that happen.",
    188    "There's no flesh or blood within this code to kill. There's only an idea. "
    189    "Ideas are bulletproof."
    190]
    191
    192sockets = []
    193
    194def log(*args, **kwargs):
    195    print(*args, **kwargs, file=sys.stderr)
    196
    197def html_table(entries, header="top"):
    198    html = "<table>"
    199    for y, row in enumerate(entries):
    200        html += "<tr>"
    201        for x, val in enumerate(row):
    202            if y == 0 and header == "top" or x == 0 and header == "left":
    203                html += "<th>" + str(val) + "</th>"
    204            else:
    205                html += "<td>" + str(val) + "</td>"
    206        html += "</tr>"
    207    html += "</table>"
    208    return html
    209
    210def html_response(html):
    211    return web.Response(text=html, content_type="text/html")
    212
    213def gen_navbar(session):
    214    if "username" in session:
    215        if random.randint(0, 5) == 0:
    216            inspire = inspire_navbar_html
    217        else:
    218            inspire = ""
    219        return navbar_user_template.format(
    220            inspire=inspire, username=session["username"])
    221    else:
    222        return navbar_nouser_template
    223
    224async def handle_main(request):
    225    session = await get_session(request)
    226    navbar = gen_navbar(session)
    227    html = main_template.format(navbar=navbar)
    228    return html_response(html)
    229
    230async def handle_ws(request):
    231    ws = web.WebSocketResponse()
    232    await ws.prepare(request)
    233    sockets.append(ws)
    234
    235    try:
    236        async for msg in ws:
    237            continue
    238    finally:
    239        sockets.remove(ws)
    240        await ws.close()
    241
    242    return ws
    243
    244async def handle_register(request):
    245    session = await get_session(request)
    246
    247    if request.method == "GET":
    248        navbar = gen_navbar(session)
    249        session["pow_prefix"] = "".join(random.choices("0123456789abcdef", k=5))
    250        html = register_template.format(navbar=navbar,
    251                pow_prefix=session["pow_prefix"])
    252        return html_response(html)
    253
    254    if "userid" in session:
    255        return web.Response(status=400, text="Already logged in")
    256
    257    try:
    258        params = await request.post()
    259        username = params["username"]
    260        p = int(params["p"])
    261        q = int(params["q"])
    262        g = int(params["g"])
    263        x = int(params["x"])
    264        y = int(params["y"])
    265    except KeyError:
    266        return web.Response(status=400, text="Missing params")
    267    except ValueError:
    268        return web.Response(status=400, text="Invalid params")
    269
    270    username = params["username"]
    271    if len(username) < 4:
    272        return web.Response(status=400, text="Username too short")
    273
    274    privkey = crypto.DSAKey(p, q, g, x, y)
    275
    276    try:
    277        sql = "INSERT INTO users (name,p,q,g,x,y) values (?,?,?,?,?,?)"
    278        vals = [str(v) for v in privkey.vals()]
    279        res = await db.execute_insert(sql, (username, *vals))
    280        userid = res[0]
    281        await db.commit()
    282    except Exception as e:
    283        traceback.print_exc()
    284        return web.Response(status=400, text="Registration failed (database)")
    285
    286    session["username"] = username
    287    session["userid"] = userid
    288
    289    return web.Response(status=200, text="OK")
    290
    291async def handle_login(request):
    292    session = await get_session(request)
    293
    294    if request.method == "GET":
    295        navbar = gen_navbar(session)
    296        html = login_template.format(navbar=navbar)
    297        return html_response(html)
    298
    299    if "userid" in session:
    300        return web.Response(status=400, text="Already logged in")
    301
    302    try:
    303        params = await request.post()
    304        username = params["username"]
    305        challenge = int(params["challenge"])
    306        r,s = params["signature"].split(",")
    307        signature = (int(r), int(s))
    308    except KeyError as e:
    309        return web.Response(status=400, text="Missing param " + str(e))
    310    except ValueError:
    311        if "," not in params["signature"]:
    312            return web.Response(status=400, text="Signature format: r,s")
    313        else:
    314            return web.Response(status=400, text="Invalid integers")
    315
    316    if len(username) < 4:
    317        return web.Response(status=400, text="Username too short")
    318
    319    if "challenge" not in session:
    320        return web.Response(status=400, text="Challenge not set")
    321
    322    if challenge != session["challenge"]:
    323        return web.Response(status=400, text="Expired challenge")
    324    session.pop("challenge")
    325
    326    sql = "SELECT id,p,q,g,x,y FROM users WHERE name = ?"
    327    async with db.execute(sql, (username,)) as cursor:
    328        row = await cursor.fetchone()
    329        if row is None:
    330            return web.Response(status=400, text="No such user")
    331        userid = row[0]
    332        privkey = crypto.DSAKey(*[int(v) for v in row[1:]])
    333        pubkey = privkey.pubkey()
    334
    335    try:
    336        assert pubkey.verify(challenge, signature)
    337    except Exception as e:
    338        if not isinstance(e, AssertionError):
    339            trace = traceback.format_exc()
    340            log(f"Login signature verify failed:\n{trace}")
    341        text = "Verify failed! Expected signature:\n"
    342        try:
    343            r,s = privkey.sign(challenge)
    344            text += f"{r},{s}"
    345        except:
    346            trace = traceback.format_exc()
    347            log(f"Generating correct signature failed:\n{trace}")
    348            text += "SIGN FAILED"
    349        return web.Response(status=400, text=text)
    350
    351    session["username"] = username
    352    session["userid"] = userid
    353
    354    return web.Response(status=200, text="OK")
    355
    356async def handle_logout(request):
    357    session = await get_session(request)
    358    session.invalidate()
    359    return web.HTTPFound("/")
    360
    361async def handle_inspire(request):
    362    session = await get_session(request)
    363    navbar = gen_navbar(session)
    364    quote = random.choice(quotes)
    365    html = inspire_template.format(navbar=navbar, quote=quote)
    366    return html_response(html=html)
    367
    368async def handle_profile(request):
    369    session = await get_session(request)
    370    try:
    371        username = request.match_info["username"]
    372    except:
    373        if "username" not in session:
    374            return web.HTTPFound("/login")
    375        username = session["username"]
    376
    377    sql = "SELECT p,q,g,x,y FROM users WHERE name = ?"
    378    async with db.execute(sql, (username,)) as cursor:
    379        row = await cursor.fetchone()
    380        if row is None:
    381            return web.Response(status=400, text="Missing info (database)")
    382        privkey = crypto.DSAKey(*[int(v) for v in row])
    383
    384    data = [("name", username),]
    385    if "username" in session:
    386        data += privkey.dict().items()
    387    else:
    388        data += privkey.pubkey().dict().items()
    389    proplist = html_table(data, header="left")
    390
    391    data = [("time", "x", "y", "wish")]
    392    if "username" in session:
    393        userid = session["userid"]
    394        sql = "SELECT x,y,time,wish FROM events WHERE userid = ?"
    395        async with db.execute(sql, (userid,)) as cursor:
    396            async for row in cursor:
    397                x = f"{row[0]:.3f}"
    398                y = f"{row[1]:.3f}"
    399                time, wish = row[2:]
    400                data.append((time, x, y, wish))
    401    eventlog = html_table(data, header="top")
    402
    403    navbar = gen_navbar(session)
    404    html = profile_template.format(navbar=navbar,
    405            proplist=proplist, eventlog=eventlog)
    406    return html_response(html)
    407
    408async def handle_genkey(request):
    409    session = await get_session(request)
    410
    411    if "pow_prefix" not in session:
    412        return web.Response(status=400, text="Invalid session")
    413
    414    try:
    415        work = request.query["pow"]
    416        pow_prefix = request.query["pow_prefix"]
    417    except KeyError:
    418        return web.Response(status=400, text="Missing param " + str(e))
    419
    420    if pow_prefix != session["pow_prefix"]:
    421        return web.Response(status=400, text="Expired pow prefix")
    422
    423    md5hash = md5(work.encode()).hexdigest()
    424    if not md5hash.startswith(session["pow_prefix"]):
    425        return web.Response(status=400, text="Bad proof of work")
    426
    427    try:
    428        privkey = crypto.DSAKey.gen()
    429    except Exception as e:
    430        return web.Response(status=400, text="Private key generation failed")
    431
    432    data = {k:str(v) for k,v in privkey.dict().items()}
    433    return web.Response(status=200, text=json.dumps(data))
    434
    435async def handle_challenge(request):
    436    session = await get_session(request)
    437
    438    session["challenge"] = crypto.gen_challenge()
    439    return web.Response(text=str(session["challenge"]))
    440
    441async def handle_launch(request):
    442    session = await get_session(request)
    443
    444    if "userid" not in session:
    445        return web.Response(status=400, text="Not logged in")
    446    userid = session["userid"]
    447
    448    try:
    449        params = await request.post()
    450        x = float(params["x"])
    451        y = float(params["y"])
    452        wish = params["wish"] if "wish" in params else ""
    453    except (KeyError, ValueError):
    454        return web.Response(status=400, text="Missing / invalid params")
    455
    456    time = datetime.strftime(datetime.now(), "%H:%M:%S")
    457    sql = "INSERT INTO events (userid,time,wish,x,y) values (?,?,?,?,?)"
    458    await db.execute(sql, (userid, time, wish, x, y))
    459    await db.commit()
    460
    461    event = {
    462        "type": "firework",
    463        "x": x,
    464        "y": y
    465    }
    466
    467    for ws in sockets:
    468        await ws.send_str(json.dumps(event))
    469
    470    return web.Response(status=200)
    471
    472async def create_runner():
    473    app = web.Application()
    474    redis_host = os.getenv("REDIS_HOST")
    475    redis_port = os.getenv("REDIS_PORT")
    476    redis = await aioredis.from_url(f"redis://{redis_host}:{redis_port}")
    477    storage = aiohttp_session.redis_storage.RedisStorage(redis)
    478    aiohttp_session.setup(app, storage)
    479    app.add_routes([
    480        web.get('/', handle_main),
    481        web.get('/ws', handle_ws),
    482        web.get("/register", handle_register),
    483        web.post("/register", handle_register),
    484        web.get("/login", handle_login),
    485        web.post("/login", handle_login),
    486        web.get("/logout", handle_logout),
    487        web.get("/inspire", handle_inspire),
    488        web.get("/profile", handle_profile),
    489        web.get("/profile/{username}", handle_profile),
    490        web.get("/genkey", handle_genkey),
    491        web.get("/challenge", handle_challenge),
    492        web.post("/launch", handle_launch),
    493        web.static("/static", "static")
    494    ])
    495    return web.AppRunner(app)
    496
    497async def main():
    498    global db
    499    db = await aiosqlite.connect("data/db.sqlite")
    500    await db.execute("PRAGMA foreign_keys = ON")
    501    runner = await create_runner()
    502    await runner.setup()
    503    site = web.TCPSite(runner, "0.0.0.0", 1812)
    504    await site.start()
    505
    506if __name__ == "__main__":
    507    loop = asyncio.new_event_loop()
    508    asyncio.set_event_loop(loop)
    509    loop.run_until_complete(main())
    510    loop.run_forever()
    511