WEKAN AUTHENTICATION BYPASS – EXPLOITING COMMON PITFALLS OF METEORJS

Wekan Authentication Bypass – Exploiting Common Pitfalls of MeteorJS

Web frameworks support the development process by providing a variety of tools to interact with data, display content, and execute business logic. These frameworks are meant to ease the development process, but they create a layer of abstraction that can leave catastrophic security vulnerabilities hiding in plain sight. While abstraction layers shield the developer from writing insecure SQL queries or handling session management, it is possible for a new class of vulnerabilities to emerge that exploit weak coding practices encouraged by the library.

In this post, we will discuss the authentication bypass vulnerability discovered in Wekan. This vulnerability (along with others in Wekan), was discovered and reported by Offensive Security.

What is Wekan?

Wekan is an open-source kanban software built using the Meteor Framework. Kanban is a method of visualizing work to be done on a project. Wekan allows users to create kanban boards with different statuses as columns. The tasks of a project are represented by cards, which can be moved into columns on a board.

Wekan supports multiple methods of installation, including Snap, Docker, and Sandstorm. For this demonstration, the instance we are targeting was installed via the “Featured Server Snaps” screen of the Ubuntu 18.04 installation step.

Wekan via Snap Install

To understand the vulnerabilities discovered in Wekan, we must first understand the framework it was built upon, Meteor.

Intro To Meteor

Meteor (or MeteorJS) is a JavaScript framework designed for cross-platform development. The documentation on Meteor’s website sums up the framework best.

Meteor applications differ from most applications in that they include code that runs on the client, inside a web browser or Cordova mobile app, code that runs on the server, inside a Node.js container, and common code that runs in both environments.

In essence, Meteor allows a developer to write one code base that runs the server, web client, and mobile application. As you can imagine, the developers of Meteor had to rethink the architecture of standard web applications. They needed to develop a method to allow generic actions that occur on the server or clients to map to specific actions that change the data being stored. This redesign adds a layer of abstraction which shifts the types of vulnerabilities away from generic injections to more logic-based types of vulnerabilities.

This layer of abstraction can be seen with Meteor’s use of collections, subscriptions, publications, and methods. Let’s begin by examining collections.

Collections

Collections is the method that Meteor uses to store data (user information, boards, etc.). When a collection is created in the code that is executed on the server-side, Meteor directly interacts with the Mongo database to store the data. However, when a collection is created by code executed on the client-side, the data is stored in a cache.

Publication and Methods

For a client to obtain data from the database, the server-side code creates a publication and the client-side code creates a subscription to the publication. Since the client-side code does not have direct access to MongoDB, to save data to the database the client must call a method which is executed on the server-side.

The publications and subscriptions utilize the Distributed Data Protocol(DDP) developed by Meteor for communication between the client and server. DDP utilizes WebSockets, allowing for full-duplex communication between the client and server. One of the parameters that publications and methods accept is the this context, which includes information about the DDP connection. The DDP connection tracks the user login state and contains, among other fields, the userId field. The userId field in DDP references the current user logged in. In the context of publications and methods, this allows a developer using the Meteor framework to validate a user session, given the existence of the this.userId field.

From a security standpoint, the publications and methods that are exposed will be very important. If a publication is exposed without the proper checks, an attacker might be able to obtain data that the developer never intended to be exposed. If a method is exposed without proper checks, an attacker might be able to write data to the database without proper authorization.

In addition to the methods that the developer creates, Meteor, by default, creates mutation methods for each collection. These mutation methods allow for insert, update, and remove actions that can be run from the client-side code. This can be disabled by setting defineMutationMethods to false.

Even though a method might only need to be executed on the server side, methods are still sent to the client (with the exception of methods that are defined in the /server directory). This means that we should be able to find the methods we have access to and what they do by reviewing the client-side code. We should also note that a developer could ensure that methods are only executed by the server (though they are still sent to the client) by checking if Meteor.isServer is true.

Now that we have the fundamentals out of the way, let’s move on to the vulnerability.

Our Environment

The authentication bypass vulnerability in Wekan that we will be exploiting was fixed in March 2020 with the release of 3.81. For this demonstration, we are using version 3.80 installed on Ubuntu 18.04 Server via the Snap store. We installed Wekan during the installation of the Ubuntu OS using the “Featured Server Snaps”. After the initial installation, we created one user (this user will become our victim) and one public board (which will be used to collect information about the user), and we configured Wekan with an SMTP server (to allow password resets).

