NodeJS - Abusing Lazy Loading Technique

Exploiting Lazy Loading technique for remote code execution

Table of Contents

Introduction

During vulnerability research on a containerized Node.js application, I discovered a critical arbitrary file overwrite vulnerability. Initial attempts to overwrite core application files like app.js proved ineffective due to two key constraints:

  • Application Restart Required: Modifying app.js required restarting the entire application.

  • Container Reset Protection: Crashes triggered immediate container resets that restored all original files.

However, I discovered the developers were using lazy loading patterns throughout the application:

Example: Lazy Loading Pattern

async content() {
    // Lazy loading - module loaded only when function is called
    const { generate } = require('random-words');
    const words = generate(5);
    const sentence = words.join(' ');
    return sentence;
}

What is Lazy Loading

"Lazy loading is a technique where modules or dependencies are loaded only when they are needed, rather than during the application's startup phase. This approach minimizes memory usage and reduces startup time, especially in applications with numerous dependencies."

Node.js Module Caching Behavior

Node.js caches modules after their first require() call. Once a module is loaded and cached, subsequent require() calls return the cached version, even if the original file has been modified on disk. This behavior is crucial to our attack.

Exploitation Strategy

This gave me an idea: what if I could modify an uncached module to abuse the lazy loading technique and gain remote code execution?

After analyzing the application's front-end, I identified:

  • Unused API routes not accessible through the UI that use lazy loading.

  • Crash triggers - actions that would force container resets and clear cached modules.

Attack Vectors

Direct Lazy Loading

  1. Overwrite lazy-loaded modules (avoiding core files like app.js).

  2. Trigger the target endpoint to load the poisoned module.

  3. Execute payload when Node.js loads the malicious code.

Clearing Cached Modules

For already-cached modules:

  1. Crash the application intentionally to clear Node.js module cache.

  2. Wait for the container to reset, clearing the cache.

  3. Overwrite lazy-loaded modules (avoiding core files like app.js).

  4. Trigger the target endpoint to load the poisoned module.

  5. Execute payload when Node.js loads the malicious code.

Demonstration

To demonstrate this vulnerability, I've created a proof-of-concept environment consisting of:

  • A vulnerable Node.js Express application with file upload and crash endpoint.

  • Exploitation script showcasing different attack scenarios

Application source code

31KB
Open

Start the application

Run the application inside a container

Vulnerable endpoint for file upload

Crash Endpoint

This crash endpoint is crucial for the cache-clearing attack vector, as it forces the container to restart and clear the Node.js module cache. This used in the second scenario.

Uncached lazy module Scenario #1

Since the content endpoint uses lazy module loading, it isn't called at application startup, leaving its module uncached in Node.js.

Overwrite the random-words to gain remote code execution

This script demonstrates successful exploitation when the module hasn't been cached yet.

Evidence

Clearing Cached Modules #2

Abusing both Lazy Modules and container mechanisms by Crashing to Clear the Module Cache.

Note: Reset the application container to return the old state after scenario #1.

Call the content endpoint first to ensure the module is cached

Evidence for the unexploitable scenario

This final script demonstrates the complete attack chain: triggering a cached module, failing to exploit, then crashing the container to clear the cache and successfully exploiting:

Evidence for RCE after crashing the application and container reset

the modified script was loaded successfully after crashing to clear the cached modules.

Last updated

Was this helpful?