Skip to content

Instantly share code, notes, and snippets.

@0xdevalias
Last active May 15, 2024 05:55
Show Gist options
  • Save 0xdevalias/8c621c5d09d780b1d321bfdb86d67cdd to your computer and use it in GitHub Desktop.
Save 0xdevalias/8c621c5d09d780b1d321bfdb86d67cdd to your computer and use it in GitHub Desktop.
Some notes and techniques for reverse engineering Webpack (and a little bit about React/Vue/Angular) apps

Reverse Engineering Webpack Apps

Some notes and techniques for reverse engineering Webpack (and a little bit about React/Vue/Angular) apps.

Table of Contents

Webpack (and similar bundlers) Chunk Format

The structure of a webpack chunk file seems to be:

  • a top level default assignment to ensure self.webpackChunk_N_E exists and is an array
  • .push() into that an array containing:
    • an array containing the chunk ID (eg. [1234])
    • an object containing the module mappings within this chunk (key=moduleId, value='module function')
      • the 'module function' generally takes 3 parameters
        • module
        • exports
        • require
    • a function for doing 'chunk init' type things

AST Structure

Program .body -> ExpressionStatement .expression ->
  CallExpression
    .callee -> MemberExpression
      .object -> AssignmentExpression
        .left -> MemberExpression
          .object -> Identifier .name -> "self"
          .property -> Identifier .name -> "webpackChunk_N_E"
        .operator -> "="
        .right -> LogicalExpression
          .left -> MemberExpression
            .object -> Identifier .name -> "self"
            .property -> Identifier .name -> "webpackChunk_N_E"
          .operator -> "||"
          .right -> ArrayExpression .elements[] -> <empty array>
      .property -> Identifier .name -> "push"
    .arguments[0] -> ArrayExpression .elements[]
      -> ArrayExpression .elements[0] -> NumericLiteral .value -> <the chunk ID>
      -> ObjectExpression .properties[] -> ObjectProperty ... <the modules within this chunk>
      -> FunctionExpression -> <used for chunk initialisation/etc>
        .params[] -> ...
        .body[]
          -> VariableDeclaration -> ...
          -> ExpressionStatement .expression -> SequenceExpression .expressions[]
            -> CallExpression ->
              .callee -> ...
              .arguments -> ...
            -> AssignmentExpression
              .left -> Identifier .name -> "_N_E"
              .operator -> "="
              .right -> CallExpression ...

Basic minified example

The following is a very basic minified example showing the basic structure of the chunk file:

  (self.webpackChunk_N_E = self.webpackChunk_N_E || []).push([
    [1234],
    {
      1111: function (Y, et, en) { /* module code */ },
      2222: function (Y, et, en) { /* module code */ },
    },
    function (Y) {
      var et = function (et) {
        return Y((Y.s = et));
      };
      Y.O(0, [9774, 179], function () {
        return et(54885), et(11867);
      }),
        (_N_E = Y.O());
    },
  ]);
  //# sourceMappingURL=_app-abcdef123456.js.map

Detailed unminified example + comments

The following more complete example chunk was generated based on examples seen in the wild + ChatGPT used to refine the improved naming/comments/etc:

