Description
Can you try to get access to this website to get the flag? You can download the source here. The website is running here. Can you log in?
Setup
Download the challenge source to study app/utils/seed.ts and app/utils/database.ts.
Open the target site (linked in the challenge) and monitor the /api/login request in DevTools Network tab.
Solution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
$ne operator payload and curl command used in this challenge. The Burp Suite for picoCTF guide covers the Repeater workflow for testing operator payloads against the login endpoint without retyping the JSON body every time. The Web Challenges and Real-World Bug Patterns guide catalogs the broader unsafe-deserialization bug class.Step 1
Submit crafted JSONObservationI noticed the source code in app/utils/database.ts passed the parsed request body directly into a MongoDB query without coercing field types, which suggested that substituting an operator object like {"$ne":"null"} for the password string would bypass the authentication check.Use the email from seed.ts and place {"$ne":"null"} as the password value. MongoDB interprets it as "password not equal to null," instantly passing the check.bash{"email":"joshiriya355@mumbama.com","password":{"$ne":"null"}}What didn't work first
Tried: Send the $ne payload with the JavaScript null literal instead of the string "null", like {"$ne": null}.
MongoDB treats null and the string "null" differently. Using the bare null literal filters for documents where password is not null - which sounds right - but MongoDB also returns documents where the field does not exist at all, so the behavior depends on the schema. In practice the string "null" is the safer bypass because it avoids ambiguity with missing fields. More importantly, the endpoint may coerce or sanitize the payload differently depending on the driver version, so testing both variants is worth doing if one fails.
Tried: Try a classic SQL injection string like ' OR '1'='1 in the password field instead of a JSON operator object.
MongoDB is not a relational database and does not parse SQL syntax. Sending a SQL injection string like ' OR '1'='1 is treated as a plain string value - MongoDB compares it literally against the stored password hash and the login fails. The NoSQL injection vector here requires substituting a JSON object holding a query operator (like $ne) for the scalar value, not injecting text that looks like a different query language.
Learn more
NoSQL injection exploits the fact that document databases like MongoDB accept query operators as part of the data payload itself. When an API endpoint deserializes user-supplied JSON directly into a database query object, an attacker can inject operators such as
$ne(not equal),$gt(greater than), or$regexto manipulate query logic.The
$neoperator in MongoDB means "not equal to." Sending{"password": {"$ne": "null"}}transforms the login query from "find user where password equals X" into "find user where password is not null" - which matches virtually every real account. This is the NoSQL equivalent of the classic SQLOR 1=1bypass.- The vulnerability requires that the backend passes unsanitized JSON directly to the MongoDB driver.
- The fix is to validate and whitelist input types - if a password field should be a string, reject objects.
- ORMs and query builders with parameterized queries prevent this; raw
db.collection.findOne({...userInput})does not.
Step 2
Grab the tokenObservationI noticed the /api/login endpoint returned a JSON response after the successful injection, which suggested inspecting the Network tab to find the authentication token embedded in the response body.Inspect the /api/login response. It returns a JSON array with a base64-encoded token field.Learn more
After a successful login, APIs typically return a token- commonly a JWT (JSON Web Token) or a simple base64-encoded payload - that the client attaches to subsequent requests to prove it is authenticated. Inspecting the raw response body in DevTools' Network tab (or with Burp) shows the exact structure.
Base64 is an encoding, not encryption. It converts binary data into ASCII-safe text using 64 printable characters. It is trivially reversible and provides zero confidentiality - anything base64-encoded is effectively plaintext to anyone who looks at it. Seeing base64 in an API response is always worth decoding.
JWTs have three base64url-encoded sections separated by dots: header, payload, and signature. Even without the secret key you can read the header and payload, which often contain user IDs, roles, and expiration times that reveal application logic.
Step 3
Decode the flagObservationI noticed the token field in the API response was a base64-encoded string (recognizable by its alphanumeric characters and trailing padding), which suggested decoding it with base64 -d to reveal the hidden picoCTF flag.Replace the placeholder token below with the actual base64 string from your /api/login response, then decode it with base64 -d (or CyberChef's From Base64 recipe) to recover the picoCTF flag.bashecho '<TOKEN_FROM_API_RESPONSE>' | base64 -dExpected output
picoCTF{jBhD2y7XoNzPv_1YxS9Ew5qL0uI6pasql_injection_f2f1...}Learn more
base64 -dis the standard Linux command for decoding base64 strings. Theechopipe feeds the encoded string as stdin. Note that base64 strings may include=or==padding at the end - this is normal and needed for correct decoding.CyberChef's From Base64 recipe is especially handy when the output is not clean ASCII (e.g., binary data or nested encodings), as it renders the result visually and lets you chain further operations like From Hex or Gunzip in a pipeline.
This challenge is a good reminder that sensitive data should never be embedded in tokens without encryption. Even if the flag were intended to be revealed after login, encoding it in base64 gives the illusion of protection while providing none.
Interactive tools
- SQL Injection Payload GeneratorGenerate SQL injection payloads for auth bypass, UNION extraction, blind SQLi, NoSQL operator injection, and sqlmap commands. Supports MySQL, PostgreSQL, SQLite, and MSSQL.
Flag
Reveal flag
picoCTF{jBhD2y7XoNzPv_1YxS9Ew5qL0uI6pasql_injection_f2f1...}
Decoding the token from /api/login yields the flag.
Key takeaway
How to prevent this
How to prevent this
NoSQL injection is not a MongoDB bug; it is an unsafe deserialization bug. Treat the type, not just the value.
- Coerce request fields to their expected primitive type before they touch the driver.
String(req.body.password)turns{"$ne": "null"}into the literal string"[object Object]"and the bypass dies. - Validate every input against a schema (Zod, Joi, AJV, Pydantic) at the edge of the request. Reject objects in fields that should be scalars; reject any property starting with
$. - Never spread user input into a query:
findOne({email, password})after type-checking, neverfindOne(req.body). Mongoose'sstrictQuery: trueandsanitize-html-style middleware (express-mongo-sanitize) provide a defense-in-depth layer.