Discovering the Vulnerability

We will be escalating privileges using an unauthenticated session. For this exploit to work, we will need to find the victim’s userId. The ID is not intended to be a secret and there are multiple ways of obtaining it. For simplicity’s sake, we have created a public board that we have access to via a link. While the board is public, it can only be accessed via a link that contains the board ID. It is important to note that Wekan does not come preconfigured with any public boards. At the end of this article, we will discuss an additional method of obtaining the userId if a public board is not available.

We will first start by visiting the public board in our browser.

Public Board Accessed via URL

With the board open, we will open the JavaScript console in the Firefox developer tools.

Opening Dev Tools

From here, we can access the collections created by Wekan by calling them directly (Users, Boards, Lists).

Accessing Collections

Since a user created this board and added a card, the user collection should be populated. We can query for all users in the collection by using the find() function. The find() function returns a Cursor and the documents discovered can be returned as an array by running the fetch() function.

Users.find().fetch()
Accessing All Users on Board

Running the command above returns an array of users on this public board. The user that is active on this board has the username “admin”. This vulnerability does not only target admin users, but any user that is registered in Wekan. We’ll take a note of the user’s id (2ewotSg5oQLakNxes) since this user will become our target.

In addition to the collections, we can also access the DDP connection and all the information sent over. This is accessible via the Meteor.connection object.

Accessing DDP Connection

As we discussed earlier, the DDP connection stores quite a bit of information. One piece of information that is particularly interesting to us is the list of Meteor methods that are defined and callable by the client. This is accessible via the _methodHandlers object.

Accessing _methodHanders

The methods that start with the / character and end with insert, remove, or update are the mutation methods included in every collection (unless specifically removed). The methods that we want to first examine are towards the bottom of the list.

» Meteor.connection._methodHandlers
​▼{...}
​    ▶"/AccountsLockout.Connections/insert": function l()
    ▶"/AccountsLockout.Connections/remove": function l()
    ▶"/AccountsLockout.Connections/update": function l()
    ...
    ▶"/unsaved-edits/remove": function l()
    ▶"/unsaved-edits/update": function l()
    ▶"/users/insert": function l()
    ▶"/users/remove": function l()
    ▶"/users/update": function l()
    ▶ATRemoveService: function ATRemoveService()
    ▶applyWipLimit: function applyWipLimit()
    ▶changeLimitToShowCardsCount: function changeLimitToShowCardsCount()
    ▶cloneBoard: function cloneBoard()
    ▶enableSoftLimit: function enableSoftLimit()
    ▶enableWipLimit: function enableWipLimit()
    ▶importBoard: function importBoard()
    ▶setCreateUser: function setCreateUser()
    ▶setEmail: function setEmail()
    ▶setListSortBy: function setListSortBy()
    ▶setPassword: function setPassword()
    ▶setUsername: function setUsername()
    ▶setUsernameAndEmail: function setUsernameAndEmail()
    ▶toggleDesktopDragHandles: function toggleDesktopDragHandles()
    ▶toggleMinicardLabelText: function toggleMinicardLabelText()
    ▶toggleSystemMessages: function toggleSystemMessages()

Specifically, the setEmail and setPassword methods warrant further investigation. Let’s first examine the setPassword method. To obtain the source, we can call the toSource() function on the setPassword function object.

Accessing Source of setPassword

Using a JavaScript beautifier, we can view the source cleaner.

setPassword(e, t) {
    check(t, String),
    check(e, String),
    Meteor.user().isAdmin && Accounts.setPassword(t, e)
}

It appears that the setPassword first checks that all parameters are strings. The minified JavaScript does not provide enough context for us to determine what the e or t parameter are. Next, the function will check if the user is an administrator, if the user is an administrator, then the function will set the password. While this method is on the client-side, the execution occurs on the server. In other words, we cannot spoof the user as admin or run the setPassword function directly, as this is intended to be executed on the server. Instead, let’s examine the setEmail function.

Accessing Source of setEmail

We will again run this through a JavaScript beautifier for a cleaner output.

