From 122648968f17d581e8f9fe929c5569e63f8e2b62 Mon Sep 17 00:00:00 2001 From: Yan Date: Mon, 2 Sep 2024 01:36:41 -0700 Subject: [PATCH] refactored old-13 (CSRF 4) --- web-security/level-13/.config | 1 - web-security/level-13/DESCRIPTION.md | 1 + web-security/level-13/run | 1 - web-security/level-13/server | 131 +++++++++++++++++++++++++++ web-security/level-13/victim | 56 ++++++++++++ 5 files changed, 188 insertions(+), 2 deletions(-) delete mode 100644 web-security/level-13/.config create mode 100644 web-security/level-13/DESCRIPTION.md delete mode 120000 web-security/level-13/run create mode 100755 web-security/level-13/server create mode 100755 web-security/level-13/victim diff --git a/web-security/level-13/.config b/web-security/level-13/.config deleted file mode 100644 index b1bd38b..0000000 --- a/web-security/level-13/.config +++ /dev/null @@ -1 +0,0 @@ -13 diff --git a/web-security/level-13/DESCRIPTION.md b/web-security/level-13/DESCRIPTION.md new file mode 100644 index 0000000..7363791 --- /dev/null +++ b/web-security/level-13/DESCRIPTION.md @@ -0,0 +1 @@ +Okay, now that you have the CSRF-to-XSS chain figured out, pull of a CSRF leading to an XSS leading to a cookie leak that'll allow you to log in and get the flag! diff --git a/web-security/level-13/run b/web-security/level-13/run deleted file mode 120000 index 84ba55b..0000000 --- a/web-security/level-13/run +++ /dev/null @@ -1 +0,0 @@ -../run \ No newline at end of file diff --git a/web-security/level-13/server b/web-security/level-13/server new file mode 100755 index 0000000..c45a66b --- /dev/null +++ b/web-security/level-13/server @@ -0,0 +1,131 @@ +#!/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 posts AS SELECT ? AS content, "admin" AS author, FALSE AS published""", [flag]) +db.execute("""CREATE TABLE 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("/ephemeral", methods=["GET"]) +def challenge_ephemeral(): + return f""" + +

You have received an ephemeral message!

+ The message: {flask.request.args.get("msg", "(none)")} +
Craft an ephemeral message:
+ + """ + +@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["author"] == username: + page += "YOUR DRAFT POST: " + post["content"] + "
\n" + elif username == "admin": + page += "NON-ADMIN POST HIDDEN FOR SAFETY" + elif post["published"]: + page += 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) +port = 8080 if os.geteuid() else 80 +app.config['SERVER_NAME'] = f"challenge.localhost:{port}" +app.run("challenge.localhost", port) diff --git a/web-security/level-13/victim b/web-security/level-13/victim new file mode 100755 index 0000000..3eb6291 --- /dev/null +++ b/web-security/level-13/victim @@ -0,0 +1,56 @@ +#!/opt/pwn.college/python + +import psutil +import urllib +import atexit +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" + +options = FirefoxOptions() +options.add_argument("--headless") +service = FirefoxService(log_path="/dev/null") + +browser = webdriver.Firefox(service=service, options=options) +atexit.register(browser.quit) + +open_ports = { s.laddr.port for s in psutil.net_connections(kind="inet") if s.status == 'LISTEN' } +if 80 in open_ports: + print("Port 80 is open. Will connect to the service there.") + challenge_url = "http://challenge.localhost:80/" + flag = open("/flag").read().strip() +elif 8080 in open_ports: + print("Port 8080 is open. Will connect to the service there. 8080 is the debug port, so you will earn a fake flag.") + challenge_url = "http://challenge.localhost:8080/" + flag = "pwn.college{fake_flag}" +else: + print("Services don't seem to be running?") + sys.exit(1) + +print(f"Visiting {challenge_url}") +browser.get(challenge_url) + +print("Logging in as admin...") +browser.find_element(By.NAME, "username").send_keys("admin") +browser.find_element(By.NAME, "password").send_keys(flag[-10:]) +browser.find_element(By.NAME, "submit").submit() +time.sleep(3) +print("Logged in!") + +print("Visiting the attacker's website (http://hacker.localhost:1337/)...") +browser.get("http://hacker.localhost:1337/") +time.sleep(3) + +print("Visited! Go check if the attack worked!")