(self.webpackChunk_N_E = self.webpackChunk_N_E || []).push([
  [1234], // Chunk ID
  { // Module definitions...
    // Module 1: Exporting a utility function using `module.exports`
    1111: function (module, exports, require) {
      // Utility function defined in Module 1
      function utilityFunction() {
        console.log("Utility function from Module 1.");
      }

      // Exporting the utility function as the module's public interface
      module.exports = utilityFunction;
    },

    // Module 2: Using `exports` to export functions and `require` to import from Module 1
    2222: function (module, exports, require) {
      // Requiring the utility function from Module 1
      var utilityFunction = require(1111); // Using the module ID to require Module 1

      // Function that uses the imported utilityFunction
      function functionUsingUtility() {
        console.log("Function in Module 2 calling utility function:");
        utilityFunction(); // Using the utility function from Module 1
      }

      // Another standalone function in Module 2
      function standaloneFunction() {
        console.log("Standalone function in Module 2.");
      }

      // Exporting both functions as properties of the exports object
      exports.functionUsingUtility = functionUsingUtility;
      exports.standaloneFunction = standaloneFunction;
    },
  },
  function (module) {
    // Initialization function: Sets up the chunk and initializes its modules.

    // 'init' function: Loads and executes a specific module by its ID.
    var init = function (moduleId) {
      // Sets the current module ID ('module.s') and loads the module.
      return module((module.s = moduleId));
    };

    // 'module.O' method: Part of webpack's runtime, handling module and chunk loading.
    // First Call to 'module.O': Handles loading of dependent chunks.
    module.O(0, [9774, 179], function () {
      // This callback is executed once all dependent chunks (9774, 179) are loaded.
      // Dependent chunks are other chunks that this chunk needs before it can be initialized.
      // For example, these could be vendor chunks or previously split code chunks that contain shared libraries or components.
    }).then(function () {
      // Second Call to 'module.O': Initializes the entry modules of this chunk.
      // Once dependent chunks are loaded, we proceed to initialize this chunk's entry modules (54885, 11867).
      // These module IDs represent the starting points or root modules of this chunk, which kickstart the functionality encapsulated within the chunk.
      module.O(0, [], function () {
        return init(54885), init(11867);
      }, 1); // The '1' here might indicate a different operation type, such as initializing entry modules.

      // '_N_E' assignment: Marks the chunk as successfully initialized.
      // 'module.O()' returns a promise that resolves when the chunk is fully loaded and initialized.
      // This assignment can be used for debugging or for chaining further actions once initialization is complete.
      (_N_E = module.O());
    });
  },
]);

Older Notes

After like basically a day of crazy deep webpack knowledge aquisition the hard way around.. I had enough understanding and keywords to find this article, which would have in many ways probably saved me a good chunk of this time/figuring out of things:

But I guess what it doesn't mention is that you can access window.webpackChunk_N_E from global scope to see all of the loaded modules, and you can tell it to load a new module with window.webpackChunk_N_E.push, so you can basically arbitrarily define a new module and inject it at runtime with something like:

window.webpackChunk_N_E.push([
  [1337],
  {31337: (U, B, G) => console.log('chunk[1]', { U, B, G })},
  (U) => console.log('chunk[2]', U)
])

Where:

  • 1337 is the module chunk ID (relatively arbitrary choice, though used to reference the module from other code)
  • 31337 is a function defined within that module (can define more than 1) where U, B, G == module, __webpack_exports__, __webpack_require__
  • And the final function I'm not 100% sure about.. I think it might get called to preload any dependencies, or do something else like that... but in any case, it gets called when you first .push your module, so you can use it to get access to U (which I believe is also __webpack_require__)

And then __webpack_require__ is both a function that can be called, but also has a bunch of helper methods that can be accessed like accessing the modules object (aka __webpack_modules__), accessing the module cache (aka: installedModules), being able to access both harmony and __esModule exports, getting the defaultExport, calling properties on an object's prototype, seeing the module build path, etc.

So I haven't put all that together in a useful/runnable PoC gadget chain yet... but I believe that is basically all of the background knowledge required to be able to do so

In a similar space as the above, it might be worth looking at the following:

Specifically this implemention that is mentioned there:

Runtime Injection / Overrides

The following are a few scripts/methods that should allow overriding/injecting into Webpack modules at runtime (without requiring the use of a debugger/breakpoints/etc to do so).

Method 1: Direct Overrides

Note: The following code hasn't been fully tested/evaluated yet.

/**
 * Injects wrappers into specified webpack modules to allow manipulation or stubbing.
 * @param {Object} overrides - An object where keys are module IDs and values are functions that take the old module function and return a new module function.
 */
function injectWebpackOverrides(overrides) {
  window.webpackChunk_N_E.forEach(chunk => {
    const [_, modules] = chunk;
    Object.keys(modules).forEach(moduleId => {
      if (Object.hasOwn(modules, moduleId)) {
        const originalModule = modules[moduleId];
        // Check if there is an override for the module and apply it
        if (overrides.hasOwnProperty(moduleId)) {
          // The override function receives the original module and returns a new module function
          modules[moduleId] = overrides[moduleId](originalModule);
        }
      }
    });
  });
}

