How CVE-2022-24785 MomentJS Path Traversal Works: Detailed Exploit Guide

How CVE-2022-24785 MomentJS Path Traversal Works: Detailed Exploit Guide

·

4 min read

CVE-2022-24785

Description

Moment.js is a JavaScript date library for parsing, validating, manipulating, and formatting dates. A path traversal vulnerability impacts npm (server) users of Moment.js between versions 1.0.1 and 2.29.1, especially if a user-provided locale string is directly used to switch moment locale. This problem is patched in 2.29.2, and the patch can be applied to all affected versions. As a workaround, sanitize the user-provided locale name before passing it to Moment.js.

Identifying the vulnerability

We decided to download an affected version of MomentJS locally via npm.

Inside of Moment-JS/node_modules/moment/src/lib/locale/locales.js there is a function named loadLocale which takes the value of name:

function loadLocale(name) {
    var oldLocale = null,
        aliasedRequire;
    // TODO: Find a better way to register and load all the locales in Node
    if (
        locales[name] === undefined &&
        typeof module !== 'undefined' &&
        module &&
        module.exports
    ) {
        try {
            oldLocale = globalLocale._abbr;
            aliasedRequire = require;
            aliasedRequire('./locale/' + name);
            getSetGlobalLocale(oldLocale);
        } catch (e) {
            // mark as not found to avoid repeating expensive file require call causing high CPU
            // when trying to find en-US, en_US, en-us for every format call
            locales[name] = null; // null means not found
        }
    }
    return locales[name];
}

The issue occurs on line 14 where the loadLocale() function dynamically requires a module based on user input (name). require() is a Node.js function used to include and load modules or JavaScript files into a Node.js application.

//require set to a variable `aliasedRequire`
const aliasedRequire = require;
// essentially require('./locale/' + name);
aliasedRequire('./locale/' + name);

If we control the name parameter we could possibly pass a traversal based string:

// Suppose name is set to a malicious value
const name = '../../someMaliciousModule'; 
aliasedRequire('./locale/' + name);

This could load ./locale/../../uploads/someMaliciousModule, potentially exposing sensitive files, or even leading to Remote Code Execution (RCE).

Proof of concept

We wrote a basic application which uses the vulnerable function to demonstrate the vulnerability. Below is the app.js code:

const express = require('express');
const moment = require('moment');

const app = express();
const port = 1337;

app.get('/time', (req, res) => {
    const locale = req.query.locale || 'en'; 

    // CVE-2022-24785 triggers at the following line in locate() function
    // locale() function passes the first parameter to require() without any sanitisation, this makes it easier to perform a path traversal attack
    const currentTime = moment().locale(locale).format('LLLL');

    res.send(`Current time (${locale}): ${currentTime}`);
});

app.listen(port, () => {
    console.log(`Server is running on https://localhost:${port}`);
});

The application listens on localhost:1337 and has an endpoint /time that accepts a query parameter locale. The application assigns the value of req.query.locale to the variable locale, defaulting to 'en' if req.query.locale is not provided. For example, if the query string is ?locale=fr, then locale would be 'fr'. On the backend, the application dynamically loads a module corresponding to the specified locale using aliasedRequire();

However, passing ?locale=../../../../../../../etc/passwd for example, does not work. When using require() in Node.js, it attempts to load JavaScript modules or files. If we were to pass a value like ../../../../../../etc/passwd to require('./locale/' + somevalue), Node.js would attempt to resolve this path relative to the current working directory of the application. However, Node.js does not directly read arbitrary files like /etc/passwd through require() because it expects modules or JavaScript files to load.

Now, let's assume the application has a file upload functionality which allows us to upload and store notes. We could use this to achieve RCE.

The path traversal combined with the ability to upload a file even .txt or note (no extension) provides us with RCE due to require();.

Is the patch secure?

Let's review the patch code for the latest version of Moment JS.

npm install moment@latest

function isLocaleNameSane(name) {
    // Prevent names that look like filesystem paths, i.e contain '/' or '\'
    // Ensure name is available and function returns boolean
    return !!(name && name.match('^[^/\\\\]*$'));
}

As of right now, there is no current way to bypass this regular expression. I've tried multiple techniques.

Credits

This discovery was a joint effort between me and my good friend Isira Adithya. Below will be Isiras Twitter/X, and his LinkedIn!

Twitter/X

LinkedIn

Well, that's all.

Essentially, that's all. It's a very basic vulnerability! As far as I am aware, no one has covered a proof of concept for this vulnerability, so this is pretty cool for everyone to see.

A Twitter/X follow is always appreciated!

Twitter/X