setEmail(e, t) {
    Array.isArray(e) && (e = e.shift()),
    check(e, String);
    const a = Users.findOne({
        "emails.address": e
    }, {
        fields: {
            _id: 1
        }
    });
    if (a) throw new Meteor.Error("email-already-taken");
    Users.update(t, {
        $set: {
            emails: [{
                address: e,
                verified: !1
            }]
        }
    })
}

The setEmail function takes two parameters. By reviewing the code, we can deduce that e is the email and t is a token or userId (based on the call to the Users.update function and the use of t as a selector). The first few lines of the function check if the email is a valid string. Next, using this email, the application will attempt to find a user with that email address. If an email is found, an “email-already-taken” error will be thrown. If the email does not already exist, an update call will be made to change the user where the ID matches the one passed in.

The setEmail method does not contain any additional validations. This method will update the user’s email value on the server and all we need to provide is the userId. In addition, we should be able to invoke this method directly from the client and provide an email and token.

Exploitation

Now that we believe we have discovered the vulnerability, let’s attempt to exploit it. The setEmail method can be invoked via Meteor.call. The first argument is the name of the method we would like to invoke (setEmail). Next, we need to pass in the arguments to the method. In this case, the first argument will be the email we would like to change the target to. The second argument will be the ID we discovered earlier (2ewotSg5oQLakNxes).

The call that we will run in the JavaScript command will be the following:

Meteor.call('setEmail', "evil@example.com", "2ewotSg5oQLakNxes");

Executing this call returned an error.

Error During Execution

However, let’s go to the login page and attempt a password reset using our new email address.

Submit Password Reset

If we check our inbox, we should receive an email with the following contents.

Hello admin,

To reset your password, simply click the link below.

http://wekan.local:8080/reset-password/GK7WbbHhk-FCuZjM1DjuDd7g98dBoYEubmKUjFcazPv

Thanks.

If we follow that link, we should get to the password reset page.

Reset Password

Typing in a new password and clicking on “Set Password” results in a successful “Password Reset” message and a redirection to the home page of the logged-in user.

Logged in as Admin

Success! We have bypassed the authentication mechanism and logged in as an administrator.

Additional ways to obtain userId

In this demo, we discovered the user’s id by viewing a public board. However, without this link, we would need another method to obtain the user’s ID. If we don’t have access to a public board, we can obtain a userId by brute forcing usernames and emails for existing accounts.

A review of the Wekan code allows us to discover the publications that are accessible to us. One of the more interesting publications can be found in the server/publications/users.js file. The publication is named user-authenticationMethod.

Meteor.publish('user-authenticationMethod', function(match) {
  check(match, String);
  return Users.find(
    { $or: [{ _id: match }, { email: match }, { username: match }] },
    {
      fields: {
        authenticationMethod: 1,
      },
    },
  );
});

This publication allows the frontend to know how to handle the authentication of the user. By providing an id, email, or username, the authentication method is sent back. By responding with the authenticationMethod, the id of the user will also be sent back.

The input for the brute force can be either username, id, or email. Once a valid user is found, the Users collection is populated with the userId and the authentication mechanism. Below is a proof of concept to brute force usernames.

usernames = ["admin", "administrator", "demo"]
for (i = 0; i < usernames.length; i++) {
  Meteor.subscribe('user-authenticationMethod', usernames[i])
}

This code can be executed in the dev console.

Brute Forcing the Username

Once complete, we can use the same Users.find().fetch() command to list all users discovered.

Fetch after Brute Force

The response contains the same ID that we received by reviewing at the public board.

Conclusion

We were able to discover a workflow to allow unauthenticated users to change emails of active users. After changing the email to one in our control, we were able to use the standard password reset flow to change the password of the target.

Understanding an application’s framework can assist us during security assessments, penetration tests, and bug bounties. In this demo, we examined the types of vulnerabilities you might find during a pentest of an application built using Meteor.


Dejan Zelic is an AWAE Content Developer at Offensive Security. He has worked as a Penetration Tester at a large fintech company concentrating on web application security. Dejan has helped organize Capture the Flag events at AppSec USA 2017, AppSec USA 2018, and CactusCon 2017. He has competed with the CTF team “Savage Submarine” to take first place at the CMD+CTRL CTF at DefCon 25. You’ll often find Dejan working on anything that has to do with Web Application Hacking, AWS hacking, and automation development. You can reach out to him on Twitter(@dejandayoff) or on his security blog: https://dejandayoff.com/