Skip to content

Instantly share code, notes, and snippets.

@0xdevalias
Last active May 2, 2024 15:40
Show Gist options
  • Star 23 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • 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) apps

Reverse Engineering Webpack Apps

Some notes and techniques for reverse engineering Webpack 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:

See Also

The following aren't strictly related to Webpack, but they're useful tools and techniques to be aware of in general when debuygging / reversing web apps:

My Other Related Deepdive Gist's and Projects

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 - 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

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