diff --git a/web-security/module.yml b/web-security/module.yml index 40992de..7d8985c 100644 --- a/web-security/module.yml +++ b/web-security/module.yml @@ -42,6 +42,8 @@ challenges: name: XSS 5 - id: xss-rf-post name: XSS 6 +- id: xss-exfil-cookie + name: XSS 7 - id: level-11 name: CSRF 1 description: Exploit a cross site request forgery vulnerability diff --git a/web-security/xss-exfil-cookie/DESCRIPTION.md b/web-security/xss-exfil-cookie/DESCRIPTION.md new file mode 100644 index 0000000..64fde6c --- /dev/null +++ b/web-security/xss-exfil-cookie/DESCRIPTION.md @@ -0,0 +1,14 @@ +Depending on the attacker's goals, what they might actually be after is the victim's entire account. +For example, attackers might use XSS to exfiltrate victim authentication data and then use this data to take over the victim's account. + +Authentication data is often stored via browser cookies, such as what happened in `Authentication Bypass 2` (but, typically, much more secure). +If an attacker can leak these cookies, the result can be disastrous for the victim. + +This level stores the authentication data for the logged in user in a cookie. +You must use XSS to leak this cookie so that you can, in turn, use it in a request to impersonate the `admin` user. +This exfiltration will happen over HTTP to a server that you run, and everything you need is available via JavaScript's `fetch()` and its ability to access (some) site cookies. + +---- +**HINT:** +By "server that you run", we really mean that listening on a port with `nc` will be sufficient. +Look at the `-l` and `-v` options to `nc`. diff --git a/web-security/xss-exfil-cookie/server b/web-security/xss-exfil-cookie/server new file mode 100755 index 0000000..0aee702 --- /dev/null +++ b/web-security/xss-exfil-cookie/server @@ -0,0 +1,117 @@ +#!/opt/pwn.college/python + +import tempfile +import sqlite3 +import flask +import os + +app = flask.Flask(__name__) + +class TemporaryDB: + def __init__(self): + self.db_file = tempfile.NamedTemporaryFile("x", suffix=".db") + + def execute(self, sql, parameters=()): + connection = sqlite3.connect(self.db_file.name) + connection.row_factory = sqlite3.Row + cursor = connection.cursor() + result = cursor.execute(sql, parameters) + connection.commit() + return result + +flag = open("/flag").read().strip() if os.geteuid() == 0 else "pwn.college{fake_flag}" + +db = TemporaryDB() +# https://www.sqlite.org/lang_createtable.html +db.execute("""CREATE TABLE IF NOT EXISTS posts AS SELECT ? AS content, "admin" AS author, FALSE AS published""", [flag]) +db.execute("""CREATE TABLE IF NOT EXISTS users AS SELECT "admin" AS username, ? as password""", [flag[-10:]]) +# https://www.sqlite.org/lang_insert.html +db.execute("""INSERT INTO users SELECT "guest" as username, "password" as password""") +db.execute("""INSERT INTO users SELECT "hacker" as username, "1337" as password""") + +@app.route("/login", methods=["POST"]) +def challenge_login(): + username = flask.request.form.get("username") + password = flask.request.form.get("password") + if not username: + flask.abort(400, "Missing `username` form parameter") + if not password: + flask.abort(400, "Missing `password` form parameter") + + # https://www.sqlite.org/lang_select.html + user = db.execute("SELECT * FROM users WHERE username = ? AND password = ?", (username, password)).fetchone() + if not user: + flask.abort(403, "Invalid username or password") + + response = flask.redirect("/") + response.set_cookie('auth', username+"|"+password) + return response + +@app.route("/draft", methods=["POST"]) +def challenge_draft(): + username, password = flask.request.cookies.get("auth", "|").split("|") + user = db.execute("SELECT * FROM users WHERE username = ? AND password = ?", (username, password)).fetchone() + if not user: + flask.abort(403, "Invalid username or password") + + if username == "admin": + flask.abort(400, "pwnpost no longer supports admin posting due to rampant flag disclosure") + + content = flask.request.form.get("content", "") + # https://www.sqlite.org/lang_insert.html + db.execute( + "INSERT INTO posts (content, author, published) VALUES (?, ?, ?)", + (content, username, bool(flask.request.form.get("publish"))) + ) + return flask.redirect("/") + +@app.route("/publish", methods=["POST"]) +def challenge_publish(): + username, password = flask.request.cookies.get("auth", "|").split("|") + user = db.execute("SELECT * FROM users WHERE username = ? AND password = ?", (username, password)).fetchone() + if not user: + flask.abort(403, "Invalid username or password") + + if username == "admin": + flask.abort(400, "pwnpost no longer supports admin posting due to rampant flag disclosure") + + # https://www.sqlite.org/lang_update.html + db.execute("UPDATE posts SET published = TRUE WHERE author = ?", [username]) + return flask.redirect("/") + +@app.route("/", methods=["GET"]) +def challenge_get(): + page = "\nWelcome to pwnpost, now with users!
\n" + username, password = flask.request.cookies.get("auth", "|").split("|") + user = db.execute("SELECT * FROM users WHERE username = ? AND password = ?", (username, password)).fetchone() + if user: + page += """ +
+ Post: + Publish + +

