-
Notifications
You must be signed in to change notification settings - Fork 11
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
6 changed files
with
203 additions
and
3 deletions.
There are no files selected for viewing
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,22 @@ | ||
You've used XSS to inject JavaScript to cause the victim to make HTTP requests. | ||
But what if there is no XSS? | ||
Can you just "inject" the HTTP requests directly? | ||
|
||
Shockingly, the answer is yes. | ||
The web was designed to enable interconnectivity across many different websites. | ||
Sites can embed images from other sites, link to other sites, and even _redirect_ to other sites. | ||
All of this flexibility represents some serious security risks, and there is almost nothing preventing a malicious website from simply directly causing a victim visitor to make potentially sensitive requests, such as (in our case) a `GET` request to `http://challenge.localhost/publish`! | ||
|
||
This style of _forging_ requests _across_ sites is called _Cross Site Request Forgery_, or CSRF for short. | ||
|
||
Note that I said _almost_ nothing prevents this. | ||
The [Same-origin Policy](https://en.wikipedia.org/wiki/Same-origin_policy) was created in the 1990s, when the web was still young, to (try to) mitigate this problem. | ||
SOP prevents a site at one Origin (say, `http://www.hacker.com` or, in our case, `http://hacker.localhost:1337`) from interacting in certain security-critical ways with sites at other Origins (say, `http://www.asu.edu` or, in our case, `http://challenge.localhost/`). | ||
SOP prevents some common CSRF vectors (e.g., when using JavaScript to make a requests across Origins, cookies will not be sent!), but there are plenty of SOP-avoiding ways to, e.g., make `GET` requests with cookies intact (e.g., `<img>`s, `<iframe>`s, or even just full-on redirects). | ||
|
||
In this level, pwnpost has fixed its XSS issues (at least for the `admin` user). | ||
You'll need to use CSRF to publish the flag post! | ||
The `/challenge/victim` of this level will log into pwnpost (`http://challenge.localhost/`) and will then visit an evil site that you can set up (`http://hacker.localhost:1337/`). | ||
`hacker.localhost` points to your local workspace, but you will need to set up a web server to serve an HTTP request on port 1337 yourself. | ||
Again, this can be done with `nc`. | ||
Because these sites will have different Origins, SOP protections will apply, so be careful about how you forge the request! |
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,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=["GET"]) | ||
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 = "<html><body>\nWelcome to pwnpost, now XSS-free (for admin, at least)!<hr>\n" | ||
username = flask.session.get("username", None) | ||
if username == "admin": | ||
page += """<b>To prevent XSS, the admin does not view messages!</b>""" | ||
elif 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><a href=publish>Publish your drafts!</a><hr> | ||
""" | ||
|
||
for post in db.execute("SELECT * FROM posts").fetchall(): | ||
page += f"""<h2>Author: {post["author"]}</h2>""" | ||
if 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) | ||
app.run("challenge.localhost", 8080 if os.geteuid() else 80) |
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,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) | ||
|
||
with run_browser() as browser: | ||
try: | ||
print("First logging in as admin to http://challenge.localhost:80/") | ||
try: | ||
port = 80 | ||
browser.get("http://challenge.localhost:80") | ||
except WebDriverException as e: | ||
if "can%E2%80%99t%20establish%20a%20connection" not in str(e): | ||
raise | ||
print("Port 80 doesn't seem to be listening. Trying http://challenge.localhost:8080/") | ||
port = 8080 | ||
browser.get("http://challenge.localhost:8080") | ||
|
||
print("pwnpost loaded!") | ||
print("Logging in as admin...") | ||
login = { "username": "admin", "password": open("/flag").read().strip() if port != 8080 else "pwn.college{fake_flag}" } | ||
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("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!") | ||
except WebDriverException as e: | ||
if "can%E2%80%99t%20establish%20a%20connection" in str(e): | ||
print("Connection error! Is the server running?") | ||
else: | ||
print(f"Failure: {e}...") | ||
sys.exit(4) |
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