/**
 * Creates a wrapped require function that can intercept module requests.
 * @param {Function} originalRequire - The original require function.
 * @param {Object} overrides - The overrides definitions.
 * @returns {Function} The wrapped require function.
 */
function createWrappedRequire(originalRequire, overrides) {
  return (moduleId) => {
    if (Object.hasOwn(overrides, moduleId)) {
      return overrides[moduleId](originalRequire(moduleId));
    }
    return originalRequire(moduleId);
  };
}

// Example usage:
injectWebpackOverrides({
  '51510': (oldModule) => {
    // Create a wrapped require function directly with the specific overrides for modules this module depends on
    const wrappedRequire = createWrappedRequire(oldModule.require, {
      '48779': (originalModule48779) => {
        // Modify or replace the behavior of module 48779
        return {
          ...originalModule48779,
          someFunction: () => 'Modified behavior'
        };
      }
    });

    // Returns a new function that uses the wrapped require
    return (e, t, a) => {
      // Call the old module function using the wrappedRequire
      return oldModule(e, t, wrappedRequire);
    };
  },
  '48779': (oldModule) => {
    // Return a completely new function, replacing the old module
    return (e, t, a) => {
      // Return directly whatever is needed for the shim
      return { /* shimmed module contents */ };
    };
  }
});

Method 2: Module Proxies

Note: The following code hasn't been fully tested/evaluated yet.

/**
 * Injects wrappers into specified webpack modules to allow manipulation or stubbing using Proxies.
 * @param {Object} overrides - An object where keys are module IDs and values are functions that take the old module function and return a new module function.
 */
function injectWebpackOverrides(overrides) {
  window.webpackChunk_N_E.forEach(chunk => {
    const [_, modules] = chunk;

    // Create a proxy to handle the modules object
    const modulesProxy = new Proxy(modules, {
      get(target, moduleId) {
        if (overrides.hasOwnProperty(moduleId)) {
          // Check if the moduleId is part of the overrides and if the override should be applied
          const originalModule = target[moduleId];
          // Return the overridden module function without modifying the original target
          return overrides[moduleId](originalModule);
        }
        // Return the original module if there is no override
        return target[moduleId];
      }
    });

    chunk[1] = modulesProxy;  // Replace the original modules with the proxied version
  });
}

Method 3: Chunk and Module Proxies

Note: The following code hasn't been fully tested/evaluated yet.

/**
 * Injects wrappers into specified webpack modules across all chunks using a global Proxy.
 * @param {Object} overrides - An object where keys are module IDs and values are functions that take the old module function and return a new module function.
 */
function injectWebpackOverrides(overrides) {
  // Wrapping the entire webpackChunk_N_E array with a Proxy
  window.webpackChunk_N_E = new Proxy(window.webpackChunk_N_E, {
    get(target, prop) {
      // Access the chunk
      const chunk = target[prop];
      if (Array.isArray(chunk)) {
        const [chunkId, modules] = chunk;

        // Use another Proxy to handle the modules within the chunk
        const modulesProxy = new Proxy(modules, {
          get(target, moduleId) {
            moduleId = moduleId.toString();  // Ensure moduleId is a string
            if (overrides.hasOwnProperty(moduleId)) {
              const originalModule = target[moduleId];
              // Apply override only if not previously applied
              if (!originalModule.__proxyWrapped) {
                target[moduleId] = overrides[moduleId](originalModule);
                target[moduleId].__proxyWrapped = true;
              }
            }
            return target[moduleId];
          }
        });

        // Return the chunk with the proxied modules
        return [chunkId, modulesProxy];
      }
      return chunk;
    }
  });
}

React Internals

Vue Internals

  • https://www.damianmullins.com/inspecting-a-vue-application-in-production/
    • The Vue.js devtools are great to use when in the process of developing a web application. However, once you deploy to production it no longer has access to the code it needs to work. So how can we inspect an application once released to production? In this post, we’ll walk through the options available to you along with some tricks and tips to make the process a little easier.

    • Now refresh the page and once the breakpoint is hit then type Vue.config.devtools = true in the devtools console. In Vue.js, by default, the devtools config setting is determined by the value of process.env.NODE_ENV, what we’re doing here is manually setting that value to true in order to force Vue.js to initiate the Vue.js devtools.

    • Once you have the element selected you can then move to the console panel in devtools and type $0. $0 will be a reference to the most recently selected element in the element panel. To see the Vue instance details you can type $0.__vue__ Now that we have a reference to the Vue component instance we can expand it in the console to see what’s inside

  • https://www.damianmullins.com/logging-vuex-actions-and-mutations-in-the-wild/
    • const store = [...document.querySelectorAll('*')]
       .find(el => el.__vue__)
       .__vue__
       .$store;
    • const store = document.querySelector('[data-app]')
       .__vue__
       .$store;