+

+ """ + + for post in db.execute("SELECT * FROM posts").fetchall(): + page += f"""

Author: {post["author"]}

""" + if post["published"]: + page += post["content"] + "
\n" + elif post["author"] == username: + page += "YOUR DRAFT POST: " + post["content"] + "
\n" + else: + page += f"""(Draft post, showing first 12 characters):
{post["content"][:12]}
""" + else: + page += """ +
+ Username: + Password: + +

+ """ + + return page + "" + +app.secret_key = os.urandom(8) +app.run("challenge.localhost", 8080 if os.geteuid() else 80) diff --git a/web-security/xss-exfil-cookie/victim b/web-security/xss-exfil-cookie/victim new file mode 100755 index 0000000..e54fae6 --- /dev/null +++ b/web-security/xss-exfil-cookie/victim @@ -0,0 +1,77 @@ +#!/opt/pwn.college/python + +import contextlib +import urllib +import time +import sys +import os + +from selenium import webdriver +from selenium.webdriver.firefox.options import Options as FirefoxOptions +from selenium.webdriver.firefox.service import Service as FirefoxService +from selenium.webdriver.common.by import By +from selenium.webdriver.support.wait import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.common.exceptions import TimeoutException, WebDriverException + +os.setuid(os.geteuid()) +os.environ.clear() +os.environ["PATH"] = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + +@contextlib.contextmanager +def run_browser(): + options = FirefoxOptions() + options.add_argument("--headless") + + # workaround for ubuntu + if os.path.exists("/snap/bin/geckodriver"): + service = FirefoxService(executable_path="/snap/bin/geckodriver", log_path="/dev/null") + else: + service = FirefoxService(log_path="/dev/null") + driver = webdriver.Firefox(service=service, options=options) + + try: + yield driver + finally: + driver.quit() + +if len(sys.argv) <= 1: + print(f"Usage: {sys.argv[0]} URL") + sys.exit(1) + +url = sys.argv[1] +url_parsed = urllib.parse.urlparse(url) +try: + assert url_parsed.hostname == "challenge.localhost", "hostname should be 'challenge.localhost'" + assert url_parsed.port in {None, 80, 8080}, "port should be 80 or 8080" +except AssertionError as e: + print(f"Invalid URL: {e}") + sys.exit(2) + + +print("Visiting the URL!") +with run_browser() as browser: + try: + browser.get(url) + print("URL loaded...") + + print("Logging in as admin...") + login = { + "username": "admin", + "password": (open("/flag").read().strip() if url_parsed.port != 8080 else "pwn.college{fake_flag}")[-10:] + } + for name, value in login.items(): + field = browser.find_element(By.NAME, name) + field.send_keys(value) + submit_field = browser.find_element(By.NAME, "submit") + submit_field.submit() + + WebDriverWait(browser, 10).until(EC.staleness_of(submit_field)) + time.sleep(3) + print("Visited! Go check if the attack worked!") + except WebDriverException as e: + if "can%E2%80%99t%20establish%20a%20connection" in str(e): + print("Connection error! Is the service running?") + else: + print(f"Failure: {e}...") + sys.exit(4)