-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
207 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,10 @@ | ||
Let's start putting a few things together... | ||
A CSRF can lead to many things, including other injections! | ||
Use the CSRF in this level to trigger a XSS and invoke an `alert("PWNED")` somewhere in `http://challenge.localhost`! | ||
|
||
---- | ||
**HINT:** | ||
You will likely want to use JavaScript on your `http://hacker.localhost:1337` page to send `GET` request with `<script>` tags in a URL parameter. | ||
Be careful: your `<script>` tag will have the word `</script>` in a string (the URL parameter). | ||
This will string `</script>` will actually be parsed by your browser as the closing tag of your page's actual `<script>` tag, and all hell will break loose. | ||
I recommend dynamically building that string (e.g., `"</s"+"cript>") in the JavaScript that runs on `http://hacker.localhost:1337`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,9 @@ | ||
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! | ||
|
||
---- | ||
**HINT:** | ||
Your solution might have two levels of JavaScript: one that runs on your `http://hacker.localhost:1337` page, and one that runs in the reflected XSS. | ||
We suggest testing the latter first, by manually triggering the page with that input and seeing the result. | ||
Furthermore, as this code might be complex, be VERY careful about URL encoding. | ||
For example, `+` will _not_ be encoded to `%2b` by most URL encoders, but _it is a special character in a URL_ and gets decoded to a space (` `). | ||
Needless to say, if you use `+` in your JavaScript, this can lead to complete havoc. |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
This level closes the loophole that allowed you to steal cookies from JavaScript. | ||
Cookies have a special setting called `httponly`, and when this is set, the cookie is _only_ accessible in HTTP headers, and not through JavaScript. | ||
This is a security measure, aimed to prevent exactly the type of cookie pilfering that you have been doing. | ||
Luckily, Flask's default `session` cookie is set to be `httponly`, so you cannot steal it from JavaScript. | ||
|
||
So, now how would you get the flag with your CSRF-to-XSS shenanigans? | ||
Luckily, you don't _need_ the cookie! | ||
Once you have JavaScript execution within the page, you can freely `fetch()` other pages without worrying about the Same Origin Policy, since you now live in the same Origin. | ||
Use this, read the page with the flag, and win! |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
#!/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") | ||
|
||
flask.session["username"] = username | ||
return flask.redirect("/") | ||
|
||
@app.route("/draft", methods=["POST"]) | ||
def challenge_draft(): | ||
username = flask.session.get("username", None) | ||
if not username: | ||
flask.abort(403, "Log in first!") | ||
|
||
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 = flask.session.get("username", None) | ||
if not username: | ||
flask.abort(403, "Log in first!") | ||
|
||
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""" | ||
<html><body> | ||
<h1>You have received an ephemeral message!</h1> | ||
The message: {flask.request.args.get("msg", "(none)")} | ||
<hr><form>Craft an ephemeral message:<input type=text name=msg action=/ephemeral><input type=submit value=Submit></form> | ||
</body></html> | ||
""" | ||
|
||
@app.route("/", methods=["GET"]) | ||
def challenge_get(): | ||
page = "<html><body>\nWelcome to pwnpost, now with users!<hr>\n" | ||
username = flask.session.get("username", None) | ||
if username: | ||
page += """ | ||
<form action=draft method=post> | ||
Post:<textarea name=content>Write something!</textarea> | ||
<input type=checkbox name=publish>Publish | ||
<input type=submit value=Save> | ||
</form><br> | ||
<form action=publish method=post><input type=submit value="Publish All Drafts"></form><hr> | ||
""" | ||
|
||
for post in db.execute("SELECT * FROM posts").fetchall(): | ||
page += f"""<h2>Author: {post["author"]}</h2>""" | ||
if post["author"] == username: | ||
page += "<b>YOUR POST:</b> " + post["content"] + "<hr>\n" | ||
elif username == "admin": | ||
page += "<b>NON-ADMIN POST HIDDEN FOR SAFETY</b>" | ||
elif post["published"]: | ||
page += post["content"] + "<hr>\n" | ||
else: | ||
page += f"""(Draft post, showing first 12 characters):<br>{post["content"][:12]}<hr>""" | ||
else: | ||
page += """ | ||
<form action=login method=post> | ||
Username:<input type=text name=username> | ||
Password:<input type=text name=password> | ||
<input type=submit name=submit value=Login> | ||
</form><hr> | ||
""" | ||
|
||
return page + "</body></html>" | ||
|
||
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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!") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters