In the depths of the Frontier, Armaxis powers the enemy’s dominance, dispatching weapons to crush rebellion. Fortified and hidden, it controls vital supply chains. Yet, a flaw whispers of opportunity, a crack to expose its secrets and disrupt their plans. Can you breach Armaxis and turn its power against tyranny?
We are given two URLs, one is the website itself, and the other is a mail client logged in as [email protected]. We are also given the source code for the challenge. After reading the source code, I found a vulnerability where the reset password token can be used for any user. When doing a password reset, the code only checks if the email exists, and it does not check if the token belongs to the email provided. This means we can use the email we have, and we could reset the admin's password.
router.post("/reset-password/request",async (req, res) => {const { email } =req.body;if (!email) returnres.status(400).send("Email is required.");try {constuser=awaitgetUserByEmail(email);if (!user) returnres.status(404).send("User not found.");constresetToken=crypto.randomBytes(16).toString("hex");constexpiresAt=Date.now() +3600000;awaitcreatePasswordReset(user.id, resetToken, expiresAt);awaittransporter.sendMail({ from:"[email protected]", to: email, subject:"Password Reset", text:`Use this token to reset your password: ${resetToken}`, });res.send("Password reset token sent to your email."); } catch (err) {console.error("Error processing reset request:", err);res.status(500).send("Error processing reset request."); }});router.post("/reset-password",async (req, res) => {const { token,newPassword,email } =req.body; // Added 'email' parameterif (!token ||!newPassword ||!email)returnres.status(400).send("Token, email, and new password are required.");try {constreset=awaitgetPasswordReset(token);if (!reset) returnres.status(400).send("Invalid or expired token.");constuser=awaitgetUserByEmail(email);if (!user) returnres.status(404).send("User not found.");awaitupdateUserPassword(user.id, newPassword);awaitdeletePasswordReset(token);res.send("Password reset successful."); } catch (err) {console.error("Error resetting password:", err);res.status(500).send("Error resetting password."); }});
The /weapons/dispatch endpoint is only accessible to the admin, further solidifying the approach I mentioned previously.
The POST handler also calls the parseMarkdownfunction, where any images on the markdown will be accessed using the curlcommand. But the command execution is not sanitized, therefore vulnerable to command injection. Then the result of the command execution will be stored as Base64 and rendered on an imgtag.
On the database.jsfile, we can see that it inserts a default admin user with a random password. We can use [email protected] when resetting the admin's password.
asyncfunctioninitializeDatabase() {try {awaitrun(`CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, email VARCHAR(255) UNIQUE, password VARCHAR(255), role VARCHAR(50) )`);awaitrun(`CREATE TABLE IF NOT EXISTS weapons ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(255), price REAL, note TEXT, dispatched_to VARCHAR(255), FOREIGN KEY (dispatched_to) REFERENCES users (email) )`);awaitrun(`CREATE TABLE IF NOT EXISTS password_resets ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, token VARCHAR(64) NOT NULL, expires_at INTEGER NOT NULL, FOREIGN KEY (user_id) REFERENCES users (id) )`);constuserCount=awaitget(`SELECT COUNT(*) as count FROM users`);if (userCount.count ===0) {constinsertUser=db.prepare(`INSERT INTO users (email, password, role) VALUES (?, ?, ?)`, );construnInsertUser=promisify(insertUser.run.bind(insertUser));awaitrunInsertUser("[email protected]",`${crypto.randomBytes(69).toString("hex")}`,"admin", );insertUser.finalize();console.log("Seeded initial users."); } } catch (error) {console.error("Error initializing database:", error); }}
From the Dockerfile, we can also see that the flag will be stored at /flag.txt.
# Use Node.js base image with Alpine LinuxFROM node:alpine# Install required dependencies for MailHog and supervisordRUN apk add --no-cache \ wget \ supervisor \ apache2-utils \ curl# Install MailHog binaryWORKDIR /RUN wget https://github.com/mailhog/MailHog/releases/download/v1.0.1/MailHog_linux_amd64RUN chmod +x MailHog_linux_amd64# Prepare email directory and copy app filesRUN mkdir -p /emailCOPY email-app /emailWORKDIR /emailRUN npm install# Generate a random password and create authentication file for MailHogRUN RANDOM_VALUE=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1) \ && htpasswd -nbBC 10 test "$RANDOM_VALUE" > /mailhog-auth \ && echo $RANDOM_VALUE > /email/password.txt# Set working directory for the main appWORKDIR /app# Copy challenge files and install dependenciesCOPY challenge .RUN npm install# Copy supervisord configurationCOPY config/supervisord.conf /etc/supervisor/conf.d/supervisord.conf# Expose ports for the app and email clientEXPOSE 8080EXPOSE 1337COPY flag.txt /flag.txt# Start supervisordCMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
Solution
First off, we need to register a new account using the email we have on the mail client.
After registering, we need go through the reset password mechanism, entering [email protected]as our email.
After requesting a code, we will receive the code on our mail client.
Now, we need to start over the reset password mechanism but now entering [email protected]as the email and using the token from when we are trying to reset [email protected].
After logging in, we can dispatch a new weapon and injecting a command using markdown images on the note.
Then, the flag will be stored as a Base64 on the image source.