let allElements = document.querySelectorAll('*');

// Filter for elements with __vue__ property
let vueElements = [...allElements]
  .filter(el => el.__vue__)
  .map(element => ({
    element,
    vue: element.__vue__,
  }));

console.log(vueElements);

The following is a snippet from Vue devtools (chrome-extension://nhdogjmejiglipccpnnnanhbledajbpd/build/detector.js)

We can see the unminified source here:

  • https://github.com/vuejs/devtools/blob/main/packages/shell-chrome/src/detector.js
    • // Method 1: Check Nuxt
      • window.__NUXT__ || window.$nuxt
      • Vue = window.$nuxt.$root && window.$nuxt.$root.constructor
      • devtoolsEnabled: (/* Vue 2 */ Vue && Vue.config.devtools) || (/* Vue 3.2.14+ */ window.__VUE_DEVTOOLS_GLOBAL_HOOK__ && window.__VUE_DEVTOOLS_GLOBAL_HOOK__.enabled)
    • // Method 2: Check Vue 3
      • window.__VUE__
      • devtoolsEnabled: /* Vue 3.2.14+ */ window.__VUE_DEVTOOLS_GLOBAL_HOOK__ && window.__VUE_DEVTOOLS_GLOBAL_HOOK__.enabled
    • // Method 3: Scan all elements inside document
      • const all = document.querySelectorAll('*')
        let el
        for (let i = 0; i < all.length; i++) {
          if (all[i].__vue__) {
            el = all[i]
            break
          }
        }
        if (el) {
          let Vue = Object.getPrototypeOf(el.__vue__).constructor
          while (Vue.super) {
            Vue = Vue.super
          }
      • devtoolsEnabled: Vue.config.devtools
      • if (detectRemainingTries > 0) {
          detectRemainingTries--
          setTimeout(() => {
            runDetect()
          }, delay)
          delay *= 5
        }
      • win.postMessage({
          devtoolsEnabled: Vue.config.devtools,
          vueDetected: true,
        }, '*')
      • window.addEventListener('message', e => {
          if (e.source === window && e.data.vueDetected) {
            chrome.runtime.sendMessage(e.data)
          }
        })
function n(e) {
  let t = 1e3,
    n = 10;
  function r() {
    const o = !(!window.__NUXT__ && !window.$nuxt);
    if (o) {
      let t;
      return (
        window.$nuxt &&
          (t = window.$nuxt.$root && window.$nuxt.$root.constructor),
        void e.postMessage(
          {
            devtoolsEnabled:
              (t && t.config.devtools) ||
              (window.__VUE_DEVTOOLS_GLOBAL_HOOK__ &&
                window.__VUE_DEVTOOLS_GLOBAL_HOOK__.enabled),
            vueDetected: !0,
            nuxtDetected: !0,
          },
          "*"
        )
      );
    }
    const i = !!window.__VUE__;
    if (i)
      return void e.postMessage(
        {
          devtoolsEnabled:
            window.__VUE_DEVTOOLS_GLOBAL_HOOK__ &&
            window.__VUE_DEVTOOLS_GLOBAL_HOOK__.enabled,
          vueDetected: !0,
        },
        "*"
      );
    const s = document.querySelectorAll("*");
    let a;
    for (let e = 0; e < s.length; e++)
      if (s[e].__vue__) {
        a = s[e];
        break;
      }
    if (a) {
      let t = Object.getPrototypeOf(a.__vue__).constructor;
      while (t.super) t = t.super;
      e.postMessage(
        {
          devtoolsEnabled: t.config.devtools,
          vueDetected: !0,
        },
        "*"
      );
    } else
      n > 0 &&
        (n--,
        setTimeout(() => {
          r();
        }, t),
        (t *= 5));
  }
  setTimeout(() => {
    r();
  }, 100);
}

Therefore, we could probably use a snippet like the following to tell Vue devtools that it should enable itself, even on a production site:

let firstVueElement = [...document.querySelectorAll('*')].find(el => el.__vue__);
let Vue = undefined;

if (firstVueElement) {
  Vue = Object.getPrototypeOf(firstVueElement.__vue__).constructor

  while (Vue.super) {
    Vue = Vue.super
  }
}

if (Vue) {
  console.log('Found Vue', { Vue });

  console.log('Vue.config', Vue.config);

  console.log('Setting Vue.config.devtools = true');
  Vue.config.devtools = true;
  
  console.log('Setting __VUE_DEVTOOLS_GLOBAL_HOOK__ values', { __VUE_DEVTOOLS_GLOBAL_HOOK__ });
  __VUE_DEVTOOLS_GLOBAL_HOOK__.Vue = Vue;
  __VUE_DEVTOOLS_GLOBAL_HOOK__.enabled = true;

  console.log('Signalling to Vue Devtools that it should enable itself');
  window.postMessage({
    devtoolsEnabled: true,
    vueDetected: true,
    nuxtDetected: false,
  }, '*')
} else {
  console.log('Failed to find Vue');
}

Angular Internals

The following sections of the Angular Devtools may be relevant in figuring out how to enable them even on production sites:

Chrome Devtools

Chrome - Devtools - Search

Chrome - Devtools - Console Utilities API reference

Chrome - Devtools - Console features reference

  • https://developer.chrome.com/docs/devtools/console/reference/
    • Console features reference

    • https://developer.chrome.com/docs/devtools/console/reference/#inspect-internal-properties
      • Borrowing the ECMAScript notation, the Console encloses some properties internal to JavaScript in double square brackets. You can't interact with such properties in your code. However, it might be useful to inspect them.

        • Any object has a [[Prototype]]

        • Primitive wrappers have a [[PrimitiveValue]] property

        • ArrayBuffer objects have the following properties: [[Int8Array]], [[Uint8Array]], [[Int16Array]], [[Int32Array]], [[ArrayBufferByteLength]], [[ArrayBufferData]]

        • In addition to ArrayBuffer-specific properties, WebAssembly.Memory objects have a [[WebAssemblyMemory]] property.

        • Keyed collections (maps and sets) have an [[Entries]] property that contains their keyed entries.

        • Promise objects have the following properties:

          • [[PromiseState]]: pending, fulfilled, or rejected

          • [[PromiseResult]]: undefined if pending, <value> if fulfilled, <reason> if rejected

        • Proxy objects have the following properties: [[Handler]] object, [[Target]] object, and [[isRevoked]] (switched off or not).

    • https://developer.chrome.com/docs/devtools/console/reference/#inspect-functions
      • To view function properties internal to JavaScript, use the console.dir() command.

        • https://developer.chrome.com/docs/devtools/console/api/#dir
          • Prints a JSON representation of the specified object.

        • Functions have the following properties:

          • [[FunctionLocation]]. A link to the line with the function definition in a source file.

          • [[Scopes]]. Lists values and expressions the function has access to. To inspect function scopes during debugging, see View and edit local, closure, and global properties.

        • Bound functions have the following properties:

          • [[TargetFunction]]. The target of bind()

          • [[BoundThis]]. The value of this

          • [[BoundArgs]]. An array of function arguments

        • Generator functions are marked with a [[IsGenerator]]: true property.

        • Generators return iterator objects and they have following properties:

          • [[GeneratorLocation]]. A link to a line with the generator definition in a source file

          • [[GeneratorState]]: suspended, closed, or running

          • [[GeneratorFunction]]. The generator that returned the object

          • [[GeneratorReceiver]]. An object that receives the value

    • etc

Chrome - Devtools - JavaScript debugging reference

Chrome - Devtools - Network

Chrome - Devtools Protocol, Programmatic Debugging, etc

See Also

Announcement Tweet

My Other Related Deepdive Gist's and Projects

@mrrootsec
Copy link

🔥

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment