diff --git a/web-security/level-12/.config b/web-security/level-12/.config deleted file mode 100644 index 48082f7..0000000 --- a/web-security/level-12/.config +++ /dev/null @@ -1 +0,0 @@ -12 diff --git a/web-security/level-12/DESCRIPTION.md b/web-security/level-12/DESCRIPTION.md new file mode 100644 index 0000000..9f5df1f --- /dev/null +++ b/web-security/level-12/DESCRIPTION.md @@ -0,0 +1,9 @@ +Recall that requests that originate from JavaScript run into the Same-Origin Policy, which slightly complicated our CSRF in the previous level. +You figured out how to make a `GET` request without JavaScript. +Can you do the same for `POST`? + +Recall that a typical `POST` request is a result of either a JavaScript-invoked request (no good for SOP) or an HTML form submission. +You'll need to do the latter. +Of course, the `/challenge/victim` won't click the `Submit` button for you --- you'll have to figure out how to do that yourself (HINT: JavaScript can click that button; the request will still count as originating from the form!). + +Go `POST`-CSRF to the flag! diff --git a/web-security/level-12/run b/web-security/level-12/run deleted file mode 120000 index 84ba55b..0000000 --- a/web-security/level-12/run +++ /dev/null @@ -1 +0,0 @@ -../run \ No newline at end of file diff --git a/web-security/level-12/server b/web-security/level-12/server new file mode 100755 index 0000000..fd5b26e --- /dev/null +++ b/web-security/level-12/server @@ -0,0 +1,104 @@ +#!/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.getuid() == 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]) +# 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") + + flask.session["username"] = username + return flask.redirect("/") + +@app.route("/draft", methods=["POST"]) +def challenge_draft(): + if "username" not in flask.session: + flask.abort(403, "Log in first!") + + content = flask.request.form.get("content", "") + # https://www.sqlite.org/lang_insert.html + db.execute( + "INSERT INTO posts (content, author, published) VALUES (?, ?, ?)", + (content, flask.session.get("username"), bool(flask.request.form.get("publish"))) + ) + return flask.redirect("/") + +@app.route("/publish", methods=["POST"]) +def challenge_publish(): + if "username" not in flask.session: + flask.abort(403, "Log in first!") + + # https://www.sqlite.org/lang_update.html + db.execute("UPDATE posts SET published = TRUE WHERE author = ?", [flask.session.get("username")]) + return flask.redirect("/") + +@app.route("/", methods=["GET"]) +def challenge_get(): + page = "\nWelcome to pwnpost, now XSS-free (for admin, at least)!
\n" + username = flask.session.get("username", None) + if username == "admin": + page += """To prevent XSS, the admin does not view messages!""" + elif username: + page += """ +
+ Post: + Publish + +

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

Author: {post["author"]}

""" + if 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) +app.run("challenge.localhost", 8080 if os.geteuid() else 80) diff --git a/web-security/level-12/victim b/web-security/level-12/victim new file mode 100755 index 0000000..584ac4b --- /dev/null +++ b/web-security/level-12/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) +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!") diff --git a/web-security/module.yml b/web-security/module.yml index bac9cf4..3ead53f 100644 --- a/web-security/module.yml +++ b/web-security/module.yml @@ -48,7 +48,6 @@ challenges: name: CSRF 1 - id: level-12 name: CSRF 2 - description: Exploit a cross site request forgery vulnerability where the request must POST - id: level-13 name: CSRF 3 description: Exploit a cross site scripting vulnerability to exfiltrate user session data